Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81109f36ea | ||
|
|
d3c30859b2 | ||
|
|
2f8dcd66ad |
@@ -20,6 +20,7 @@ next-env.d.ts
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
/output/
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# Medscribe Web
|
# Medscribe Web
|
||||||
|
|
||||||
Marketing homepage for [Medscribe](../README.md) — a privacy-first, on-device medical companion for iOS and Android.
|
Public product website for [Medscribe](../README.md) — a privacy-first,
|
||||||
|
on-device medical companion in internal beta on iOS and Android.
|
||||||
|
|
||||||
Built with [create-t3-app](https://create.t3.gg/) (Next.js + TypeScript + Tailwind). Single-page site; no database or API routes.
|
Built with [create-t3-app](https://create.t3.gg/) (Next.js + TypeScript +
|
||||||
|
Tailwind). There is no product database or beta-form backend.
|
||||||
|
|
||||||
Uses **bun** exclusively (`bun.lock`).
|
Uses **bun** exclusively (`bun.lock`).
|
||||||
|
|
||||||
@@ -15,6 +17,26 @@ bun run dev
|
|||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000).
|
Open [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
The first public product slice includes:
|
||||||
|
|
||||||
|
- `/` — consumer-first portable-context story
|
||||||
|
- `/how-it-works` — inputs, review loop, medication maintenance, and handoff
|
||||||
|
- `/privacy` — data map, sharing limitations, and evidence boundaries
|
||||||
|
- `/beta` — expectations and a health-data-free email request
|
||||||
|
|
||||||
|
Named partner print material remains isolated and excluded from search:
|
||||||
|
|
||||||
|
- `/briefs/northwell`
|
||||||
|
- `/cvs`
|
||||||
|
- `/walgreens`
|
||||||
|
|
||||||
|
`/executive-summary` and `/landscape` remain retired/internal and are also
|
||||||
|
excluded from search. `SITE_PASSWORD` still gates the entire site when set; a
|
||||||
|
future pass should make protection route-specific before the public site is
|
||||||
|
deployed alongside private briefs.
|
||||||
|
|
||||||
### Analytics (optional)
|
### Analytics (optional)
|
||||||
|
|
||||||
Set in `.env` (see `.env.example`):
|
Set in `.env` (see `.env.example`):
|
||||||
@@ -31,7 +53,10 @@ bun run build
|
|||||||
bun run start
|
bun run start
|
||||||
```
|
```
|
||||||
|
|
||||||
Screenshots live in `public/screenshots/`. They are wrapped in an iPhone-style frame via [`react-device-mockup`](https://github.com/jung-youngmin/react-device-mockup).
|
Screenshots live in `public/screenshots/`. The long-form homepage uses
|
||||||
|
precomposited, transparent iPhone product-bezel assets in
|
||||||
|
`public/screenshots/pitch/`; supporting pages use the lightweight CSS frame.
|
||||||
|
Public pages label product imagery as synthetic demo data and beta UI.
|
||||||
|
|
||||||
### Capturing raw screenshots for the site
|
### Capturing raw screenshots for the site
|
||||||
|
|
||||||
@@ -39,6 +64,11 @@ Export **in-app content only** from the simulator or device — no simulator chr
|
|||||||
|
|
||||||
1. Run the app on **iPhone 17 Pro** (or 16 Pro) simulator or device, or on an **Android** emulator/device.
|
1. Run the app on **iPhone 17 Pro** (or 16 Pro) simulator or device, or on an **Android** emulator/device.
|
||||||
2. Capture each screen (⌘S in Simulator saves to Desktop; `adb exec-out screencap -p` on Android).
|
2. Capture each screen (⌘S in Simulator saves to Desktop; `adb exec-out screencap -p` on Android).
|
||||||
3. Drop PNGs into `public/screenshots/` and update paths in `src/app/page.tsx` / `src/components/feature-showcase.tsx`.
|
3. Drop PNGs into `public/screenshots/` and update paths in the relevant route
|
||||||
|
or `src/components/feature-showcase.tsx`.
|
||||||
|
|
||||||
The frame dimensions assume tall iPhone screenshots and are controlled in `src/components/phone-mockup.tsx`. Use `captureMode="simulator"` only for old captures that already include device chrome.
|
The CSS frame dimensions assume tall iPhone screenshots and are controlled in
|
||||||
|
`src/components/phone-mockup.tsx`. Use `captureMode="simulator"` only for old
|
||||||
|
captures that already include device chrome. Homepage product images are
|
||||||
|
composited separately at 810 × 1656 with transparent backgrounds and rendered
|
||||||
|
through `src/components/product-device.tsx`.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"puppeteer-core": "^25.4.0",
|
||||||
"tailwindcss": "^4.0.15",
|
"tailwindcss": "^4.0.15",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.27.0",
|
"typescript-eslint": "^8.27.0",
|
||||||
@@ -221,6 +222,8 @@
|
|||||||
|
|
||||||
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
||||||
|
|
||||||
|
"@puppeteer/browsers": ["@puppeteer/browsers@3.0.6", "", { "dependencies": { "modern-tar": "^0.7.6", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA=="],
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||||
@@ -349,6 +352,8 @@
|
|||||||
|
|
||||||
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
@@ -403,10 +408,14 @@
|
|||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"chromium-bidi": ["chromium-bidi@17.0.2", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg=="],
|
||||||
|
|
||||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||||
|
|
||||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
@@ -437,6 +446,8 @@
|
|||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"devtools-protocol": ["devtools-protocol@0.0.1653615", "", {}, "sha512-pGVkY3T/qXxAp2nFPodwYqOevk6ncNMSmvL8QfRCx5ZWGd6Vor7AFNmyaA8Zs6uJyP1QAfjuLandCgvSix1BNA=="],
|
||||||
|
|
||||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
|
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
|
||||||
@@ -469,6 +480,8 @@
|
|||||||
|
|
||||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "bin": "bin/eslint.js" }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "bin": "bin/eslint.js" }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
||||||
@@ -537,6 +550,10 @@
|
|||||||
|
|
||||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
|
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
@@ -705,6 +722,10 @@
|
|||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
|
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||||
|
|
||||||
|
"modern-tar": ["modern-tar@0.7.7", "", {}, "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.15", "", { "bin": "bin/nanoid.cjs" }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
|
"nanoid": ["nanoid@3.3.15", "", { "bin": "bin/nanoid.cjs" }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
|
||||||
@@ -767,6 +788,8 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"puppeteer-core": ["puppeteer-core@25.4.0", "", { "dependencies": { "@puppeteer/browsers": "3.0.6", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1653615", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.1" } }, "sha512-K1plkLOdeoUnGeT1OvdqF3qxl33v+Ra/uH5VyPEhXdMcpvGiEskHzxxEU3fgpccJpJLIipB/rPUsvkZRWeKqOA=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||||
@@ -831,6 +854,8 @@
|
|||||||
|
|
||||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||||
|
|
||||||
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
||||||
|
|
||||||
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
||||||
@@ -843,6 +868,8 @@
|
|||||||
|
|
||||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||||
|
|
||||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||||
|
|
||||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
@@ -881,6 +908,8 @@
|
|||||||
|
|
||||||
"typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
|
"typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
|
||||||
|
|
||||||
|
"typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.62.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.62.0", "@typescript-eslint/parser": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q=="],
|
"typescript-eslint": ["typescript-eslint@8.62.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.62.0", "@typescript-eslint/parser": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q=="],
|
||||||
@@ -893,6 +922,8 @@
|
|||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="],
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||||
@@ -905,6 +936,16 @@
|
|||||||
|
|
||||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||||
|
|
||||||
|
"ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
@@ -923,6 +964,8 @@
|
|||||||
|
|
||||||
"@unrs/resolver-binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
"@unrs/resolver-binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||||
|
|
||||||
|
"cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||||
|
|
||||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
@@ -941,6 +984,10 @@
|
|||||||
|
|
||||||
"tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
"tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||||
|
|
||||||
|
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
|
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||||
@@ -987,6 +1034,8 @@
|
|||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||||
|
|
||||||
|
"cliui/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||||
@@ -1039,6 +1088,8 @@
|
|||||||
|
|
||||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||||
|
|
||||||
|
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"puppeteer-core": "^25.4.0",
|
||||||
"tailwindcss": "^4.0.15",
|
"tailwindcss": "^4.0.15",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.27.0"
|
"typescript-eslint": "^8.27.0"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="2550" height="3300" viewBox="0 0 850 1100" preserveAspectRatio="xMidYMin slice">
|
||||||
|
<defs>
|
||||||
|
<pattern id="grid" width="28" height="28" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 28 0 L 0 0 0 28" fill="none" stroke="#0f172a" stroke-opacity="0.055" stroke-width="1"/>
|
||||||
|
</pattern>
|
||||||
|
<linearGradient id="wash" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#ccfbf1" stop-opacity="0.5"/>
|
||||||
|
<stop offset="100%" stop-color="#ccfbf1" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="850" height="1100" fill="#f8fafc"/>
|
||||||
|
<rect width="850" height="1100" fill="url(#grid)"/>
|
||||||
|
<rect width="850" height="260" fill="url(#wash)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 722 B |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 63 KiB |
@@ -0,0 +1,108 @@
|
|||||||
|
// Capture the long-form homepage in both system color schemes.
|
||||||
|
//
|
||||||
|
// bun scripts/capture-homepage.mjs [baseUrl] [outputDirectory]
|
||||||
|
//
|
||||||
|
// Requires a running local server and Google Chrome. If SITE_PASSWORD is set,
|
||||||
|
// the script uses the same signed access cookie as the login route.
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import puppeteer from "puppeteer-core";
|
||||||
|
|
||||||
|
const baseUrl = process.argv[2] ?? "http://localhost:3000";
|
||||||
|
const outputDirectory = path.resolve(process.argv[3] ?? "output");
|
||||||
|
const chrome = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
||||||
|
const password = process.env.SITE_PASSWORD;
|
||||||
|
|
||||||
|
await fs.mkdir(outputDirectory, { recursive: true });
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
executablePath: chrome,
|
||||||
|
headless: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function capture({ name, width, height, colorScheme }) {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.setViewport({ width, height, deviceScaleFactor: 1 });
|
||||||
|
await page.emulateMediaFeatures([
|
||||||
|
{ name: "prefers-color-scheme", value: colorScheme },
|
||||||
|
{ name: "prefers-reduced-motion", value: "reduce" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
const value = crypto
|
||||||
|
.createHmac("sha256", password)
|
||||||
|
.update("medscribe-site-access-v1")
|
||||||
|
.digest("base64");
|
||||||
|
await browser.setCookie({
|
||||||
|
name: "medscribe_site_access",
|
||||||
|
value,
|
||||||
|
url: baseUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.goto(baseUrl, {
|
||||||
|
waitUntil: "domcontentloaded",
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
if (page.url().includes("/login")) {
|
||||||
|
throw new Error("Homepage capture was redirected to /login.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.addStyleTag({
|
||||||
|
content:
|
||||||
|
".pitch-lazy{content-visibility:visible!important}*{animation:none!important;transition:none!important}",
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
await document.fonts.ready;
|
||||||
|
for (let y = 0; y < document.documentElement.scrollHeight; y += 700) {
|
||||||
|
window.scrollTo(0, y);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
|
}
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
});
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => Array.from(document.images).every((image) => image.complete),
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const layout = await page.evaluate(() => ({
|
||||||
|
clientWidth: document.documentElement.clientWidth,
|
||||||
|
scrollWidth: document.documentElement.scrollWidth,
|
||||||
|
scrollHeight: document.documentElement.scrollHeight,
|
||||||
|
}));
|
||||||
|
if (layout.scrollWidth > layout.clientWidth) {
|
||||||
|
throw new Error(
|
||||||
|
`${name} has horizontal overflow: ${layout.scrollWidth}px > ${layout.clientWidth}px`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination = path.join(outputDirectory, `${name}.png`);
|
||||||
|
await page.screenshot({ path: destination, fullPage: true });
|
||||||
|
console.log(`${name}: ${layout.clientWidth} × ${layout.scrollHeight}`);
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const colorScheme of ["light", "dark"]) {
|
||||||
|
await capture({
|
||||||
|
name: `homepage-${colorScheme}`,
|
||||||
|
width: 1440,
|
||||||
|
height: 1000,
|
||||||
|
colorScheme,
|
||||||
|
});
|
||||||
|
await capture({
|
||||||
|
name: `homepage-${colorScheme}-mobile`,
|
||||||
|
width: 390,
|
||||||
|
height: 844,
|
||||||
|
colorScheme,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Render a marketing route to a print-styled PDF, the same way the browser's
|
||||||
|
// "Print PDF" button does (print CSS + designed print-only components).
|
||||||
|
//
|
||||||
|
// bun scripts/render-pdf.mjs <route> <output.pdf> [baseUrl]
|
||||||
|
// bun scripts/render-pdf.mjs /cvs ../docs/business/CVS_SUMMARY.pdf
|
||||||
|
//
|
||||||
|
// Self-authenticates past the SITE_PASSWORD gate by computing the same signed
|
||||||
|
// access cookie the login route issues (see src/lib/site-auth.ts). Requires a
|
||||||
|
// running dev/preview server and a local Google Chrome.
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import puppeteer from "puppeteer-core";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const route = process.argv[2] ?? "/cvs";
|
||||||
|
const outArg = process.argv[3] ?? "../docs/business/CVS_SUMMARY.pdf";
|
||||||
|
const baseUrl = process.argv[4] ?? "http://localhost:3000";
|
||||||
|
const outPath = path.resolve(__dirname, "..", outArg);
|
||||||
|
|
||||||
|
const CHROME =
|
||||||
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
||||||
|
const AUTH_COOKIE_NAME = "medscribe_site_access";
|
||||||
|
const ACCESS_PAYLOAD = "medscribe-site-access-v1";
|
||||||
|
|
||||||
|
const password = process.env.SITE_PASSWORD;
|
||||||
|
const cookies = [];
|
||||||
|
if (password) {
|
||||||
|
const token = crypto
|
||||||
|
.createHmac("sha256", password)
|
||||||
|
.update(ACCESS_PAYLOAD)
|
||||||
|
.digest("base64");
|
||||||
|
cookies.push({
|
||||||
|
name: AUTH_COOKIE_NAME,
|
||||||
|
value: token,
|
||||||
|
url: baseUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
executablePath: CHROME,
|
||||||
|
headless: true,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
if (cookies.length) await browser.setCookie(...cookies);
|
||||||
|
const res = await page.goto(`${baseUrl}${route}`, {
|
||||||
|
waitUntil: "networkidle0",
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
const finalUrl = page.url();
|
||||||
|
if (finalUrl.includes("/login")) {
|
||||||
|
throw new Error(
|
||||||
|
`Redirected to login (${res?.status()}). Check SITE_PASSWORD is set in the env used to run this script.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await page.pdf({
|
||||||
|
path: outPath,
|
||||||
|
printBackground: true,
|
||||||
|
preferCSSPageSize: true,
|
||||||
|
});
|
||||||
|
console.log(`Wrote ${outPath}`);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Check,
|
||||||
|
ClipboardCheck,
|
||||||
|
Mail,
|
||||||
|
MessageSquareText,
|
||||||
|
RotateCcw,
|
||||||
|
Smartphone,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { SectionLabel } from "~/components/section-label";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { productRelease } from "~/content/product";
|
||||||
|
import { BETA_INVITE_MAILTO } from "~/lib/beta";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Request beta access",
|
||||||
|
description:
|
||||||
|
"Learn who the Medscribe internal beta is for, what participants should expect, and how to request access without submitting health information.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
{
|
||||||
|
icon: ClipboardCheck,
|
||||||
|
title: "Review is part of testing",
|
||||||
|
body: "Check generated summaries, extracted medication fields, and the Know Me view before relying on them.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: MessageSquareText,
|
||||||
|
title: "Direct feedback",
|
||||||
|
body: "Report confusing language, incorrect extraction, notification behavior, and sharing friction.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: RotateCcw,
|
||||||
|
title: "Possible resets",
|
||||||
|
body: "This is an internal beta. Builds, local data, and setup may need to be reset as the product changes.",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const testFocus = [
|
||||||
|
"Whether the record still feels current after several weeks",
|
||||||
|
"Medication-label edge cases and correction flow",
|
||||||
|
"Reminder permissions, timing, travel, and daylight-saving behavior",
|
||||||
|
"Caregiver pairing, encrypted export, and import comprehension",
|
||||||
|
"Whether the Know Me handoff is readable and appropriately cautious",
|
||||||
|
"Physical-device model performance and battery or thermal behavior",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export default function BetaPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="px-4 pt-16 pb-18 sm:px-6 sm:pt-22">
|
||||||
|
<div className="mx-auto grid max-w-6xl gap-12 lg:grid-cols-[1fr_0.8fr] lg:items-center">
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="success">{productRelease.stage}</Badge>
|
||||||
|
<Badge variant="outline">{productRelease.platforms}</Badge>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-foreground mt-6 max-w-3xl font-serif text-5xl leading-[1.02] font-semibold tracking-[-0.03em] sm:text-6xl">
|
||||||
|
Test the care context, not just the demo.
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-6 max-w-2xl text-lg leading-relaxed sm:text-xl">
|
||||||
|
We are looking for people managing repeated visits or several
|
||||||
|
medications, especially family caregiver pairs willing to review
|
||||||
|
the record over time and tell us where it fails.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-border-soft bg-surface rounded-[2rem] border p-7 shadow-[0_24px_70px_rgba(15,23,42,0.08)] sm:p-9">
|
||||||
|
<div className="bg-primary-light text-primary flex size-12 items-center justify-center rounded-xl">
|
||||||
|
<Mail className="size-6" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-foreground mt-5 font-serif text-3xl font-semibold">
|
||||||
|
Request access by email
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-4 text-sm leading-relaxed">
|
||||||
|
The draft asks only for your platform, whether you are testing as
|
||||||
|
a patient, caregiver, or both, and whether you can test with
|
||||||
|
another person.
|
||||||
|
</p>
|
||||||
|
<Button asChild size="lg" className="mt-7 w-full sm:w-auto">
|
||||||
|
<a href={BETA_INVITE_MAILTO}>
|
||||||
|
Draft beta request
|
||||||
|
<ArrowRight className="size-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<p className="bg-surface-soft text-muted-foreground mt-5 rounded-xl p-4 text-xs leading-relaxed">
|
||||||
|
Do not send diagnoses, medication names, visit notes, transcripts,
|
||||||
|
or other health details. Email is not a medical-support or
|
||||||
|
emergency channel.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-border-soft border-y bg-[#f4f0e8]/75 px-4 py-20 sm:px-6 lg:py-24 dark:bg-[#151b25]">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<SectionLabel>A good fit</SectionLabel>
|
||||||
|
<h2 className="text-foreground mt-4 max-w-3xl font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
The beta needs real maintenance and handoff situations.
|
||||||
|
</h2>
|
||||||
|
<div className="mt-10 grid gap-4 md:grid-cols-3">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
icon: Smartphone,
|
||||||
|
title: "Several moving pieces",
|
||||||
|
body: "You regularly coordinate visits, medication reminders, or follow-ups.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Users,
|
||||||
|
title: "A caregiver relationship",
|
||||||
|
body: "You can test a deliberate handoff with a trusted family caregiver.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Check,
|
||||||
|
title: "Comfort reviewing",
|
||||||
|
body: "You understand generated information can be wrong and will check it.",
|
||||||
|
},
|
||||||
|
].map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
key={item.title}
|
||||||
|
className="border-border-soft bg-surface rounded-2xl border p-6"
|
||||||
|
>
|
||||||
|
<Icon className="text-primary size-6" aria-hidden="true" />
|
||||||
|
<h3 className="text-foreground mt-5 font-serif text-2xl font-semibold">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mt-3 text-sm leading-relaxed">
|
||||||
|
{item.body}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="px-4 py-20 sm:px-6 lg:py-28">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<div className="grid gap-12 lg:grid-cols-[0.8fr_1.2fr]">
|
||||||
|
<div>
|
||||||
|
<SectionLabel>What to expect</SectionLabel>
|
||||||
|
<h2 className="text-foreground mt-4 font-serif text-4xl font-semibold tracking-tight">
|
||||||
|
This is collaborative product testing.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-5 leading-relaxed">
|
||||||
|
Availability is controlled and continuity is not guaranteed.
|
||||||
|
Medscribe is not emergency care, diagnosis, dosing guidance, or
|
||||||
|
a replacement for your care team.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
|
{expectations.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
key={item.title}
|
||||||
|
className="border-border-soft bg-surface rounded-2xl border p-5"
|
||||||
|
>
|
||||||
|
<Icon className="text-primary size-5" aria-hidden="true" />
|
||||||
|
<h3 className="text-foreground mt-4 font-serif text-xl font-semibold">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mt-3 text-sm leading-relaxed">
|
||||||
|
{item.body}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-16 rounded-[2rem] bg-[#0f4f4b] p-7 text-white sm:p-10">
|
||||||
|
<div className="grid gap-10 lg:grid-cols-[0.7fr_1.3fr]">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold tracking-[0.14em] text-teal-100 uppercase">
|
||||||
|
Current test focus
|
||||||
|
</p>
|
||||||
|
<h2 className="mt-4 font-serif text-3xl font-semibold">
|
||||||
|
The questions we are trying to answer
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<ul className="grid gap-3 sm:grid-cols-2">
|
||||||
|
{testFocus.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="flex gap-3 rounded-xl border border-white/15 bg-white/8 p-4 text-sm leading-relaxed text-teal-50/85"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className="mt-0.5 size-4 shrink-0 text-teal-100"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-border-soft mt-12 flex flex-col items-start justify-between gap-5 border-t pt-10 sm:flex-row sm:items-center">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-foreground font-serif text-2xl font-semibold">
|
||||||
|
Want the technical boundaries first?
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
See what stays local, how sharing works, and the current
|
||||||
|
evidence.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<Link href="/privacy">Read privacy & evidence</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
import { NorthwellPrintSummary } from "~/components/northwell-print-summary";
|
||||||
|
import { PrintButton } from "~/components/print-button";
|
||||||
|
import { SectionLabel } from "~/components/section-label";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Northwell executive summary",
|
||||||
|
robots: { index: false, follow: false, nocache: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NorthwellBriefPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NorthwellPrintSummary />
|
||||||
|
<div className="print-hidden mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:py-24">
|
||||||
|
<SectionLabel>Executive summary · Northwell Health</SectionLabel>
|
||||||
|
<h1 className="text-foreground mt-4 font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
The health-system executive summary
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mx-auto mt-5 max-w-xl leading-relaxed">
|
||||||
|
This private, search-excluded route preserves the existing three-page
|
||||||
|
print document. Use Print PDF (or Cmd+P, then Save as PDF) to export
|
||||||
|
it.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex justify-center">
|
||||||
|
<PrintButton size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
import { CvsPrintSummary } from "~/components/cvs-print-summary";
|
||||||
|
import { PrintButton } from "~/components/print-button";
|
||||||
|
import { SectionLabel } from "~/components/section-label";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "CVS executive summary",
|
||||||
|
robots: { index: false, follow: false, nocache: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CvsSummaryPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CvsPrintSummary />
|
||||||
|
<div className="print-hidden mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:py-24">
|
||||||
|
<SectionLabel>Executive summary · CVS Health</SectionLabel>
|
||||||
|
<h1 className="text-foreground mt-4 font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
The pharmacy executive summary
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mx-auto mt-5 max-w-xl leading-relaxed">
|
||||||
|
This page is formatted as a two-page print document: the medication
|
||||||
|
record that moves adherence, keeps refills, and lowers cost of care.
|
||||||
|
Use Print PDF (or Cmd+P, then Save as PDF) to export it.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex justify-center">
|
||||||
|
<PrintButton size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import { MedscribeLogo } from "~/components/medscribe-logo";
|
import { MedscribeLogo } from "~/components/medscribe-logo";
|
||||||
@@ -20,6 +21,7 @@ export const metadata: Metadata = {
|
|||||||
title: "Executive Summary",
|
title: "Executive Summary",
|
||||||
description:
|
description:
|
||||||
"One-page executive summary for Medscribe, a private medical context app for families managing complex care.",
|
"One-page executive summary for Medscribe, a private medical context app for families managing complex care.",
|
||||||
|
robots: { index: false, follow: false, nocache: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const TAGLINE =
|
const TAGLINE =
|
||||||
@@ -131,6 +133,11 @@ const printLightTokens = {
|
|||||||
} as CSSProperties;
|
} as CSSProperties;
|
||||||
|
|
||||||
export default function ExecutiveSummaryPage() {
|
export default function ExecutiveSummaryPage() {
|
||||||
|
// The home page is now the executive summary (single-page site). This deep
|
||||||
|
// page is retired but kept in place — its content (incl. the print one-pager)
|
||||||
|
// is a one-line revert away if we want a longer version back.
|
||||||
|
redirect("/");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ===================== SCREEN ===================== */}
|
{/* ===================== SCREEN ===================== */}
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Camera,
|
||||||
|
Check,
|
||||||
|
FileText,
|
||||||
|
Keyboard,
|
||||||
|
Mic,
|
||||||
|
ShieldAlert,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { PhoneMockup } from "~/components/phone-mockup";
|
||||||
|
import { SectionLabel } from "~/components/section-label";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { productClaims } from "~/content/product";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "How it works",
|
||||||
|
description:
|
||||||
|
"See how Medscribe turns visits and medication routines into one local record a person can review and carry.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputs = [
|
||||||
|
{
|
||||||
|
icon: Keyboard,
|
||||||
|
title: "Type or paste",
|
||||||
|
body: "Start from notes you already have and keep the source close to the result.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Camera,
|
||||||
|
title: "Import a screenshot",
|
||||||
|
body: "Bring in a portal summary or prescription label, then correct extracted fields.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: FileText,
|
||||||
|
title: "Attach a document",
|
||||||
|
body: "Use a photo or document as the input to a local review workflow.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Mic,
|
||||||
|
title: "Record with consent",
|
||||||
|
body: "Optional visit audio requires consent and is transcribed on the device.",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const boundaries = [
|
||||||
|
"It does not diagnose or recommend treatment.",
|
||||||
|
"It does not calculate, shift, or supply medication timing.",
|
||||||
|
"It does not replace a pharmacist, prescribing team, or emergency service.",
|
||||||
|
"It does not automatically synchronize a household in the background.",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export default function HowItWorksPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="px-4 pt-16 pb-18 sm:px-6 sm:pt-22">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<Badge variant="success">Current beta workflow</Badge>
|
||||||
|
<h1 className="text-foreground mt-6 max-w-4xl font-serif text-5xl leading-[1.02] font-semibold tracking-[-0.03em] sm:text-6xl">
|
||||||
|
One record, built through everyday use.
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-6 max-w-2xl text-lg leading-relaxed sm:text-xl">
|
||||||
|
Medscribe is useful at a handoff only if ordinary moments keep the
|
||||||
|
picture current. Every generated result has a person in the review
|
||||||
|
loop.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-border-soft bg-surface/75 border-y px-4 py-18 sm:px-6 lg:py-22">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<SectionLabel>Start where the information is</SectionLabel>
|
||||||
|
<h2 className="text-foreground mt-4 max-w-3xl font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
Four ways in. One review checkpoint.
|
||||||
|
</h2>
|
||||||
|
<div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{inputs.map((item, index) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
key={item.title}
|
||||||
|
className="border-border-soft bg-canvas rounded-2xl border p-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="bg-primary-light text-primary flex size-11 items-center justify-center rounded-xl">
|
||||||
|
<Icon className="size-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground text-xs font-bold">
|
||||||
|
0{index + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-foreground mt-5 font-serif text-2xl font-semibold">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mt-3 text-sm leading-relaxed">
|
||||||
|
{item.body}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="border-primary/20 bg-primary-light/45 mt-6 flex gap-3 rounded-2xl border p-5">
|
||||||
|
<Check
|
||||||
|
className="text-primary mt-0.5 size-5 shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<p className="text-foreground text-sm leading-relaxed">
|
||||||
|
The person reviews summaries and extracted medication fields.
|
||||||
|
Medscribe does not silently turn model output into an
|
||||||
|
authoritative medical record.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="px-4 py-20 sm:px-6 lg:py-28">
|
||||||
|
<div className="mx-auto max-w-6xl space-y-24">
|
||||||
|
<div className="grid gap-12 lg:grid-cols-2 lg:items-center">
|
||||||
|
<div>
|
||||||
|
<SectionLabel>Review what changed</SectionLabel>
|
||||||
|
<h2 className="text-foreground mt-4 font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
A summary is a draft, not a verdict.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-5 leading-relaxed">
|
||||||
|
Supported transcription and summarization run locally. The visit
|
||||||
|
remains available beside the summary so a person can check
|
||||||
|
follow-ups and wording.
|
||||||
|
</p>
|
||||||
|
<p className="border-border-soft bg-surface text-muted-foreground mt-5 rounded-2xl border p-5 text-sm leading-relaxed">
|
||||||
|
<strong className="text-foreground">What comes next:</strong> a
|
||||||
|
structured review inbox for accepting, correcting, or dismissing
|
||||||
|
changes inferred from a visit.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-primary-light/45 justify-self-center rounded-[2.5rem] p-7">
|
||||||
|
<PhoneMockup
|
||||||
|
src="/screenshots/appointment-summary.png"
|
||||||
|
alt="Synthetic Medscribe beta visit summary"
|
||||||
|
width={235}
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground mt-4 text-center text-xs">
|
||||||
|
Synthetic demo data · beta interface
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-12 lg:grid-cols-2 lg:items-center">
|
||||||
|
<div className="justify-self-center rounded-[2.5rem] bg-[#f4f0e8] p-7 lg:order-1 dark:bg-[#151b25]">
|
||||||
|
<PhoneMockup
|
||||||
|
src="/screenshots/medication-detail.png"
|
||||||
|
alt="Synthetic Medscribe beta medication detail"
|
||||||
|
width={235}
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground mt-4 text-center text-xs">
|
||||||
|
Synthetic demo data · beta interface
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="lg:order-2">
|
||||||
|
<SectionLabel>Maintain the medication picture</SectionLabel>
|
||||||
|
<h2 className="text-foreground mt-4 font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
Facts and schedules carry their boundary with them.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-5 leading-relaxed">
|
||||||
|
{productClaims.medicationFacts.publicCopy} Schedules can record
|
||||||
|
whether they are fixed-time or as-needed, the times supplied by
|
||||||
|
the person's source, confirmation, and timezone.
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mt-5 text-sm leading-relaxed">
|
||||||
|
Medscribe records timing supplied by a label, pharmacist, or
|
||||||
|
prescribing team. It does not calculate or shift medication
|
||||||
|
times.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-12 lg:grid-cols-2 lg:items-center">
|
||||||
|
<div>
|
||||||
|
<SectionLabel>Prepare the handoff</SectionLabel>
|
||||||
|
<h2 className="text-foreground mt-4 font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
Know Me is the view. Sharing is a separate choice.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-5 leading-relaxed">
|
||||||
|
Open the local Know Me screen when someone needs a concise
|
||||||
|
picture. For a caregiver, pair devices, compare a safety number,
|
||||||
|
review the record, and manually export an encrypted snapshot.
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mt-5 text-sm leading-relaxed">
|
||||||
|
The current import path does not yet require the sender to be in
|
||||||
|
the recipient's active trust list, so recipients must
|
||||||
|
verify the sender out of band. Revocation prevents a future
|
||||||
|
local export; it cannot invalidate already-exported ciphertext
|
||||||
|
or erase a copy the recipient imported.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-primary-light/45 justify-self-center rounded-[2.5rem] p-7">
|
||||||
|
<PhoneMockup
|
||||||
|
src="/screenshots/emergency-record.png"
|
||||||
|
alt="Synthetic Medscribe beta Know Me handoff"
|
||||||
|
width={235}
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground mt-4 text-center text-xs">
|
||||||
|
Synthetic demo data · beta interface
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-border-soft border-t bg-[#f4f0e8]/75 px-4 py-20 sm:px-6 dark:bg-[#151b25]">
|
||||||
|
<div className="mx-auto grid max-w-6xl gap-12 lg:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<div className="bg-primary-light text-primary flex size-12 items-center justify-center rounded-xl">
|
||||||
|
<ShieldAlert className="size-6" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-foreground mt-5 font-serif text-4xl font-semibold tracking-tight">
|
||||||
|
What Medscribe does not do
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-4 leading-relaxed">
|
||||||
|
A useful boundary is part of the product, not fine print after it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{boundaries.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="border-border-soft bg-surface text-foreground flex gap-3 rounded-2xl border p-4 text-sm leading-relaxed"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className="text-primary mt-0.5 size-5 shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="px-4 py-20 sm:px-6">
|
||||||
|
<div className="mx-auto flex max-w-6xl flex-col items-start justify-between gap-7 rounded-[2rem] bg-[#0f4f4b] px-7 py-10 text-white sm:px-10 md:flex-row md:items-center">
|
||||||
|
<div>
|
||||||
|
<h2 className="font-serif text-3xl font-semibold">
|
||||||
|
See the trust boundary task by task.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-teal-50/75">
|
||||||
|
Read what stays local, what can use a network, and what remains
|
||||||
|
unfinished.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
className="border-white/20 bg-white text-[#0f4f4b] hover:bg-teal-50"
|
||||||
|
>
|
||||||
|
<Link href="/privacy">
|
||||||
|
Privacy & evidence
|
||||||
|
<ArrowRight className="size-4" aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ export const metadata: Metadata = {
|
|||||||
title: "Solution Landscape",
|
title: "Solution Landscape",
|
||||||
description:
|
description:
|
||||||
"How the medical-context space is divided today, and the combined job no existing product owns.",
|
"How the medical-context space is divided today, and the combined job no existing product owns.",
|
||||||
|
robots: { index: false, follow: false, nocache: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const INTRO =
|
const INTRO =
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ export default function MarketingLayout({
|
|||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<a
|
||||||
|
href="#main-content"
|
||||||
|
className="print-hidden bg-primary-button fixed top-2 left-2 z-[100] -translate-y-20 rounded-lg px-4 py-2 text-sm font-semibold text-white transition-transform focus:translate-y-0"
|
||||||
|
>
|
||||||
|
Skip to content
|
||||||
|
</a>
|
||||||
<ScreenBackground />
|
<ScreenBackground />
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<main>{children}</main>
|
<main id="main-content">{children}</main>
|
||||||
<SiteFooter />
|
<SiteFooter />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,332 +1,522 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
ArrowRight,
|
||||||
ChevronRight,
|
Check,
|
||||||
ClipboardList,
|
ContactRound,
|
||||||
HeartPulse,
|
KeyRound,
|
||||||
Lock,
|
LockKeyhole,
|
||||||
Mic,
|
ScanLine,
|
||||||
Pill,
|
ShieldCheck,
|
||||||
Shield,
|
Smartphone,
|
||||||
Users,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import { FeatureShowcase } from "~/components/feature-showcase";
|
import { ProductDevice } from "~/components/product-device";
|
||||||
import { PhoneMockup } from "~/components/phone-mockup";
|
|
||||||
import { SectionLabel } from "~/components/section-label";
|
|
||||||
import {
|
|
||||||
BETA_CTA_BODY,
|
|
||||||
BETA_CTA_TITLE,
|
|
||||||
BETA_INVITE_MAILTO,
|
|
||||||
} from "~/lib/beta";
|
|
||||||
import { Badge } from "~/components/ui/badge";
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import { productClaims, productRelease } from "~/content/product";
|
||||||
Card,
|
import { BETA_INVITE_MAILTO } from "~/lib/beta";
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
|
|
||||||
const quickActions = [
|
export const metadata: Metadata = {
|
||||||
{
|
title: "Carry the context when care gets complicated",
|
||||||
icon: HeartPulse,
|
description:
|
||||||
title: "Know Me card",
|
"Medscribe is a private, patient-controlled beta that helps keep visits, medications, and critical context together on your device.",
|
||||||
meta: "Baseline, conditions, allergies",
|
};
|
||||||
featured: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Pill,
|
|
||||||
title: "Time-critical meds",
|
|
||||||
meta: "Schedules caregivers can trust",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Mic,
|
|
||||||
title: "Visit capture",
|
|
||||||
meta: "Consent-gated, on-device",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Users,
|
|
||||||
title: "Caregiver handoff",
|
|
||||||
meta: "One current shared truth",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const pillars = [
|
const visitMoments = [
|
||||||
{
|
{
|
||||||
icon: ClipboardList,
|
number: "01",
|
||||||
title: "Keep the record current",
|
eyebrow: "Visits stay findable",
|
||||||
body: "Capture visits, scan labels, and keep conditions, medications, allergies, contacts, and caregiver notes in one portable record.",
|
title: "The visit has a place to land.",
|
||||||
|
body: "Keep the recording, transcript, and reviewed summary together instead of reconstructing the appointment from memory.",
|
||||||
|
image: "/screenshots/pitch/visits-natural.webp",
|
||||||
|
alt: "Medscribe Visits screen in a natural titanium iPhone product bezel",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: AlertTriangle,
|
number: "02",
|
||||||
title: "Make the critical context obvious",
|
eyebrow: "Words stay checkable",
|
||||||
body: "The emergency view prioritizes the facts clinicians need fast: baseline, time-critical meds, active conditions, allergies, and who to call.",
|
title: "Go back to what was actually said.",
|
||||||
|
body: "Search the local transcript and compare it with the generated summary before anything becomes part of the record you rely on.",
|
||||||
|
image: "/screenshots/pitch/transcript-black.webp",
|
||||||
|
alt: "Medscribe visit transcript in a black titanium iPhone product bezel",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Shield,
|
number: "03",
|
||||||
title: "Stay private by architecture",
|
eyebrow: "The next step stays visible",
|
||||||
body: "Transcription, OCR, and medication chat run locally. No cloud AI processing. Sharing should be explicit, revocable, and encrypted.",
|
title: "Review the useful part. Keep the uncertainty.",
|
||||||
|
body: "Medications, follow-ups, diagnoses, and instructions are prepared on-device. Suggestions remain suggestions until a person checks them.",
|
||||||
|
image: "/screenshots/pitch/summary-natural.webp",
|
||||||
|
alt: "Medscribe appointment summary in a natural titanium iPhone product bezel",
|
||||||
},
|
},
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
const impact = [
|
const proofStats = [
|
||||||
{
|
{
|
||||||
title: "Fewer blank handoffs",
|
value: productRelease.drugFacts.toLocaleString("en-US"),
|
||||||
body: "When a caregiver is overwhelmed, the important facts are already assembled: conditions, medications, allergies, baseline, contacts, and recent changes.",
|
label: "bundled medication records",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Safer medication moments",
|
value: productRelease.rxNormTerms.toLocaleString("en-US"),
|
||||||
body: "Time-critical schedules and dose history stay visible, so a missed or delayed medication is easier to catch before it becomes a crisis.",
|
label: "RxNorm terms for matching",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "More confident families",
|
value: productRelease.interactionPairs.toLocaleString("en-US"),
|
||||||
body: "Caregivers can see what changed, where it came from, and what still needs a clinician or pharmacist instead of guessing from memory.",
|
label: "known interaction pairs",
|
||||||
},
|
},
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
|
const privacyPrinciples = [
|
||||||
|
{
|
||||||
|
icon: Smartphone,
|
||||||
|
title: "Your working record stays local",
|
||||||
|
body: productClaims.localRecord.publicCopy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: LockKeyhole,
|
||||||
|
title: "AI runs on the device",
|
||||||
|
body: productClaims.localAi.publicCopy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ShieldCheck,
|
||||||
|
title: "Facts do not come from model memory",
|
||||||
|
body: productClaims.medicationFacts.publicCopy,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const betaFit = [
|
||||||
|
"Managing several medications or repeated visits",
|
||||||
|
"Coordinating care with a family caregiver",
|
||||||
|
"Willing to review generated information and report errors",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const today = new Date().toLocaleDateString("en-US", {
|
|
||||||
weekday: "long",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-6xl space-y-14 px-4 py-8 sm:px-6 sm:py-12 lg:space-y-20 lg:py-14">
|
<>
|
||||||
|
<section className="relative isolate min-h-[calc(100svh-4rem)] overflow-hidden px-4 pt-14 sm:px-6 sm:pt-20 lg:pt-24">
|
||||||
{/* Hero */}
|
<div className="bg-primary-light/55 absolute top-0 left-1/2 -z-10 h-[38rem] w-[68rem] max-w-[120vw] -translate-x-1/2 rounded-full blur-3xl" />
|
||||||
<section className="grid items-center gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(230px,280px)] lg:gap-14">
|
<div className="mx-auto max-w-7xl text-center">
|
||||||
<div className="space-y-6">
|
<div className="animate-fade-in-up flex items-center justify-center gap-3">
|
||||||
<div className="animate-fade-in-up space-y-5">
|
<span className="bg-success-light text-success rounded-full px-3 py-1.5 text-xs font-bold tracking-[0.12em] uppercase">
|
||||||
<SectionLabel>Medscribe · Private complex-care context</SectionLabel>
|
{productRelease.stage}
|
||||||
<h1 className="max-w-2xl font-serif text-4xl leading-[1.05] font-semibold tracking-tight text-foreground sm:text-5xl lg:text-6xl">
|
</span>
|
||||||
Be known when care gets complicated
|
<span className="text-muted-foreground text-xs font-semibold tracking-[0.12em] uppercase">
|
||||||
|
{productRelease.platforms}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="animate-fade-in-up-delay-1 text-foreground mx-auto mt-7 max-w-6xl font-serif text-[clamp(3.6rem,9vw,8.5rem)] leading-[0.86] font-semibold tracking-[-0.055em]">
|
||||||
|
Carry the context.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="max-w-xl text-base leading-relaxed text-muted-foreground sm:text-lg">
|
<p className="animate-fade-in-up-delay-2 text-muted-foreground mx-auto mt-7 max-w-2xl text-lg leading-relaxed sm:text-xl lg:text-2xl">
|
||||||
A private medical context app for families managing complex care.
|
Visits, medications, and the critical details between them—kept
|
||||||
It keeps visits, medications, baseline, contacts, allergies, and
|
together on the device you carry.
|
||||||
emergency facts ready before the chart catches up.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">{today}</p>
|
<div className="animate-fade-in-up-delay-3 mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="animate-fade-in-up-delay-1 flex flex-wrap gap-3">
|
|
||||||
<Button asChild size="lg">
|
<Button asChild size="lg">
|
||||||
<a href="/executive-summary">
|
<Link href="/beta">
|
||||||
Read executive summary
|
Request beta access
|
||||||
<ChevronRight className="size-4" />
|
<ArrowRight className="size-4" aria-hidden="true" />
|
||||||
</a>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" size="lg">
|
<Button asChild size="lg" variant="outline">
|
||||||
<a href="#features">See how it works</a>
|
<Link href="/how-it-works">See how it works</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="animate-fade-in-up-delay-2 flex flex-wrap gap-2">
|
|
||||||
<Badge variant="success">Beta available</Badge>
|
|
||||||
<Badge variant="outline">iOS · TestFlight</Badge>
|
|
||||||
<Badge variant="outline">Android · Play internal</Badge>
|
|
||||||
<Badge variant="outline">Emergency-ready</Badge>
|
|
||||||
<Badge variant="outline">No cloud AI</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="animate-fade-in-up-delay-3 flex justify-center lg:justify-end">
|
<div className="relative mx-auto mt-12 h-[36rem] max-w-5xl sm:h-[46rem] lg:mt-8 lg:h-[54rem]">
|
||||||
<div className="flex w-fit items-start justify-center">
|
<div className="absolute top-24 left-1/2 w-[17rem] -translate-x-[86%] -rotate-6 sm:top-28 sm:w-[22rem] lg:top-32 lg:w-[26rem]">
|
||||||
<PhoneMockup
|
<ProductDevice
|
||||||
src="/screenshots/home.png"
|
src="/screenshots/pitch/summary-natural.webp"
|
||||||
alt="Medscribe home dashboard"
|
alt="Medscribe visit summary in a natural titanium iPhone product bezel"
|
||||||
|
sizes="(min-width: 1024px) 416px, (min-width: 640px) 352px, 272px"
|
||||||
priority
|
priority
|
||||||
width={190}
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-0 left-1/2 z-10 w-[18rem] -translate-x-[14%] rotate-5 sm:w-[23rem] lg:w-[28rem]">
|
||||||
|
<ProductDevice
|
||||||
|
src="/screenshots/pitch/home-black.webp"
|
||||||
|
alt="Medscribe home screen in a black titanium iPhone product bezel"
|
||||||
|
sizes="(min-width: 1024px) 448px, (min-width: 640px) 368px, 288px"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground absolute inset-x-0 bottom-5 text-center text-xs font-medium tracking-wide sm:bottom-10">
|
||||||
|
Synthetic demo data · beta interface
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<div className="mx-auto max-w-6xl text-center">
|
||||||
|
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
||||||
|
The missing picture
|
||||||
|
</p>
|
||||||
|
<h2 className="text-foreground mx-auto mt-7 max-w-5xl font-serif text-[clamp(2.8rem,7vw,6.5rem)] leading-[0.98] font-semibold tracking-[-0.045em]">
|
||||||
|
Care happens in fragments. Your context shouldn't.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mx-auto mt-8 max-w-2xl text-lg leading-relaxed sm:text-xl">
|
||||||
|
One instruction lives in a portal. A medication label sits in a
|
||||||
|
cabinet. The latest change lives in someone's memory. Medscribe
|
||||||
|
is the patient-held thread between those moments.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="pitch-lazy px-4 py-24 sm:px-6 lg:py-36">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-16 lg:grid-cols-[0.78fr_1.22fr] lg:gap-24">
|
||||||
|
<div className="lg:sticky lg:top-28 lg:h-fit">
|
||||||
|
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
||||||
|
After the visit
|
||||||
|
</p>
|
||||||
|
<h2 className="text-foreground mt-6 max-w-xl font-serif text-5xl leading-[0.98] font-semibold tracking-[-0.04em] sm:text-6xl">
|
||||||
|
Turn what happened into something you can check.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-7 max-w-lg text-lg leading-relaxed">
|
||||||
|
Bring in typed notes, a portal screenshot, a document, or an
|
||||||
|
optional consent-gated recording. The work happens locally. You
|
||||||
|
decide what is useful.
|
||||||
|
</p>
|
||||||
|
<div className="border-primary/25 bg-primary-light/35 mt-8 max-w-lg rounded-2xl border p-5">
|
||||||
|
<p className="text-foreground text-sm leading-relaxed">
|
||||||
|
<strong className="font-semibold">The safety loop:</strong>{" "}
|
||||||
|
review generated information before acting. Medscribe does not
|
||||||
|
diagnose, prescribe, or replace clinical verification.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ol className="space-y-8 sm:space-y-12">
|
||||||
|
{visitMoments.map((moment) => (
|
||||||
|
<li
|
||||||
|
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)]"
|
||||||
|
>
|
||||||
|
<div className="relative z-10 max-w-xl">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="bg-primary-light text-primary flex size-9 items-center justify-center rounded-full text-xs font-bold">
|
||||||
|
{moment.number}
|
||||||
|
</span>
|
||||||
|
<p className="text-primary text-xs font-bold tracking-[0.16em] uppercase">
|
||||||
|
{moment.eyebrow}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-foreground mt-5 font-serif text-3xl font-semibold tracking-[-0.025em] sm:text-4xl">
|
||||||
|
{moment.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mt-4 max-w-lg leading-relaxed">
|
||||||
|
{moment.body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-primary-light/45 absolute -right-36 bottom-14 -z-0 size-96 rounded-full blur-3xl" />
|
||||||
|
<div className="relative mx-auto mt-10 w-[min(72vw,21rem)] translate-y-14">
|
||||||
|
<ProductDevice
|
||||||
|
src={moment.image}
|
||||||
|
alt={moment.alt}
|
||||||
|
sizes="(min-width: 1024px) 336px, (min-width: 640px) 320px, 72vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<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="mx-auto max-w-7xl">
|
||||||
|
<div className="mx-auto max-w-4xl text-center">
|
||||||
|
<p className="text-xs font-bold tracking-[0.2em] text-teal-200 uppercase">
|
||||||
|
Between visits
|
||||||
|
</p>
|
||||||
|
<h2 className="mt-6 font-serif text-[clamp(3rem,7vw,6.7rem)] leading-[0.94] font-semibold tracking-[-0.045em]">
|
||||||
|
Keep the medication picture from going stale.
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto mt-8 max-w-2xl text-lg leading-relaxed text-teal-50/75 sm:text-xl">
|
||||||
|
The list, the label, the schedule, and the latest confirmation
|
||||||
|
belong in the same view—not scattered across paper and memory.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-16 grid gap-14 lg:grid-cols-2 lg:items-center lg:gap-6">
|
||||||
|
<div className="relative mx-auto h-[42rem] w-full max-w-2xl sm:h-[50rem] lg:h-[56rem]">
|
||||||
|
<div className="absolute top-20 left-[4%] w-[17rem] -rotate-6 sm:w-[21rem] lg:w-[23rem]">
|
||||||
|
<ProductDevice
|
||||||
|
src="/screenshots/pitch/medication-detail-natural.webp"
|
||||||
|
alt="Medscribe medication detail in a natural titanium iPhone product bezel"
|
||||||
|
sizes="(min-width: 1024px) 368px, (min-width: 640px) 336px, 272px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-0 right-[2%] z-10 w-[18rem] rotate-5 sm:w-[22rem] lg:w-[24rem]">
|
||||||
|
<ProductDevice
|
||||||
|
src="/screenshots/pitch/medications-black.webp"
|
||||||
|
alt="Medscribe medication list in a black titanium iPhone product bezel"
|
||||||
|
sizes="(min-width: 1024px) 384px, (min-width: 640px) 352px, 288px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-px overflow-hidden rounded-[2rem] border border-white/12 bg-white/12">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: "Show where a schedule came from",
|
||||||
|
body: "Schedules can carry their source, confirmation, timezone, and freshness state.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Keep facts deterministic",
|
||||||
|
body: productClaims.medicationFacts.publicCopy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Make uncertainty visible",
|
||||||
|
body: "Missing or stale details stay visible so the person or clinician can verify them.",
|
||||||
|
},
|
||||||
|
].map((item) => (
|
||||||
|
<article
|
||||||
|
key={item.title}
|
||||||
|
className="bg-[#0b3b37]/90 p-7 sm:p-9"
|
||||||
|
>
|
||||||
|
<Check className="size-5 text-teal-200" aria-hidden="true" />
|
||||||
|
<h3 className="mt-5 font-serif text-2xl font-semibold sm:text-3xl">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 leading-relaxed text-teal-50/70">
|
||||||
|
{item.body}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<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="order-2 lg:order-1">
|
||||||
|
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
||||||
|
Ask from the record
|
||||||
|
</p>
|
||||||
|
<h2 className="text-foreground mt-6 max-w-2xl font-serif text-[clamp(3.25rem,6vw,6.5rem)] leading-[0.94] font-semibold tracking-[-0.045em]">
|
||||||
|
Better questions begin with your context.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-8 max-w-xl text-lg leading-relaxed sm:text-xl">
|
||||||
|
Ask about a visit or medication without uploading the record to a
|
||||||
|
remote model. Answers are grounded in local history and bundled
|
||||||
|
facts, with the boundary kept in view.
|
||||||
|
</p>
|
||||||
|
<p className="border-primary/35 text-muted-foreground mt-7 max-w-xl border-l-2 pl-5 text-sm leading-relaxed">
|
||||||
|
Informational only. Medication changes still belong with a
|
||||||
|
clinician or pharmacist.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="order-1 mx-auto w-[min(78vw,27rem)] lg:order-2">
|
||||||
|
<ProductDevice
|
||||||
|
src="/screenshots/pitch/ask-black.webp"
|
||||||
|
alt="Medscribe Ask response grounded in local health context, shown in a black titanium iPhone product bezel"
|
||||||
|
sizes="(min-width: 1024px) 432px, (min-width: 640px) 384px, 78vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Strategic spine */}
|
<section className="pitch-lazy overflow-hidden px-4 py-24 sm:px-6 lg:py-36">
|
||||||
<section className="animate-fade-in-up space-y-5">
|
<div className="mx-auto max-w-7xl">
|
||||||
<SectionLabel>The story</SectionLabel>
|
<div className="grid items-center gap-16 lg:grid-cols-[0.92fr_1.08fr] lg:gap-24">
|
||||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:items-start">
|
<div className="relative mx-auto w-[min(80vw,28rem)]">
|
||||||
<div className="space-y-3">
|
<div className="bg-primary-light/70 absolute inset-[12%_-12%_8%] -z-10 rounded-[4rem] blur-2xl" />
|
||||||
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
|
<ProductDevice
|
||||||
The story scatters right when it matters
|
src="/screenshots/pitch/know-me-natural.webp"
|
||||||
</h2>
|
alt="Medscribe Know Me handoff record in a natural titanium iPhone product bezel"
|
||||||
<p className="max-w-xl leading-relaxed text-muted-foreground">
|
sizes="(min-width: 1024px) 448px, (min-width: 640px) 400px, 80vw"
|
||||||
A daughter remembers the medication schedule. A spouse knows what
|
|
||||||
normal looks like. A specialist mentioned a change three visits ago.
|
|
||||||
In a crisis, those details are scattered across people, portals, and
|
|
||||||
memory.
|
|
||||||
</p>
|
|
||||||
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground/80 italic">
|
|
||||||
I built the first version of Medscribe after watching my own family
|
|
||||||
try to hold this together for my grandfather. The record scattered
|
|
||||||
right when we needed it most.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg border border-border-soft bg-surface p-5 shadow-[0_1px_4px_rgba(0,0,0,0.05)]">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<HeartPulse className="mt-0.5 size-5 shrink-0 text-primary" />
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="font-serif text-xl font-semibold tracking-tight">
|
|
||||||
One current story
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
||||||
Medscribe gives the family a private place to keep that context
|
|
||||||
alive: current meds, timing, baseline, allergies, recent visit
|
|
||||||
summaries, emergency contacts, and the source trail behind each
|
|
||||||
fact.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{quickActions.map((action) => {
|
|
||||||
const Icon = action.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={action.title}
|
|
||||||
className={`card-hover ${action.featured ? "border-primary-button/20 bg-primary-button text-white" : ""}`}
|
|
||||||
>
|
|
||||||
<CardHeader className="gap-3">
|
|
||||||
<div
|
|
||||||
className={`flex size-10 items-center justify-center rounded-[10px] ${
|
|
||||||
action.featured ? "bg-white/18" : "bg-primary-light"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={`size-5 ${action.featured ? "text-white" : "text-primary"}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className={action.featured ? "text-white" : ""}>
|
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
||||||
{action.title}
|
At the handoff
|
||||||
</CardTitle>
|
</p>
|
||||||
<CardDescription
|
<h2 className="text-foreground mt-6 max-w-2xl font-serif text-[clamp(3.2rem,6vw,6.2rem)] leading-[0.95] font-semibold tracking-[-0.045em]">
|
||||||
className={action.featured ? "text-white/72" : ""}
|
Bring the picture when the chart is incomplete.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-8 max-w-xl text-lg leading-relaxed sm:text-xl">
|
||||||
|
Know Me gathers selected medications, allergies, conditions,
|
||||||
|
baseline, and contacts into one readable snapshot. Freshness
|
||||||
|
signals show what may need another look.
|
||||||
|
</p>
|
||||||
|
<div className="mt-9 grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="border-border-soft bg-surface rounded-2xl border p-5">
|
||||||
|
<ScanLine
|
||||||
|
className="text-primary size-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<p className="text-foreground mt-4 font-semibold">
|
||||||
|
Quick handoff
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm leading-relaxed">
|
||||||
|
Show or share a selected, readable snapshot.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="border-border-soft bg-surface rounded-2xl border p-5">
|
||||||
|
<ContactRound
|
||||||
|
className="text-primary size-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<p className="text-foreground mt-4 font-semibold">
|
||||||
|
Caregiver copy
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm leading-relaxed">
|
||||||
|
Manually export an encrypted copy for a paired recipient.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-24 overflow-hidden rounded-[2.5rem] bg-[#0f4f4b] p-7 text-white sm:p-10 lg:p-14">
|
||||||
|
<div className="grid gap-12 lg:grid-cols-[0.85fr_1.15fr] lg:items-center">
|
||||||
|
<div>
|
||||||
|
<KeyRound className="size-7 text-teal-200" aria-hidden="true" />
|
||||||
|
<h3 className="mt-5 font-serif text-4xl leading-tight font-semibold sm:text-5xl">
|
||||||
|
Share a snapshot. Not an invisible feed.
|
||||||
|
</h3>
|
||||||
|
<p className="mt-5 max-w-xl leading-relaxed text-teal-50/75">
|
||||||
|
Pair by QR code, compare a safety number, and choose when to
|
||||||
|
export. The caregiver imports a local copy on their device.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ol className="grid gap-3 sm:grid-cols-3">
|
||||||
|
{[
|
||||||
|
["1", "You review", "Choose the record to send"],
|
||||||
|
["2", "Encrypted copy", "Protected for the recipient key"],
|
||||||
|
["3", "They import", "A local, readable snapshot"],
|
||||||
|
].map(([number, title, body]) => (
|
||||||
|
<li
|
||||||
|
key={number}
|
||||||
|
className="rounded-2xl border border-white/15 bg-white/8 p-5"
|
||||||
>
|
>
|
||||||
{action.meta}
|
<span className="flex size-8 items-center justify-center rounded-full bg-white/12 text-xs font-bold">
|
||||||
</CardDescription>
|
{number}
|
||||||
|
</span>
|
||||||
|
<p className="mt-8 font-serif text-xl font-semibold">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-teal-50/65">
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<p className="mt-8 border-t border-white/12 pt-6 text-xs leading-relaxed text-teal-50/60">
|
||||||
|
Manual today—not live sync or a household cloud account.
|
||||||
|
Import-side sender trust enforcement is still being hardened;
|
||||||
|
revocation cannot recall a copy already exported.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Interactive feature showcase */}
|
<section className="pitch-lazy relative isolate overflow-hidden bg-[#070d15] px-4 py-24 text-white sm:px-6 lg:py-36">
|
||||||
<section id="features" className="animate-fade-in-up scroll-mt-24 space-y-5">
|
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_50%_0%,rgba(45,212,191,0.13),transparent_36%)]" />
|
||||||
<div className="space-y-2">
|
<div className="mx-auto max-w-7xl">
|
||||||
<SectionLabel>The record</SectionLabel>
|
<div className="mx-auto max-w-4xl text-center">
|
||||||
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
|
<p className="text-xs font-bold tracking-[0.2em] text-teal-300 uppercase">
|
||||||
The pieces that keep the story current
|
A precise privacy boundary
|
||||||
</h2>
|
|
||||||
<p className="max-w-2xl text-muted-foreground">
|
|
||||||
Visits, medications, reminders, search, and questions all point toward
|
|
||||||
the same outcome: a record the family can trust when the stakes rise.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<h2 className="mt-6 font-serif text-[clamp(3.2rem,7vw,7rem)] leading-[0.92] font-semibold tracking-[-0.05em]">
|
||||||
<FeatureShowcase />
|
Local changes what you have to trust.
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Privacy */}
|
|
||||||
<section id="privacy" className="animate-fade-in-up scroll-mt-24 space-y-6">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<SectionLabel>Why this is different</SectionLabel>
|
|
||||||
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
||||||
Built for the family record, not another cloud inbox
|
|
||||||
</h2>
|
</h2>
|
||||||
<p className="max-w-2xl text-muted-foreground">
|
<p className="mx-auto mt-8 max-w-2xl text-lg leading-relaxed text-slate-300 sm:text-xl">
|
||||||
The crowded space already has visit recorders, medication apps, and
|
The working record and supported AI tasks stay on the device.
|
||||||
caregiver portals. Medscribe's wedge is the private, portable
|
Medication facts come from bundled references, not a model trying
|
||||||
context layer families control before an institution is ready — and
|
to remember medicine.
|
||||||
it works alongside the systems a care team already uses, not instead
|
|
||||||
of them.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<dl className="mt-20 grid border-y border-white/12 md:grid-cols-3 md:divide-x md:divide-white/12">
|
||||||
{pillars.map((pillar) => {
|
{proofStats.map((stat) => (
|
||||||
const Icon = pillar.icon;
|
<div
|
||||||
|
key={stat.label}
|
||||||
|
className="border-b border-white/12 px-5 py-10 text-center last:border-b-0 md:border-b-0 lg:py-14"
|
||||||
|
>
|
||||||
|
<dt className="text-sm text-slate-400">{stat.label}</dt>
|
||||||
|
<dd className="mt-3 font-serif text-5xl font-semibold tracking-[-0.04em] text-teal-200 sm:text-6xl lg:text-7xl">
|
||||||
|
{stat.value}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="mt-16 grid gap-4 md:grid-cols-3">
|
||||||
|
{privacyPrinciples.map((principle) => {
|
||||||
|
const Icon = principle.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={pillar.title} className="card-hover">
|
<article
|
||||||
<CardHeader>
|
key={principle.title}
|
||||||
<div className="flex size-10 items-center justify-center rounded-[10px] bg-primary-light">
|
className="rounded-[1.75rem] border border-white/12 bg-white/[0.045] p-7"
|
||||||
<Icon className="size-5 text-primary" />
|
>
|
||||||
</div>
|
<Icon className="size-6 text-teal-200" aria-hidden="true" />
|
||||||
<CardTitle>{pillar.title}</CardTitle>
|
<h3 className="mt-8 font-serif text-2xl font-semibold">
|
||||||
<CardDescription>{pillar.body}</CardDescription>
|
{principle.title}
|
||||||
</CardHeader>
|
</h3>
|
||||||
</Card>
|
<p className="mt-4 text-sm leading-relaxed text-slate-400">
|
||||||
|
{principle.body}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="border-primary/15 bg-primary-light/40">
|
<div className="mt-10 text-center">
|
||||||
<CardContent className="flex items-start gap-4 pt-5">
|
<Button asChild variant="secondary">
|
||||||
<Lock className="mt-0.5 size-5 shrink-0 text-primary" />
|
<Link href="/privacy">
|
||||||
<p className="text-sm leading-relaxed text-foreground">
|
Read privacy & evidence
|
||||||
All AI models run on your device. No health data is uploaded for
|
<ArrowRight className="size-4" aria-hidden="true" />
|
||||||
processing. The medication chatbot stays in drug-information
|
</Link>
|
||||||
territory. It defers dosing and diagnosis questions to your
|
|
||||||
clinician or pharmacist.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Impact */}
|
|
||||||
<section id="impact" className="animate-fade-in-up scroll-mt-24 space-y-6">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<SectionLabel>Potential impact</SectionLabel>
|
|
||||||
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
||||||
Less panic, more context, better handoffs
|
|
||||||
</h2>
|
|
||||||
<p className="max-w-2xl text-muted-foreground">
|
|
||||||
The goal is simple: make the person easier to understand at the exact
|
|
||||||
moment their care team has the least context.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
|
||||||
{impact.map((item) => (
|
|
||||||
<Card key={item.title} className="card-hover">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{item.title}</CardTitle>
|
|
||||||
<CardDescription>{item.body}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* CTA */}
|
|
||||||
<section className="animate-fade-in-up">
|
|
||||||
<Card className="overflow-hidden border-primary-button/20 bg-primary-button text-white">
|
|
||||||
<CardHeader className="gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<CardTitle className="text-white">{BETA_CTA_TITLE}</CardTitle>
|
|
||||||
<CardDescription className="text-white/75">
|
|
||||||
{BETA_CTA_BODY}
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
className="border-white/30 bg-white text-primary-button hover:bg-white/90"
|
|
||||||
>
|
|
||||||
<a href={BETA_INVITE_MAILTO}>Request an invite</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<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%)]">
|
||||||
|
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
||||||
|
The right beta participant
|
||||||
|
</p>
|
||||||
|
<h2 className="text-foreground mx-auto mt-6 max-w-4xl font-serif text-[clamp(3.2rem,7vw,7rem)] leading-[0.92] font-semibold tracking-[-0.05em]">
|
||||||
|
Help build the record people can actually carry.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mx-auto mt-8 max-w-2xl text-lg leading-relaxed sm:text-xl">
|
||||||
|
This is an internal beta. Expect close feedback, manual review,
|
||||||
|
rough edges, and possible resets—not guaranteed continuity.
|
||||||
|
</p>
|
||||||
|
<ul className="mx-auto mt-9 grid max-w-4xl gap-3 text-left md:grid-cols-3">
|
||||||
|
{betaFit.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="border-primary/15 bg-surface/65 text-foreground flex items-start gap-3 rounded-2xl border p-4 text-sm leading-relaxed"
|
||||||
|
>
|
||||||
|
<span className="bg-primary-light mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full">
|
||||||
|
<Check className="text-primary size-3" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<a href={BETA_INVITE_MAILTO}>
|
||||||
|
Draft beta request
|
||||||
|
<ArrowRight className="size-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild size="lg" variant="outline">
|
||||||
|
<Link href="/beta">What to expect</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-muted-foreground mx-auto mt-6 max-w-xl text-xs leading-relaxed">
|
||||||
|
Start with your platform and whether you are testing as a patient,
|
||||||
|
caregiver, or both. Please do not email health details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
ArrowRight,
|
||||||
|
Check,
|
||||||
|
Database,
|
||||||
|
FileCheck2,
|
||||||
|
KeyRound,
|
||||||
|
LockKeyhole,
|
||||||
|
Wifi,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { SectionLabel } from "~/components/section-label";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { productClaims, productRelease } from "~/content/product";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Privacy & evidence",
|
||||||
|
description:
|
||||||
|
"A task-by-task view of Medscribe's local architecture, network behavior, caregiver sharing, evidence, and beta limitations.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataMap = [
|
||||||
|
{
|
||||||
|
task: "Visit transcription",
|
||||||
|
location: "Device",
|
||||||
|
network: "No cloud inference",
|
||||||
|
control: "Recording requires consent",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task: "Summary generation",
|
||||||
|
location: "Device",
|
||||||
|
network: "No cloud inference",
|
||||||
|
control: "The person reviews output",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task: "Medication-label OCR",
|
||||||
|
location: "Device",
|
||||||
|
network: "No cloud inference",
|
||||||
|
control: "Extracted fields remain editable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task: "Drug facts and known pairs",
|
||||||
|
location: "Bundled on device",
|
||||||
|
network: "No runtime fact lookup",
|
||||||
|
control: "Sources and coverage are disclosed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task: "Model installation or update",
|
||||||
|
location: "Downloaded to device",
|
||||||
|
network: "Network may be required",
|
||||||
|
control: "Approved URLs and file integrity checks",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task: "Caregiver sharing",
|
||||||
|
location: "Encrypted device-to-device snapshot",
|
||||||
|
network: "The person chooses ciphertext transport",
|
||||||
|
control: "Verify out of band; import trust enforcement is incomplete",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task: "Primary record storage",
|
||||||
|
location: "Local app database",
|
||||||
|
network: "No Medscribe cloud account",
|
||||||
|
control: "The unlocked device remains a trust boundary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
task: "Website analytics",
|
||||||
|
location: "Marketing site only",
|
||||||
|
network: "Optional Umami script when configured",
|
||||||
|
control: "Disabled when no website ID is configured",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const protects = [
|
||||||
|
"Runtime disclosure of visits or medications to a cloud AI provider",
|
||||||
|
"A Medscribe account database becoming the default copy of the record",
|
||||||
|
"A readable caregiver export being sent to someone who was not paired",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const doesNotProtect = [
|
||||||
|
"Information viewed on an unlocked or compromised phone",
|
||||||
|
"A snapshot that a verified recipient already imported",
|
||||||
|
"Someone intentionally copying or sharing visible information",
|
||||||
|
"Every possible drug interaction or model failure",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export default function PrivacyPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="px-4 pt-16 pb-18 sm:px-6 sm:pt-22">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Badge variant="success">
|
||||||
|
Last verified {productRelease.verifiedOn}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline">Internal beta</Badge>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-foreground mt-6 max-w-4xl font-serif text-5xl leading-[1.02] font-semibold tracking-[-0.03em] sm:text-6xl">
|
||||||
|
Privacy should be specific enough to challenge.
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-6 max-w-3xl text-lg leading-relaxed sm:text-xl">
|
||||||
|
“Local” is not a magic word. Here is where each task happens, when a
|
||||||
|
network can be involved, what the design protects, and what it does
|
||||||
|
not.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-border-soft bg-surface/80 border-y px-4 py-18 sm:px-6 lg:py-22">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<SectionLabel>Data map</SectionLabel>
|
||||||
|
<h2 className="text-foreground mt-4 font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
Task by task, not “everything” absolutes.
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="border-border-soft bg-canvas mt-10 overflow-x-auto rounded-2xl border shadow-[0_18px_55px_rgba(15,23,42,0.06)]">
|
||||||
|
<table className="w-full min-w-[780px] border-collapse text-left text-sm">
|
||||||
|
<caption className="sr-only">
|
||||||
|
Medscribe tasks, processing location, network behavior, and user
|
||||||
|
control
|
||||||
|
</caption>
|
||||||
|
<thead>
|
||||||
|
<tr className="border-border-soft bg-surface-soft border-b">
|
||||||
|
{[
|
||||||
|
"Task",
|
||||||
|
"Where it happens",
|
||||||
|
"Network behavior",
|
||||||
|
"User control",
|
||||||
|
].map((heading) => (
|
||||||
|
<th
|
||||||
|
key={heading}
|
||||||
|
scope="col"
|
||||||
|
className="text-muted-foreground px-5 py-4 text-xs font-bold tracking-[0.1em] uppercase"
|
||||||
|
>
|
||||||
|
{heading}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{dataMap.map((row) => (
|
||||||
|
<tr
|
||||||
|
key={row.task}
|
||||||
|
className="border-border-soft/80 border-b last:border-0"
|
||||||
|
>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
className="text-foreground px-5 py-4 font-semibold"
|
||||||
|
>
|
||||||
|
{row.task}
|
||||||
|
</th>
|
||||||
|
<td className="text-muted-foreground px-5 py-4">
|
||||||
|
{row.location}
|
||||||
|
</td>
|
||||||
|
<td className="text-muted-foreground px-5 py-4">
|
||||||
|
{row.network}
|
||||||
|
</td>
|
||||||
|
<td className="text-muted-foreground px-5 py-4">
|
||||||
|
{row.control}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="px-4 py-20 sm:px-6 lg:py-28">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
icon: LockKeyhole,
|
||||||
|
title: "Supported AI work stays local",
|
||||||
|
body: productClaims.localAi.publicCopy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Database,
|
||||||
|
title: "The primary record stays local",
|
||||||
|
body: `${productClaims.localRecord.publicCopy} The beta relies on the device and operating system's protections; it does not claim a separately encrypted app database.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Wifi,
|
||||||
|
title: "Downloads are the network exception",
|
||||||
|
body: "Installing or updating a model can require a network. Approved model files are checked against an expected SHA-256 digest before use.",
|
||||||
|
},
|
||||||
|
].map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
key={item.title}
|
||||||
|
className="border-border-soft bg-surface rounded-2xl border p-6"
|
||||||
|
>
|
||||||
|
<span className="bg-primary-light text-primary flex size-11 items-center justify-center rounded-xl">
|
||||||
|
<Icon className="size-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
<h3 className="text-foreground mt-5 font-serif text-2xl font-semibold">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mt-3 text-sm leading-relaxed">
|
||||||
|
{item.body}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-border-soft border-y bg-[#f4f0e8]/75 px-4 py-20 sm:px-6 lg:py-24 dark:bg-[#151b25]">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<div className="grid gap-12 lg:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<div className="bg-primary-light text-primary flex size-12 items-center justify-center rounded-xl">
|
||||||
|
<KeyRound className="size-6" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<SectionLabel className="mt-6">Caregiver sharing</SectionLabel>
|
||||||
|
<h2 className="text-foreground mt-4 font-serif text-4xl font-semibold tracking-tight">
|
||||||
|
Verify a person, then export deliberately.
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground mt-5 leading-relaxed">
|
||||||
|
New snapshots use authenticated encryption targeted to the
|
||||||
|
recipient key. Ciphertext integrity and wrong-recipient
|
||||||
|
rejection are tested. The current import path does not yet
|
||||||
|
require the sender to be in the recipient's active trust
|
||||||
|
list, and recipient-key metadata is not authenticated and
|
||||||
|
checked on import. Verify the sender out of band. The beta does
|
||||||
|
not claim the Signal protocol or live synchronization.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="border-border-soft bg-surface rounded-2xl border p-6 sm:p-8">
|
||||||
|
<p className="text-foreground font-semibold">
|
||||||
|
The practical rule
|
||||||
|
</p>
|
||||||
|
<ol className="mt-5 space-y-4">
|
||||||
|
{[
|
||||||
|
"Compare the safety number with the intended caregiver.",
|
||||||
|
"Review the snapshot before every material export.",
|
||||||
|
"Re-export after important changes; it is not automatic.",
|
||||||
|
"Treat revocation as a block on future local exports; it cannot invalidate already-exported ciphertext.",
|
||||||
|
].map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="text-muted-foreground flex gap-3 text-sm leading-relaxed"
|
||||||
|
>
|
||||||
|
<span className="bg-primary-light text-primary flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-bold">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="px-4 py-20 sm:px-6 lg:py-28">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<SectionLabel>Evidence, not superlatives</SectionLabel>
|
||||||
|
<div className="mt-4 grid gap-10 lg:grid-cols-[0.85fr_1.15fr] lg:items-end">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-foreground font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
What the current release can actually point to.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
Medication language is based on named public reference datasets,
|
||||||
|
but condensed wording is not FDA-reviewed. Coverage counts
|
||||||
|
describe the bundle; they do not prove clinical completeness.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{[
|
||||||
|
[
|
||||||
|
productRelease.drugFacts.toLocaleString(),
|
||||||
|
"Condensed drug records",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
productRelease.rxNormTerms.toLocaleString(),
|
||||||
|
"RxNorm search terms",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
productRelease.interactionPairs.toLocaleString(),
|
||||||
|
"Known high-priority pairs",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
productRelease.flaggedForReview.toLocaleString(),
|
||||||
|
"Records flagged for review",
|
||||||
|
],
|
||||||
|
].map(([value, label]) => (
|
||||||
|
<div
|
||||||
|
key={label}
|
||||||
|
className="border-border-soft bg-surface rounded-2xl border p-6"
|
||||||
|
>
|
||||||
|
<dt className="text-muted-foreground text-sm leading-relaxed">
|
||||||
|
{label}
|
||||||
|
</dt>
|
||||||
|
<dd className="text-foreground mt-2 font-serif text-4xl font-semibold">
|
||||||
|
{value}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="border-border-soft bg-surface-soft mt-6 flex gap-3 rounded-2xl border p-5">
|
||||||
|
<FileCheck2
|
||||||
|
className="text-primary mt-0.5 size-5 shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||||
|
Source families include openFDA label data, RxNorm terminology,
|
||||||
|
MED-RT and DailyMed class inputs, plus a bounded high-priority
|
||||||
|
interaction list. Release dates and full methodology should be
|
||||||
|
published with each wider release.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="border-border-soft bg-surface/75 border-t px-4 py-20 sm:px-6">
|
||||||
|
<div className="mx-auto grid max-w-6xl gap-8 lg:grid-cols-2">
|
||||||
|
<div className="border-success/25 bg-success-light/45 rounded-2xl border p-6 sm:p-8">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Check className="text-success size-6" aria-hidden="true" />
|
||||||
|
<h2 className="text-foreground font-serif text-2xl font-semibold">
|
||||||
|
Designed to protect against
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<ul className="text-muted-foreground mt-5 space-y-3 text-sm leading-relaxed">
|
||||||
|
{protects.map((item) => (
|
||||||
|
<li key={item} className="border-success/30 border-l-2 pl-4">
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-amber-500/25 bg-amber-50 p-6 text-slate-900 sm:p-8 dark:bg-amber-950/30 dark:text-slate-100">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<AlertTriangle
|
||||||
|
className="size-6 text-amber-700 dark:text-amber-300"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<h2 className="font-serif text-2xl font-semibold">
|
||||||
|
It does not eliminate
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-5 space-y-3 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
|
||||||
|
{doesNotProtect.map((item) => (
|
||||||
|
<li key={item} className="border-l-2 border-amber-500/35 pl-4">
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-10 flex max-w-6xl flex-col items-start justify-between gap-5 rounded-2xl bg-[#0f4f4b] p-7 text-white sm:flex-row sm:items-center sm:p-9">
|
||||||
|
<div>
|
||||||
|
<h2 className="font-serif text-3xl font-semibold">
|
||||||
|
Help test the boundary.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-sm text-teal-50/75">
|
||||||
|
The internal beta is focused on correctness, comprehension, and
|
||||||
|
caregiver handoffs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
className="border-white/20 bg-white text-[#0f4f4b] hover:bg-teal-50"
|
||||||
|
>
|
||||||
|
<Link href="/beta">
|
||||||
|
Beta expectations
|
||||||
|
<ArrowRight className="size-4" aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
import { PrintButton } from "~/components/print-button";
|
||||||
|
import { SectionLabel } from "~/components/section-label";
|
||||||
|
import { WalgreensPrintSummary } from "~/components/walgreens-print-summary";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Walgreens executive summary",
|
||||||
|
robots: { index: false, follow: false, nocache: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function WalgreensSummaryPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WalgreensPrintSummary />
|
||||||
|
<div className="print-hidden mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:py-24">
|
||||||
|
<SectionLabel>Executive summary · Walgreens</SectionLabel>
|
||||||
|
<h1 className="text-foreground mt-4 font-serif text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||||
|
The pharmacy executive summary
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mx-auto mt-5 max-w-xl leading-relaxed">
|
||||||
|
This page is formatted as a two-page print document: what Medscribe
|
||||||
|
can do, and how every capability pays off twice, for the patient and
|
||||||
|
for Walgreens. Use Print PDF (or Cmd+P, then Save as PDF) to export
|
||||||
|
it.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex justify-center">
|
||||||
|
<PrintButton size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,11 +17,12 @@ const playfair = Playfair_Display({
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
default: "Medscribe | Be known when care gets complicated",
|
default: "Medscribe | Carry the context when care gets complicated",
|
||||||
template: "Medscribe | %s",
|
template: "Medscribe | %s",
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
"A private, emergency-ready medical context app for families managing complex care. Keep visits, medications, baseline, allergies, and caregiver handoffs current on device.",
|
"A private, patient-controlled beta that helps keep visits, medications, and critical context together on your device.",
|
||||||
|
applicationName: "Medscribe",
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: "/favicon.ico", sizes: "any" },
|
{ url: "/favicon.ico", sizes: "any" },
|
||||||
@@ -29,12 +30,14 @@ export const metadata: Metadata = {
|
|||||||
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||||
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||||
],
|
],
|
||||||
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
|
apple: [
|
||||||
|
{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Medscribe",
|
title: "Medscribe",
|
||||||
description:
|
description:
|
||||||
"Be known when care gets complicated. Private medical context for families managing complex care.",
|
"Carry the context when care gets complicated. Private, patient-controlled medical context on your device.",
|
||||||
type: "website",
|
type: "website",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -43,7 +46,10 @@ export default function RootLayout({
|
|||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={`${instrumentSans.variable} ${playfair.variable}`}>
|
<html
|
||||||
|
lang="en"
|
||||||
|
className={`${instrumentSans.variable} ${playfair.variable}`}
|
||||||
|
>
|
||||||
<body className="min-h-screen">
|
<body className="min-h-screen">
|
||||||
{children}
|
{children}
|
||||||
<UmamiAnalytics />
|
<UmamiAnalytics />
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
return {
|
||||||
|
rules: {
|
||||||
|
userAgent: "*",
|
||||||
|
allow: "/",
|
||||||
|
disallow: [
|
||||||
|
"/api/",
|
||||||
|
"/briefs/",
|
||||||
|
"/cvs",
|
||||||
|
"/executive-summary",
|
||||||
|
"/landscape",
|
||||||
|
"/login",
|
||||||
|
"/walgreens",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import {
|
||||||
|
HeartPulse,
|
||||||
|
Layers,
|
||||||
|
Mic,
|
||||||
|
MessageSquare,
|
||||||
|
ScanLine,
|
||||||
|
ShieldCheck,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pharmacy-specific print summary (CVS-targeted, reusable for other pharmacies).
|
||||||
|
* Two designed pages, built for the product rather than mirroring the Northwell
|
||||||
|
* layout:
|
||||||
|
* Page 1 - what Medscribe can do (capabilities, patient-facing power).
|
||||||
|
* Page 2 - every capability pays off twice (patient value -> CVS value).
|
||||||
|
* Reuses the `northwell-print-*` sheet/label classes from globals.css only as
|
||||||
|
* the page frame; the interior is its own design.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function PrintPage({
|
||||||
|
page,
|
||||||
|
children,
|
||||||
|
}: Readonly<{ page: number; children: React.ReactNode }>) {
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={
|
||||||
|
page === 1
|
||||||
|
? "northwell-print-page"
|
||||||
|
: "northwell-print-page northwell-print-page--continuation"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img className="northwell-print-wash" src="/print-wash.svg" alt="" />
|
||||||
|
{page === 1 ? (
|
||||||
|
<header className="northwell-print-header">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img className="h-6 w-auto" src="/medscribe-ms-logo.svg" alt="" />
|
||||||
|
<span className="font-serif text-[17px] font-semibold text-slate-950">
|
||||||
|
Medscribe
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] font-semibold tracking-[0.18em] text-slate-500 uppercase">
|
||||||
|
Executive summary · CVS Health
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="northwell-print-content">{children}</div>
|
||||||
|
|
||||||
|
<footer className="northwell-print-footer">
|
||||||
|
<span>Page {page} of 2</span>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const capabilities = [
|
||||||
|
{
|
||||||
|
icon: Mic,
|
||||||
|
title: "Understand a visit",
|
||||||
|
body: "Type, paste, snap the after-visit summary, or record the conversation with consent. Medscribe transcribes and summarizes it on the phone.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ScanLine,
|
||||||
|
title: "Read a pill bottle",
|
||||||
|
body: "Point the camera at a prescription label and the drug, dose, and schedule land in the record automatically.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: MessageSquare,
|
||||||
|
title: "Answer the hard questions",
|
||||||
|
body: "Ask whether two medications are safe together and get an answer grounded in bundled FDA drug facts and known interaction pairs, not a chatbot's guess.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Layers,
|
||||||
|
title: "Hold the whole picture",
|
||||||
|
body: "Medications, allergies, conditions, baseline, and visit history across every provider, in one place the patient owns.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: HeartPulse,
|
||||||
|
title: "Be ready for an emergency",
|
||||||
|
body: "A patient-controlled Know Me card puts the time-critical facts one tap away for a caregiver or an ER.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ShieldCheck,
|
||||||
|
title: "Share without exposure",
|
||||||
|
body: "End-to-end encrypted sharing lets a caregiver help, without a server ever reading the record.",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const payoffs = [
|
||||||
|
{
|
||||||
|
patientTitle: "One picture, everywhere they are treated",
|
||||||
|
patient:
|
||||||
|
"Nothing falls through the cracks between a specialist, an urgent care, and the pharmacy.",
|
||||||
|
cvsTitle: "One central pharmacy",
|
||||||
|
cvs: "Refills route back to CVS and scattered scripts consolidate, instead of leaking to Amazon, Cost Plus, or mail order. Share of scripts, won.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientTitle: "Medications that fit their day",
|
||||||
|
patient:
|
||||||
|
"Reminders and refill nudges that actually help, so they stay on their medications.",
|
||||||
|
cvsTitle: "Star Ratings",
|
||||||
|
cvs: "Diabetes, hypertension, and statin adherence are triple-weighted in Part D Star Ratings: Quality Bonus Payments and retention for Aetna and SilverScript.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientTitle: "Fewer dangerous surprises",
|
||||||
|
patient:
|
||||||
|
"Interactions and duplicate prescriptions caught across every prescriber, not one pharmacy's slice.",
|
||||||
|
cvsTitle: "Lower cost of care",
|
||||||
|
cvs: "Fewer adverse events and ER visits: direct medical spend saved for the members shared with Caremark and Aetna.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientTitle: "Ready at the counter",
|
||||||
|
patient:
|
||||||
|
"They arrive with a clean list and their routine questions already answered.",
|
||||||
|
cvsTitle: "Pharmacist leverage",
|
||||||
|
cvs: "Less low-value counter volume, more time for the billable MTM and CMR work pharmacists already do.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientTitle: "A tool that is theirs",
|
||||||
|
patient:
|
||||||
|
"An advocate they control, on their own device. Not a sales pitch, and never on a server.",
|
||||||
|
cvsTitle: "Engagement without the liability",
|
||||||
|
cvs: "A CVS-branded habit patients actually open, with no PHI on a server: no new breach surface and no BAA to negotiate.",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function CvsPrintSummary() {
|
||||||
|
return (
|
||||||
|
<div className="northwell-print-only" aria-hidden="true">
|
||||||
|
<PrintPage page={1}>
|
||||||
|
<div className="grid grid-cols-[1.16fr_0.84fr] items-start gap-9 pt-5">
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">What Medscribe is</p>
|
||||||
|
<h1 className="mt-4 font-serif text-[40px] leading-[1.0] font-semibold tracking-[-0.035em] text-slate-950">
|
||||||
|
It captures, understands, and carries the patient's whole
|
||||||
|
medical picture.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 text-[15px] leading-[1.55] text-slate-600">
|
||||||
|
Real medical AI on the patient's phone. Medscribe summarizes
|
||||||
|
visits, reads pill bottles, answers medication questions from
|
||||||
|
bundled drug facts, and keeps one emergency-ready record across
|
||||||
|
every provider. Nothing is sent to the cloud.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 grid grid-cols-2 gap-x-5 gap-y-2 text-[8.5px] font-bold tracking-[0.08em] text-slate-600 uppercase">
|
||||||
|
{[
|
||||||
|
"On-device AI",
|
||||||
|
"Facts-first",
|
||||||
|
"Patient-owned",
|
||||||
|
"iOS and Android beta",
|
||||||
|
].map((badge) => (
|
||||||
|
<span key={badge} className="flex items-center gap-2">
|
||||||
|
<span className="size-1 rounded-full bg-teal-600" />
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto w-[186px] rounded-[28px] border-[6px] border-slate-950 bg-slate-950 p-1 shadow-[0_18px_45px_rgba(15,23,42,0.18)]">
|
||||||
|
<div className="overflow-hidden rounded-[19px] bg-white">
|
||||||
|
<Image
|
||||||
|
src="/screenshots/medications.png"
|
||||||
|
alt="Medscribe medications screen"
|
||||||
|
width={1206}
|
||||||
|
height={2622}
|
||||||
|
priority
|
||||||
|
unoptimized
|
||||||
|
className="block h-auto w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 border-t border-teal-700/25 pt-5">
|
||||||
|
<p className="northwell-print-label">What it can do</p>
|
||||||
|
<h2 className="mt-2 font-serif text-[25px] leading-[1.12] font-semibold text-slate-950">
|
||||||
|
Real medical AI, working entirely on the phone
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-3 gap-x-7 gap-y-7">
|
||||||
|
{capabilities.map(({ icon: Icon, title, body }) => (
|
||||||
|
<div key={title}>
|
||||||
|
<Icon className="size-5 text-teal-700" strokeWidth={1.75} />
|
||||||
|
<h3 className="mt-2.5 font-serif text-[16px] leading-[1.15] font-semibold text-slate-950">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1.5 text-[10px] leading-[1.5] text-slate-600">
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 grid grid-cols-3 gap-7 border-t border-teal-700/25 pt-5">
|
||||||
|
{[
|
||||||
|
[
|
||||||
|
"On the device, not the cloud",
|
||||||
|
"Summarization and medication Q&A run on the phone. No health data goes to a server for AI, so there is no new breach surface and no BAA.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Facts first, not guesswork",
|
||||||
|
"Drug facts, classes, and interaction pairs come from bundled reference data. The model helps phrase the answer; it does not invent it.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Owned by the patient",
|
||||||
|
"One record that spans every provider, that the patient controls, carries, and decides how to share.",
|
||||||
|
],
|
||||||
|
].map(([title, body]) => (
|
||||||
|
<div key={title}>
|
||||||
|
<p className="font-serif text-[14px] font-semibold text-slate-950">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1.5 text-[9.5px] leading-[1.5] text-slate-600">
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PrintPage>
|
||||||
|
|
||||||
|
<PrintPage page={2}>
|
||||||
|
<section className="pt-3">
|
||||||
|
<p className="northwell-print-label">Who it helps</p>
|
||||||
|
<h2 className="northwell-print-title">Every capability pays off twice</h2>
|
||||||
|
<p className="northwell-print-lede max-w-[660px]">
|
||||||
|
The features that make Medscribe genuinely useful to the patient are
|
||||||
|
the same ones that move the numbers CVS is measured on. Cross-provider
|
||||||
|
by design; one central pharmacy in practice.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-[0.92fr_1.08fr] gap-8">
|
||||||
|
<p className="text-[9px] font-bold tracking-[0.18em] text-slate-500 uppercase">
|
||||||
|
For the patient
|
||||||
|
</p>
|
||||||
|
<p className="text-[9px] font-bold tracking-[0.18em] text-teal-700 uppercase">
|
||||||
|
For CVS
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-1">
|
||||||
|
{payoffs.map((row) => (
|
||||||
|
<div
|
||||||
|
key={row.cvsTitle}
|
||||||
|
className="grid grid-cols-[0.92fr_1.08fr] gap-8 border-t border-slate-200 py-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-serif text-[14px] leading-[1.2] font-semibold text-slate-950">
|
||||||
|
{row.patientTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-[9.5px] leading-[1.45] text-slate-600">
|
||||||
|
{row.patient}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="border-l border-teal-600/30 pl-8">
|
||||||
|
<h3 className="font-serif text-[14px] leading-[1.2] font-semibold text-teal-900">
|
||||||
|
{row.cvsTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-[9.5px] leading-[1.45] text-slate-600">
|
||||||
|
{row.cvs}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-5 bg-teal-50/70 px-5 py-3 text-[10px] leading-[1.5] text-slate-700">
|
||||||
|
<span className="font-semibold text-slate-900">
|
||||||
|
It sits beside the CVS stack, not on top of it.{" "}
|
||||||
|
</span>
|
||||||
|
Dispensing, Caremark benefits, and the CVS app keep doing their jobs.
|
||||||
|
Medscribe is the patient-owned layer that feeds value back in with
|
||||||
|
consent. A partnership unlocks co-branding, refill routing, and
|
||||||
|
adherence reporting against Star and EQuIPP measures.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section className="mt-6 grid grid-cols-[1.12fr_0.88fr] items-start gap-9 border-t border-slate-300 pt-5">
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">The ask</p>
|
||||||
|
<h3 className="mt-3 font-serif text-[20px] leading-[1.22] font-semibold text-slate-950">
|
||||||
|
A short conversation about a record that moves adherence and keeps
|
||||||
|
refills at CVS.
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-[10.5px] leading-[1.5] text-slate-600">
|
||||||
|
Live in beta on iOS and Android today. The core patient experience
|
||||||
|
ships now; the business layer is what a partnership builds together.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">Who is behind it</p>
|
||||||
|
<h3 className="mt-3 font-serif text-[19px] font-semibold text-slate-950">
|
||||||
|
Sean O'Connor
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-[10.5px] leading-[1.5] text-slate-600">
|
||||||
|
A Bucknell computer science and engineering graduate who built
|
||||||
|
Medscribe after watching his family lose the full picture of his
|
||||||
|
grandfather's care.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PrintPage>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,120 +1,122 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { KeyboardEvent } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
Camera,
|
|
||||||
Check,
|
Check,
|
||||||
ContactRound,
|
ContactRound,
|
||||||
Home,
|
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
Shield,
|
Pill,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { PhoneMockup } from "~/components/phone-mockup";
|
import { PhoneMockup } from "~/components/phone-mockup";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{
|
|
||||||
id: "home",
|
|
||||||
label: "Profile",
|
|
||||||
icon: Home,
|
|
||||||
title: "One profile for the person you care for",
|
|
||||||
description:
|
|
||||||
"Visits, medications, reminders, and questions live together under the person they belong to.",
|
|
||||||
points: [
|
|
||||||
"Switch between everyone you care for",
|
|
||||||
"One home for the whole picture",
|
|
||||||
"Nothing scattered across apps",
|
|
||||||
],
|
|
||||||
image: "/screenshots/home.png",
|
|
||||||
alt: "Medscribe home dashboard with record visit, search, medications, and ask shortcuts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "context",
|
|
||||||
label: "Context",
|
|
||||||
icon: ContactRound,
|
|
||||||
title: "The context behind the handoff",
|
|
||||||
description:
|
|
||||||
"Conditions, medication changes, visit summaries, and caregiver notes stay close enough to become useful when time is short.",
|
|
||||||
points: [
|
|
||||||
"Baseline, allergies, and conditions up front",
|
|
||||||
"Time-critical medications flagged",
|
|
||||||
"A source trail behind each fact",
|
|
||||||
],
|
|
||||||
image: "/screenshots/emergency-record.png",
|
|
||||||
alt: "Medscribe emergency record with critical care context",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "visits",
|
id: "visits",
|
||||||
label: "Visits",
|
label: "Visits",
|
||||||
icon: CalendarDays,
|
icon: CalendarDays,
|
||||||
title: "Visit recordings that refresh the record",
|
title: "Turn a visit into context you can review",
|
||||||
description:
|
description:
|
||||||
"A visit can become searchable context, so a medication change or follow-up instruction does not depend on memory alone.",
|
"Type or paste notes, import a portal screenshot or document, or use consent-gated recording. The beta creates a local summary for the person to check.",
|
||||||
points: [
|
points: [
|
||||||
"Consent-gated, on-device recording",
|
"Supported transcription and summarization run on device",
|
||||||
"Searchable transcript and summary",
|
"The person reviews generated information",
|
||||||
"Follow-ups captured, not forgotten",
|
"Structured visit-to-record change review is still roadmap",
|
||||||
],
|
],
|
||||||
image: "/screenshots/visit-timeline.png",
|
image: "/screenshots/visit-detail.png",
|
||||||
alt: "Medscribe visit timeline with appointment cards",
|
alt: "Synthetic Medscribe beta visit detail showing a visit summary and follow-up information",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "meds",
|
id: "medications",
|
||||||
label: "Meds",
|
label: "Medications",
|
||||||
icon: Camera,
|
icon: Pill,
|
||||||
title: "Medication OCR, reminders, and history",
|
title: "Keep the daily medication picture close",
|
||||||
description:
|
description:
|
||||||
"Prescription labels, reminders, refill alerts, and dose history help the family see what is supposed to happen and what actually happened.",
|
"Scan a label, correct the extracted fields, set reminders, follow refills, and see a local intake history without asking a language model to invent drug facts.",
|
||||||
points: [
|
points: [
|
||||||
"Scan labels with the camera",
|
"Label text remains editable before it becomes the record",
|
||||||
"Reminders and refill alerts",
|
"Bundled facts and known pairs use named reference datasets",
|
||||||
"Dose history at a glance",
|
"Coverage is intentionally bounded, not a complete interaction check",
|
||||||
],
|
],
|
||||||
image: "/screenshots/medications.png",
|
image: "/screenshots/medication-detail.png",
|
||||||
alt: "Medscribe medications screen with active medications",
|
alt: "Synthetic Medscribe beta medication detail with schedule and medication information",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ask",
|
id: "questions",
|
||||||
label: "Ask",
|
label: "Questions",
|
||||||
icon: MessageCircle,
|
icon: MessageCircle,
|
||||||
title: "Medication questions grounded in the record",
|
title: "Facts first, fluent answers second",
|
||||||
description:
|
description:
|
||||||
"Plain-language questions can draw from the person's record and bundled FDA facts, while keeping diagnosis and dosing with clinicians.",
|
"Medication questions can use the person’s local record and bundled fields. Dosing changes, diagnosis, and treatment decisions stay with a clinician or pharmacist.",
|
||||||
points: [
|
points: [
|
||||||
"Plain-language answers",
|
"Known facts render from bundled data",
|
||||||
"Grounded in the record and FDA facts",
|
"The local model helps with conversational phrasing",
|
||||||
"Defers dosing and diagnosis to clinicians",
|
"High-risk or unsupported questions defer to a professional",
|
||||||
],
|
],
|
||||||
image: "/screenshots/chat.png",
|
image: "/screenshots/chat-response.png",
|
||||||
alt: "Medscribe Ask chat answering a question about blood pressure medications",
|
alt: "Synthetic Medscribe beta answer with medication context and a safety boundary",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "privacy",
|
id: "handoff",
|
||||||
label: "Privacy",
|
label: "Know Me",
|
||||||
icon: Shield,
|
icon: ContactRound,
|
||||||
title: "Private by architecture, not policy",
|
title: "Prepare one readable handoff",
|
||||||
description:
|
description:
|
||||||
"The sensitive work happens on device, so the family record is not another cloud inbox waiting to be mined.",
|
"The Know Me view brings selected medications, allergies, conditions, baseline, and contacts together. Current, unconfirmed, and review-needed states make uncertainty visible.",
|
||||||
points: [
|
points: [
|
||||||
"On-device AI, no cloud processing",
|
"Medication schedules can include source and confirmation",
|
||||||
"No inbox to breach or mine",
|
"The person can show or share the snapshot they reviewed",
|
||||||
"You control what is ever shared",
|
"It complements—not replaces—clinical verification",
|
||||||
],
|
],
|
||||||
image: "/screenshots/settings.png",
|
image: "/screenshots/emergency-record.png",
|
||||||
alt: "Medscribe settings showing on-device AI models and privacy notice",
|
alt: "Synthetic Medscribe beta Know Me handoff with active medications and review state",
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
type FeatureId = (typeof features)[number]["id"];
|
||||||
|
|
||||||
export function FeatureShowcase() {
|
export function FeatureShowcase() {
|
||||||
const [activeId, setActiveId] = useState<(typeof features)[number]["id"]>("home");
|
const [activeId, setActiveId] = useState<FeatureId>("visits");
|
||||||
const active = features.find((f) => f.id === activeId) ?? features[0];
|
const active =
|
||||||
|
features.find((feature) => feature.id === activeId) ?? features[0];
|
||||||
|
|
||||||
|
const onTabKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
const activeIndex = features.findIndex(
|
||||||
|
(feature) => feature.id === activeId,
|
||||||
|
);
|
||||||
|
let nextIndex: number | undefined;
|
||||||
|
|
||||||
|
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
|
||||||
|
nextIndex = (activeIndex + 1) % features.length;
|
||||||
|
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
|
||||||
|
nextIndex = (activeIndex - 1 + features.length) % features.length;
|
||||||
|
} else if (event.key === "Home") {
|
||||||
|
nextIndex = 0;
|
||||||
|
} else if (event.key === "End") {
|
||||||
|
nextIndex = features.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIndex === undefined) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const nextFeature = features[nextIndex];
|
||||||
|
if (!nextFeature) return;
|
||||||
|
|
||||||
|
setActiveId(nextFeature.id);
|
||||||
|
document.getElementById(`feature-tab-${nextFeature.id}`)?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid items-center gap-8 lg:grid-cols-[minmax(0,1fr)_300px] lg:gap-12">
|
<div className="grid items-center gap-10 lg:grid-cols-[minmax(0,1fr)_340px] lg:gap-16">
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Explore the Medscribe beta"
|
||||||
|
className="flex max-w-full gap-2 overflow-x-auto pb-1"
|
||||||
|
>
|
||||||
{features.map((feature) => {
|
{features.map((feature) => {
|
||||||
const Icon = feature.icon;
|
const Icon = feature.icon;
|
||||||
const isActive = feature.id === activeId;
|
const isActive = feature.id === activeId;
|
||||||
@@ -122,49 +124,67 @@ export function FeatureShowcase() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={feature.id}
|
key={feature.id}
|
||||||
|
id={`feature-tab-${feature.id}`}
|
||||||
type="button"
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={isActive}
|
||||||
|
aria-controls={`feature-panel-${feature.id}`}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
onClick={() => setActiveId(feature.id)}
|
onClick={() => setActiveId(feature.id)}
|
||||||
|
onKeyDown={onTabKeyDown}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-all",
|
"focus-visible:ring-primary/40 inline-flex min-h-11 shrink-0 items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition-colors outline-none focus-visible:ring-2",
|
||||||
isActive
|
isActive
|
||||||
? "border-primary-button bg-primary-button text-white shadow-sm"
|
? "border-primary-button bg-primary-button text-white shadow-sm"
|
||||||
: "border-border-soft bg-surface text-muted-foreground hover:border-primary/20 hover:text-foreground",
|
: "border-border-soft bg-surface text-muted-foreground hover:border-primary/30 hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4" aria-hidden="true" />
|
||||||
{feature.label}
|
{feature.label}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div
|
||||||
<h3 className="font-serif text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">
|
id={`feature-panel-${active.id}`}
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby={`feature-tab-${active.id}`}
|
||||||
|
tabIndex={0}
|
||||||
|
className="focus-visible:ring-primary/40 rounded-xl outline-none focus-visible:ring-2"
|
||||||
|
>
|
||||||
|
<p className="text-primary text-xs font-bold tracking-[0.14em] uppercase">
|
||||||
|
Current beta
|
||||||
|
</p>
|
||||||
|
<h3 className="text-foreground mt-3 max-w-xl font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
|
||||||
{active.title}
|
{active.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground sm:text-base">
|
<p className="text-muted-foreground mt-4 max-w-xl leading-relaxed">
|
||||||
{active.description}
|
{active.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="grid gap-x-5 gap-y-2.5 sm:grid-cols-2">
|
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
|
||||||
{active.points.map((point) => (
|
{active.points.map((point) => (
|
||||||
<li
|
<li
|
||||||
key={point}
|
key={point}
|
||||||
className="flex items-start gap-2 text-sm leading-snug text-foreground"
|
className="text-foreground flex items-start gap-2.5 text-sm leading-relaxed"
|
||||||
>
|
>
|
||||||
<span className="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full bg-primary-light">
|
<span className="bg-primary-light mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full">
|
||||||
<Check className="size-3 text-primary" />
|
<Check className="text-primary size-3" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
<span>{point}</span>
|
<span>{point}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="justify-self-center lg:justify-self-end">
|
<div className="justify-self-center lg:justify-self-end">
|
||||||
<div className="flex items-center justify-center rounded-2xl border border-border-soft bg-gradient-to-b from-surface-soft to-primary-light/30 p-6 shadow-[0_18px_45px_rgba(15,23,42,0.10)]">
|
<div className="border-border-soft bg-surface-soft/80 rounded-[2.5rem] border p-5 shadow-[0_30px_80px_rgba(15,23,42,0.10)] sm:p-7">
|
||||||
<PhoneMockup src={active.image} alt={active.alt} width={188} />
|
<PhoneMockup src={active.image} alt={active.alt} width={220} />
|
||||||
|
<p className="text-muted-foreground mt-4 text-center text-xs">
|
||||||
|
Synthetic demo data · beta interface
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
function PrintPage({
|
||||||
|
page,
|
||||||
|
children,
|
||||||
|
}: Readonly<{ page: number; children: React.ReactNode }>) {
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={
|
||||||
|
page === 1
|
||||||
|
? "northwell-print-page"
|
||||||
|
: "northwell-print-page northwell-print-page--continuation"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img className="northwell-print-wash" src="/print-wash.svg" alt="" />
|
||||||
|
{page === 1 ? (
|
||||||
|
<header className="northwell-print-header">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img className="h-6 w-auto" src="/medscribe-ms-logo.svg" alt="" />
|
||||||
|
<span className="font-serif text-[17px] font-semibold text-slate-950">
|
||||||
|
Medscribe
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] font-semibold tracking-[0.18em] text-slate-500 uppercase">
|
||||||
|
Executive summary · Northwell Health
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="northwell-print-content">{children}</div>
|
||||||
|
|
||||||
|
<footer className="northwell-print-footer">
|
||||||
|
<span>Page {page} of 3</span>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const people = [
|
||||||
|
[
|
||||||
|
"The student on their own",
|
||||||
|
"Managing prescriptions and appointments for the first time, away from the parent who used to track it all.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"The newly diagnosed",
|
||||||
|
"A new routine, new instructions, and follow-ups that are a blur by the next visit.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"The long-distance caregiver",
|
||||||
|
"An adult child coordinating a parent's medications with siblings who cover different days.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"The emergency",
|
||||||
|
"An older patient at an out-of-network ER, where no one has met them or knows which medications cannot be delayed.",
|
||||||
|
],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const productPillars = [
|
||||||
|
[
|
||||||
|
"Capture visits",
|
||||||
|
"Type or paste notes, import a portal screenshot, attach a document, or use consent-gated recording. Summarization stays on device.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Keep medications current",
|
||||||
|
"Pill-bottle label recognition, reminders, refill tracking, dose history, and questions grounded in bundled FDA facts.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Be ready to hand off",
|
||||||
|
"A patient-controlled Know Me record and encrypted caregiver sharing keep critical context ready when the chart is incomplete.",
|
||||||
|
],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const northwellFit = [
|
||||||
|
[
|
||||||
|
"AI patients can trust",
|
||||||
|
"Visit summarization and medication Q&A run on the phone, while medication facts come from bundled reference data rather than model recall. It is a clear AI posture for patients, privacy teams, and clinicians.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"A Northwell story worth telling",
|
||||||
|
"Northwell can lead with a concrete patient benefit: one private, emergency-ready picture that travels across systems. The experience could carry Northwell's brand without pretending to replace MyChart.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"An everyday touchstone",
|
||||||
|
"Reminders, refills, dose tracking, questions, and record updates create useful reasons to return between appointments. Each moment can point the patient back to the Northwell care team.",
|
||||||
|
],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const recordContents = [
|
||||||
|
"Active medications, doses, schedules, and refill status",
|
||||||
|
"Allergies, conditions, baseline, and the context that changes a handoff",
|
||||||
|
"Visit summaries, follow-up instructions, and questions for the next visit",
|
||||||
|
"Emergency contacts and a concise, patient-controlled Know Me record",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function NorthwellPrintSummary() {
|
||||||
|
return (
|
||||||
|
<div className="northwell-print-only" aria-hidden="true">
|
||||||
|
<PrintPage page={1}>
|
||||||
|
<div className="grid grid-cols-[1.16fr_0.84fr] items-start gap-9 pt-5">
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">The patient-owned record</p>
|
||||||
|
<h1 className="mt-4 font-serif text-[44px] leading-[0.98] font-semibold tracking-[-0.035em] text-slate-950">
|
||||||
|
Be known when care gets complicated
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 text-[16px] leading-[1.55] text-slate-600">
|
||||||
|
Medscribe keeps a patient's medications, visits, baseline,
|
||||||
|
and critical care context current between appointments, then makes
|
||||||
|
that picture ready for a caregiver, a new doctor, or an ER.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 grid grid-cols-2 gap-x-5 gap-y-2 text-[8.5px] font-bold tracking-[0.08em] text-slate-600 uppercase">
|
||||||
|
{[
|
||||||
|
"On-device AI",
|
||||||
|
"Emergency-ready",
|
||||||
|
"Patient-owned",
|
||||||
|
"iOS and Android beta",
|
||||||
|
].map((badge) => (
|
||||||
|
<span key={badge} className="flex items-center gap-2">
|
||||||
|
<span className="size-1 rounded-full bg-teal-600" />
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto w-[194px] rounded-[28px] border-[6px] border-slate-950 bg-slate-950 p-1 shadow-[0_18px_45px_rgba(15,23,42,0.18)]">
|
||||||
|
<div className="overflow-hidden rounded-[19px] bg-white">
|
||||||
|
<Image
|
||||||
|
src="/screenshots/home.png"
|
||||||
|
alt="Medscribe home screen"
|
||||||
|
width={1206}
|
||||||
|
height={2622}
|
||||||
|
priority
|
||||||
|
unoptimized
|
||||||
|
className="block h-auto w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-7 border-t border-teal-700/25 pt-5">
|
||||||
|
<p className="northwell-print-label">The job</p>
|
||||||
|
<p className="mt-3 font-serif text-[25px] leading-[1.2] font-semibold text-slate-950">
|
||||||
|
One trustworthy picture that survives fragmented care, missed
|
||||||
|
appointments, secondhand updates, and emergency handoffs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-3 gap-5">
|
||||||
|
{[
|
||||||
|
[
|
||||||
|
"Keep it current",
|
||||||
|
"Everyday visit and medication workflows maintain the record.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Make it obvious",
|
||||||
|
"Time-critical medications and baseline context rise to the top.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Keep it controlled",
|
||||||
|
"The patient decides what is shared, when, and with whom.",
|
||||||
|
],
|
||||||
|
].map(([title, body]) => (
|
||||||
|
<div key={title} className="pr-3">
|
||||||
|
<p className="font-serif text-[16px] font-semibold text-slate-950">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[10.5px] leading-[1.5] text-slate-600">
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-[0.72fr_1.28fr] gap-5">
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">One living picture</p>
|
||||||
|
<p className="mt-2 text-[10.5px] leading-[1.5] text-slate-600">
|
||||||
|
The record is useful in an emergency because ordinary use keeps it
|
||||||
|
from becoming a stale export.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul className="grid grid-cols-2 gap-x-5 gap-y-2 text-[9.5px] leading-[1.45] text-slate-600">
|
||||||
|
{recordContents.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="relative pl-3 before:absolute before:top-[0.42em] before:left-0 before:size-1 before:rounded-full before:bg-teal-500 before:content-['']"
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</PrintPage>
|
||||||
|
|
||||||
|
<PrintPage page={2}>
|
||||||
|
<section className="pt-3">
|
||||||
|
<p className="northwell-print-label">What it is</p>
|
||||||
|
<h2 className="northwell-print-title">
|
||||||
|
A medical record the patient owns and carries
|
||||||
|
</h2>
|
||||||
|
<p className="northwell-print-lede">
|
||||||
|
The daily tools are not separate features. They are the maintenance
|
||||||
|
system for one emergency-ready record.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 grid grid-cols-3 gap-5">
|
||||||
|
{productPillars.map(([title, body], index) => (
|
||||||
|
<div key={title} className="min-h-[120px] pr-3">
|
||||||
|
<span className="text-[10px] font-bold text-teal-700">
|
||||||
|
0{index + 1}
|
||||||
|
</span>
|
||||||
|
<h3 className="mt-2 font-serif text-[17px] font-semibold text-slate-950">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-[10.5px] leading-[1.5] text-slate-600">
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-7">
|
||||||
|
<p className="northwell-print-label">Who it is for</p>
|
||||||
|
<h2 className="northwell-print-title">
|
||||||
|
Anyone holding care together
|
||||||
|
</h2>
|
||||||
|
<div className="mt-4 grid grid-cols-2 gap-x-7 gap-y-2">
|
||||||
|
{people.map(([title, body]) => (
|
||||||
|
<div key={title} className="min-h-[70px] pr-4">
|
||||||
|
<h3 className="font-serif text-[15px] font-semibold text-slate-950">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1.5 text-[10.5px] leading-[1.45] text-slate-600">
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-7 border-l-4 border-teal-600 bg-teal-50/50 py-5 pr-5 pl-6">
|
||||||
|
<p className="northwell-print-label">What makes it different</p>
|
||||||
|
<h2 className="mt-3 font-serif text-[24px] font-semibold text-slate-950">
|
||||||
|
Every alternative owns a slice. Medscribe owns the whole job.
|
||||||
|
</h2>
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-6 text-[10.5px] leading-[1.55] text-slate-600">
|
||||||
|
<p>
|
||||||
|
Every institutional record stops at its own walls. Epic aggregates
|
||||||
|
Epic; urgent care, community specialists, and facilities on other
|
||||||
|
systems can still sit outside the picture in front of a clinician.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Visit notetakers, medication trackers, record vaults, and
|
||||||
|
emergency IDs each solve one slice. Medscribe connects daily
|
||||||
|
maintenance to the handoff across Epic and non-Epic care, on the
|
||||||
|
patient's device and under the patient's control.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-5 grid grid-cols-2 gap-7 border-t border-slate-300 pt-5">
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">Facts first</p>
|
||||||
|
<p className="mt-2 text-[10px] leading-[1.5] text-slate-600">
|
||||||
|
Medication facts, drug classes, and known interaction pairs come
|
||||||
|
from bundled reference data. The language model helps with
|
||||||
|
phrasing; it is not treated as the source of medical facts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">Local by default</p>
|
||||||
|
<p className="mt-2 text-[10px] leading-[1.5] text-slate-600">
|
||||||
|
Visit summarization and medication Q&A run on the phone.
|
||||||
|
Caregiver sharing is end-to-end encrypted, so a server is not
|
||||||
|
asked to read the patient's record in order to coordinate
|
||||||
|
care.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PrintPage>
|
||||||
|
|
||||||
|
<PrintPage page={3}>
|
||||||
|
<section className="pt-3">
|
||||||
|
<p className="northwell-print-label">The Northwell opportunity</p>
|
||||||
|
<h2 className="northwell-print-title">
|
||||||
|
A trusted AI touchstone between visits
|
||||||
|
</h2>
|
||||||
|
<p className="northwell-print-lede max-w-[650px]">
|
||||||
|
Medscribe gives Northwell a useful presence in the part of care no
|
||||||
|
portal owns: the ordinary days when patients remember, refill,
|
||||||
|
question, coordinate, and try to follow the plan.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 grid grid-cols-3 gap-7">
|
||||||
|
{northwellFit.map(([title, body], index) => (
|
||||||
|
<div key={title}>
|
||||||
|
<span className="text-[10px] font-bold text-teal-700">
|
||||||
|
0{index + 1}
|
||||||
|
</span>
|
||||||
|
<h3 className="font-serif text-[16px] font-semibold text-slate-950">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-[10px] leading-[1.55] text-slate-600">
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-7 bg-teal-50/60 px-6 py-5">
|
||||||
|
<p className="northwell-print-label">The touchstone</p>
|
||||||
|
<h2 className="mt-2 font-serif text-[24px] leading-[1.18] font-semibold text-slate-950">
|
||||||
|
MyChart organizes the appointment. Medscribe helps the patient live
|
||||||
|
the plan.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-[11px] leading-[1.55] text-slate-600">
|
||||||
|
A reminder, a refill, a quick question, or an update to the record
|
||||||
|
becomes a small moment of continuity. Presented as part of the
|
||||||
|
Northwell experience, those moments can reinforce the care plan and
|
||||||
|
point the patient back to the team that knows them.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-6 border-t border-slate-300 pt-5">
|
||||||
|
<p className="northwell-print-label">How it fits the stack</p>
|
||||||
|
<div className="mt-4 grid grid-cols-2 gap-9">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-serif text-[16px] font-semibold text-slate-950">
|
||||||
|
Epic remains the record of care
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-[10.5px] leading-[1.55] text-slate-600">
|
||||||
|
Northwell's clinical workflows, orders, results, messages,
|
||||||
|
and institutional history stay where they belong. Medscribe is
|
||||||
|
not another EHR and does not need to displace MyChart.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-serif text-[16px] font-semibold text-slate-950">
|
||||||
|
The patient carries the missing context
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-[10.5px] leading-[1.55] text-slate-600">
|
||||||
|
Non-Epic visits, medication changes, caregiver knowledge, and
|
||||||
|
emergency context stay with the patient. Consent governs when
|
||||||
|
that picture is shared back into a Northwell interaction.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-6 grid grid-cols-[1.12fr_0.88fr] gap-9 border-t border-slate-300 pt-5">
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">The marketing value</p>
|
||||||
|
<h3 className="mt-3 font-serif text-[19px] leading-[1.25] font-semibold text-slate-950">
|
||||||
|
A patient promise people can understand
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-[10.5px] leading-[1.5] text-slate-600">
|
||||||
|
Private AI. One current picture. Ready for the caregiver, the new
|
||||||
|
doctor, or the ER. That is a more human story than “AI in
|
||||||
|
healthcare,” and it gives Northwell a visible way to stand
|
||||||
|
for continuity beyond its own walls.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">Built and live</p>
|
||||||
|
<h3 className="mt-3 font-serif text-[19px] font-semibold text-slate-950">
|
||||||
|
Sean O'Connor
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-[10.5px] leading-[1.5] text-slate-600">
|
||||||
|
A Bucknell computer science and engineering graduate who built
|
||||||
|
Medscribe after watching his family lose the full picture of his
|
||||||
|
grandfather's care. The product is live in beta on iOS and
|
||||||
|
Android today.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PrintPage>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { IPhoneMockup } from "react-device-mockup";
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
@@ -27,33 +24,26 @@ export function PhoneMockup({
|
|||||||
const isSimulatorCapture = captureMode === "simulator";
|
const isSimulatorCapture = captureMode === "simulator";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("mx-auto w-fit drop-shadow-[0_18px_45px_rgba(15,23,42,0.14)]", className)}>
|
<div
|
||||||
<IPhoneMockup
|
className={cn(
|
||||||
screenWidth={width}
|
"mx-auto overflow-hidden rounded-[2.25rem] border-[7px] border-slate-950 bg-slate-950 p-1 shadow-[0_24px_60px_rgba(15,23,42,0.18)]",
|
||||||
screenType="island"
|
className,
|
||||||
frameColor="#111827"
|
)}
|
||||||
statusbarColor="#f8fafc"
|
style={{ width }}
|
||||||
hideStatusBar
|
|
||||||
hideNavBar
|
|
||||||
transparentNavBar
|
|
||||||
>
|
>
|
||||||
<div className="relative h-full w-full overflow-hidden bg-surface">
|
<div className="bg-surface relative aspect-[1206/2622] overflow-hidden rounded-[1.65rem]">
|
||||||
<Image
|
<Image
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
width={1206}
|
fill
|
||||||
height={2622}
|
|
||||||
priority={priority}
|
priority={priority}
|
||||||
unoptimized
|
sizes={`${width}px`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-full object-cover",
|
"object-cover",
|
||||||
isSimulatorCapture
|
isSimulatorCapture ? "scale-[1.22] object-[50%_58%]" : "object-top",
|
||||||
? "scale-[1.22] object-[50%_58%]"
|
|
||||||
: "object-top",
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</IPhoneMockup>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
type ProductDeviceProps = {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
className?: string;
|
||||||
|
sizes?: string;
|
||||||
|
priority?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A precomposited product capture using the licensed device bezels in
|
||||||
|
* `public/screenshots/pitch`. Keeping the frame in the source image preserves
|
||||||
|
* the bezel details without adding layout or client-side work.
|
||||||
|
*/
|
||||||
|
export function ProductDevice({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
className,
|
||||||
|
sizes = "(min-width: 1024px) 360px, (min-width: 640px) 320px, 72vw",
|
||||||
|
priority = false,
|
||||||
|
}: ProductDeviceProps) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
width={810}
|
||||||
|
height={1656}
|
||||||
|
sizes={sizes}
|
||||||
|
priority={priority}
|
||||||
|
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)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,19 @@
|
|||||||
export function SectionLabel({ children }: { children: React.ReactNode }) {
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
export function SectionLabel({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<p className="text-xs font-semibold tracking-widest text-muted-foreground uppercase">
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground text-xs font-semibold tracking-widest uppercase",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,71 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { MedscribeLogo } from "~/components/medscribe-logo";
|
||||||
|
|
||||||
|
const footerLinks = [
|
||||||
|
{ href: "/how-it-works", label: "How it works" },
|
||||||
|
{ href: "/privacy", label: "Privacy & evidence" },
|
||||||
|
{ href: "/beta", label: "Beta access" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() {
|
||||||
return (
|
return (
|
||||||
<footer className="print-hidden border-t border-border-soft bg-surface/70">
|
<footer className="print-hidden border-border-soft bg-surface/80 border-t">
|
||||||
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-4 py-10 text-sm text-muted-foreground sm:px-6 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mx-auto grid max-w-6xl gap-10 px-4 py-12 text-sm sm:px-6 md:grid-cols-[1.3fr_0.7fr_0.7fr]">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-serif text-base font-semibold text-foreground">Medscribe</p>
|
<Link
|
||||||
<p className="mt-1 max-w-md leading-relaxed">
|
href="/"
|
||||||
Be known when care gets complicated.
|
className="text-foreground focus-visible:ring-primary/40 inline-flex items-center gap-2.5 rounded-md outline-none focus-visible:ring-2"
|
||||||
|
>
|
||||||
|
<MedscribeLogo className="h-7 w-auto" />
|
||||||
|
<span className="font-serif text-lg font-semibold">Medscribe</span>
|
||||||
|
</Link>
|
||||||
|
<p className="text-muted-foreground mt-4 max-w-md leading-relaxed">
|
||||||
|
A private, patient-controlled beta for keeping visits, medications,
|
||||||
|
and critical context together on the device you carry.
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mt-3 text-xs leading-relaxed">
|
||||||
|
Medscribe does not diagnose, prescribe, or replace emergency
|
||||||
|
services or a clinician's verification.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<nav aria-label="Footer product navigation">
|
||||||
|
<p className="text-foreground font-semibold">Product</p>
|
||||||
|
<ul className="text-muted-foreground mt-3 space-y-2.5">
|
||||||
|
{footerLinks.map((item) => (
|
||||||
|
<li key={item.href}>
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className="hover:text-foreground focus-visible:ring-primary/40 rounded-sm underline-offset-4 outline-none hover:underline focus-visible:ring-2"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p className="text-foreground font-semibold">Contact</p>
|
||||||
|
<p className="text-muted-foreground mt-3 leading-relaxed">
|
||||||
Built by{" "}
|
Built by{" "}
|
||||||
<a
|
<a
|
||||||
href="https://soconnor.dev"
|
href="https://soconnor.dev"
|
||||||
className="text-foreground underline-offset-4 hover:underline"
|
className="text-foreground focus-visible:ring-primary/40 rounded-sm underline-offset-4 outline-none hover:underline focus-visible:ring-2"
|
||||||
>
|
>
|
||||||
Sean O'Connor
|
Sean O'Connor
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
<a
|
||||||
|
href="mailto:sean@soconnor.dev?subject=Medscribe%20website%20question"
|
||||||
|
className="text-foreground focus-visible:ring-primary/40 mt-2 inline-block rounded-sm underline-offset-4 outline-none hover:underline focus-visible:ring-2"
|
||||||
|
>
|
||||||
|
sean@soconnor.dev
|
||||||
|
</a>
|
||||||
|
<p className="text-muted-foreground/80 mt-5 text-xs">
|
||||||
|
© {new Date().getFullYear()} Hadlock Holdings LLC
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,133 +1,74 @@
|
|||||||
"use client";
|
import { Menu } from "lucide-react";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { FileText, Home, Menu, X } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
|
|
||||||
import { MedscribeLogo } from "~/components/medscribe-logo";
|
import { MedscribeLogo } from "~/components/medscribe-logo";
|
||||||
import { cn } from "~/lib/utils";
|
import { Button } from "~/components/ui/button";
|
||||||
|
|
||||||
const navLinks = [
|
const navigation = [
|
||||||
{ href: "/", label: "About", icon: Home },
|
{ href: "/how-it-works", label: "How it works" },
|
||||||
{ href: "/executive-summary", label: "Summary", icon: FileText },
|
{ href: "/privacy", label: "Privacy & evidence" },
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
const pathname = usePathname();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="print-hidden sticky top-3 z-50 px-3 sm:px-6">
|
<header className="print-hidden border-border-soft/70 bg-canvas/88 sticky top-0 z-50 border-b backdrop-blur-lg">
|
||||||
<div className="relative mx-auto max-w-5xl">
|
<div className="mx-auto flex h-16 max-w-6xl items-center gap-5 px-4 sm:px-6">
|
||||||
<div className="flex h-14 items-center justify-between gap-4 rounded-full border border-border-soft/80 bg-canvas/82 px-4 shadow-[0_10px_30px_rgba(15,23,42,0.08)] backdrop-blur-md sm:px-5">
|
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="flex items-center text-foreground transition-opacity hover:opacity-80"
|
className="text-foreground focus-visible:ring-primary/40 flex shrink-0 items-center gap-2.5 rounded-md transition-opacity outline-none hover:opacity-75 focus-visible:ring-2"
|
||||||
aria-label="Medscribe home"
|
aria-label="Medscribe home"
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
>
|
>
|
||||||
<MedscribeLogo className="h-7 w-[88px]" />
|
<MedscribeLogo className="h-7 w-auto" />
|
||||||
|
<span className="font-serif text-lg font-semibold tracking-tight">
|
||||||
|
Medscribe
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop nav */}
|
<nav
|
||||||
<nav className="hidden items-center gap-6 text-sm sm:flex">
|
aria-label="Primary navigation"
|
||||||
{navLinks.map((link) => {
|
className="ml-auto hidden items-center gap-1 md:flex"
|
||||||
const isActive = link.href === pathname;
|
>
|
||||||
|
{navigation.map((item) => (
|
||||||
return (
|
|
||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={item.href}
|
||||||
href={link.href}
|
href={item.href}
|
||||||
aria-current={isActive ? "page" : undefined}
|
className="text-muted-foreground hover:bg-surface-soft hover:text-foreground focus-visible:ring-primary/40 rounded-lg px-3 py-2 text-sm font-medium transition-colors outline-none focus-visible:ring-2"
|
||||||
className={cn(
|
|
||||||
"relative py-1 transition-colors",
|
|
||||||
isActive
|
|
||||||
? "text-foreground"
|
|
||||||
: "text-muted-foreground hover:text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{link.label}
|
{item.label}
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-x-0 -bottom-0.5 h-0.5 origin-center rounded-full bg-primary transition-transform duration-200",
|
|
||||||
isActive ? "scale-x-100" : "scale-x-0",
|
|
||||||
)}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
))}
|
||||||
})}
|
<Button asChild size="sm" className="ml-2">
|
||||||
|
<Link href="/beta">Request beta access</Link>
|
||||||
|
</Button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Mobile menu toggle */}
|
<details className="group relative ml-auto md:hidden">
|
||||||
<button
|
<summary className="border-border-soft bg-surface text-foreground focus-visible:ring-primary/40 flex size-11 cursor-pointer list-none items-center justify-center rounded-xl border outline-none marker:content-none focus-visible:ring-2 [&::-webkit-details-marker]:hidden">
|
||||||
type="button"
|
<Menu className="size-5" aria-hidden="true" />
|
||||||
onClick={() => setIsOpen((v) => !v)}
|
<span className="sr-only">Open navigation</span>
|
||||||
className="relative size-6 text-muted-foreground transition-colors hover:text-foreground focus:outline-none sm:hidden"
|
</summary>
|
||||||
aria-label={isOpen ? "Close menu" : "Open menu"}
|
<nav
|
||||||
aria-expanded={isOpen}
|
aria-label="Mobile navigation"
|
||||||
|
className="border-border-soft bg-surface absolute top-13 right-0 flex w-64 flex-col rounded-2xl border p-2 shadow-[0_18px_50px_rgba(15,23,42,0.16)]"
|
||||||
>
|
>
|
||||||
<Menu
|
{navigation.map((item) => (
|
||||||
className={cn(
|
|
||||||
"absolute inset-0 transition-opacity duration-200",
|
|
||||||
isOpen ? "opacity-0" : "opacity-100",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<X
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-0 transition-opacity duration-200",
|
|
||||||
isOpen ? "opacity-100" : "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile dropdown */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-x-0 top-16 overflow-hidden rounded-2xl border border-border-soft/80 bg-canvas/95 shadow-[0_10px_30px_rgba(15,23,42,0.12)] backdrop-blur-md transition-all duration-200 sm:hidden",
|
|
||||||
isOpen
|
|
||||||
? "pointer-events-auto opacity-100"
|
|
||||||
: "pointer-events-none -translate-y-1 opacity-0",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<nav className="flex flex-col p-2">
|
|
||||||
{navLinks.map((link) => {
|
|
||||||
const isActive = link.href === pathname;
|
|
||||||
const Icon = link.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={item.href}
|
||||||
href={link.href}
|
href={item.href}
|
||||||
aria-current={isActive ? "page" : undefined}
|
className="text-foreground hover:bg-surface-soft focus-visible:ring-primary/40 rounded-xl px-4 py-3 text-sm font-medium outline-none focus-visible:ring-2"
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors",
|
|
||||||
isActive
|
|
||||||
? "bg-primary-light/60 text-foreground"
|
|
||||||
: "text-muted-foreground hover:bg-surface-soft hover:text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Icon className="size-4" />
|
{item.label}
|
||||||
{link.label}
|
</Link>
|
||||||
|
))}
|
||||||
|
<Link
|
||||||
|
href="/beta"
|
||||||
|
className="bg-primary-button hover:bg-primary-button/90 focus-visible:ring-primary/40 mt-1 rounded-xl px-4 py-3 text-center text-sm font-semibold text-white outline-none focus-visible:ring-2"
|
||||||
|
>
|
||||||
|
Request beta access
|
||||||
</Link>
|
</Link>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
</nav>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Backdrop */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 -z-10 bg-canvas/30 backdrop-blur-sm transition-opacity duration-200 sm:hidden",
|
|
||||||
isOpen ? "opacity-100" : "pointer-events-none opacity-0",
|
|
||||||
)}
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex shrink-0 items-center justify-center gap-2 rounded-[14px] text-sm font-semibold whitespace-nowrap transition-all outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"print-hidden inline-flex shrink-0 items-center justify-center gap-2 rounded-[14px] text-sm font-semibold whitespace-nowrap transition-all outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import {
|
||||||
|
HeartPulse,
|
||||||
|
Layers,
|
||||||
|
Mic,
|
||||||
|
MessageSquare,
|
||||||
|
ScanLine,
|
||||||
|
ShieldCheck,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pharmacy-specific print summary (Walgreens variant). Page 1 (the product) is
|
||||||
|
* identical to the CVS piece; page 2's "For Walgreens" column reflects that
|
||||||
|
* Walgreens does not own a PBM or insurer the way CVS owns Caremark and Aetna,
|
||||||
|
* so adherence and cost value flow to pharmacy performance reimbursement and to
|
||||||
|
* Walgreens' plan and value-based-care partners, not an owned plan.
|
||||||
|
* Reuses the `northwell-print-*` sheet/label classes from globals.css as the
|
||||||
|
* page frame only.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function PrintPage({
|
||||||
|
page,
|
||||||
|
children,
|
||||||
|
}: Readonly<{ page: number; children: React.ReactNode }>) {
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={
|
||||||
|
page === 1
|
||||||
|
? "northwell-print-page"
|
||||||
|
: "northwell-print-page northwell-print-page--continuation"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img className="northwell-print-wash" src="/print-wash.svg" alt="" />
|
||||||
|
{page === 1 ? (
|
||||||
|
<header className="northwell-print-header">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img className="h-6 w-auto" src="/medscribe-ms-logo.svg" alt="" />
|
||||||
|
<span className="font-serif text-[17px] font-semibold text-slate-950">
|
||||||
|
Medscribe
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] font-semibold tracking-[0.18em] text-slate-500 uppercase">
|
||||||
|
Executive summary · Walgreens
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="northwell-print-content">{children}</div>
|
||||||
|
|
||||||
|
<footer className="northwell-print-footer">
|
||||||
|
<span>Page {page} of 2</span>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const capabilities = [
|
||||||
|
{
|
||||||
|
icon: Mic,
|
||||||
|
title: "Understand a visit",
|
||||||
|
body: "Type, paste, snap the after-visit summary, or record the conversation with consent. Medscribe transcribes and summarizes it on the phone.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ScanLine,
|
||||||
|
title: "Read a pill bottle",
|
||||||
|
body: "Point the camera at a prescription label and the drug, dose, and schedule land in the record automatically.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: MessageSquare,
|
||||||
|
title: "Answer the hard questions",
|
||||||
|
body: "Ask whether two medications are safe together and get an answer grounded in bundled FDA drug facts and known interaction pairs, not a chatbot's guess.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Layers,
|
||||||
|
title: "Hold the whole picture",
|
||||||
|
body: "Medications, allergies, conditions, baseline, and visit history across every provider and pharmacy, in one place the patient owns.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: HeartPulse,
|
||||||
|
title: "Be ready for an emergency",
|
||||||
|
body: "A patient-controlled Know Me card puts the time-critical facts one tap away for a caregiver or an ER.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ShieldCheck,
|
||||||
|
title: "Share without exposure",
|
||||||
|
body: "End-to-end encrypted sharing lets a caregiver help, without a server ever reading the record.",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const payoffs = [
|
||||||
|
{
|
||||||
|
patientTitle: "One picture, everywhere they are treated",
|
||||||
|
patient:
|
||||||
|
"Nothing falls through the cracks between a specialist, an urgent care, and the pharmacy.",
|
||||||
|
bizTitle: "One central pharmacy",
|
||||||
|
biz: "Refills route back to Walgreens and scattered scripts consolidate, instead of leaking to Amazon, CVS, Cost Plus, or mail order. Share of scripts, won.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientTitle: "Medications that fit their day",
|
||||||
|
patient:
|
||||||
|
"Reminders and refill nudges that actually help, so they stay on their medications.",
|
||||||
|
bizTitle: "Pharmacy quality and Star Ratings",
|
||||||
|
biz: "The diabetes, hypertension, and statin adherence measures are triple-weighted in Part D Star Ratings, which drives performance reimbursement (EQuIPP, performance-based networks) and quality for the plans Walgreens serves.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientTitle: "Fewer dangerous surprises",
|
||||||
|
patient:
|
||||||
|
"Interactions and duplicate prescriptions caught across every prescriber, not one pharmacy's slice.",
|
||||||
|
bizTitle: "Lower cost of care",
|
||||||
|
biz: "Fewer adverse events and ER visits: medical spend saved for the plan and value-based-care partners Walgreens shares members with.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientTitle: "Ready at the counter",
|
||||||
|
patient:
|
||||||
|
"They arrive with a clean list and their routine questions already answered.",
|
||||||
|
bizTitle: "Pharmacist leverage",
|
||||||
|
biz: "Less low-value counter volume, more time for the billable MTM and CMR work pharmacists already do.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientTitle: "A tool that is theirs",
|
||||||
|
patient:
|
||||||
|
"An advocate they control, on their own device. Not a sales pitch, and never on a server.",
|
||||||
|
bizTitle: "Engagement without the liability",
|
||||||
|
biz: "A Walgreens-branded habit patients actually open through myWalgreens, with no PHI on a server: no new breach surface and no BAA to negotiate.",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function WalgreensPrintSummary() {
|
||||||
|
return (
|
||||||
|
<div className="northwell-print-only" aria-hidden="true">
|
||||||
|
<PrintPage page={1}>
|
||||||
|
<div className="grid grid-cols-[1.16fr_0.84fr] items-start gap-9 pt-5">
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">What Medscribe is</p>
|
||||||
|
<h1 className="mt-4 font-serif text-[40px] leading-[1.0] font-semibold tracking-[-0.035em] text-slate-950">
|
||||||
|
It captures, understands, and carries the patient's whole
|
||||||
|
medical picture.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 text-[15px] leading-[1.55] text-slate-600">
|
||||||
|
Real medical AI on the patient's phone. Medscribe summarizes
|
||||||
|
visits, reads pill bottles, answers medication questions from
|
||||||
|
bundled drug facts, and keeps one emergency-ready record across
|
||||||
|
every doctor and pharmacy. Nothing is sent to the cloud.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 grid grid-cols-2 gap-x-5 gap-y-2 text-[8.5px] font-bold tracking-[0.08em] text-slate-600 uppercase">
|
||||||
|
{[
|
||||||
|
"On-device AI",
|
||||||
|
"Facts-first",
|
||||||
|
"Patient-owned",
|
||||||
|
"iOS and Android beta",
|
||||||
|
].map((badge) => (
|
||||||
|
<span key={badge} className="flex items-center gap-2">
|
||||||
|
<span className="size-1 rounded-full bg-teal-600" />
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto w-[186px] rounded-[28px] border-[6px] border-slate-950 bg-slate-950 p-1 shadow-[0_18px_45px_rgba(15,23,42,0.18)]">
|
||||||
|
<div className="overflow-hidden rounded-[19px] bg-white">
|
||||||
|
<Image
|
||||||
|
src="/screenshots/medications.png"
|
||||||
|
alt="Medscribe medications screen"
|
||||||
|
width={1206}
|
||||||
|
height={2622}
|
||||||
|
priority
|
||||||
|
unoptimized
|
||||||
|
className="block h-auto w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 border-t border-teal-700/25 pt-5">
|
||||||
|
<p className="northwell-print-label">What it can do</p>
|
||||||
|
<h2 className="mt-2 font-serif text-[25px] leading-[1.12] font-semibold text-slate-950">
|
||||||
|
Real medical AI, working entirely on the phone
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-3 gap-x-7 gap-y-7">
|
||||||
|
{capabilities.map(({ icon: Icon, title, body }) => (
|
||||||
|
<div key={title}>
|
||||||
|
<Icon className="size-5 text-teal-700" strokeWidth={1.75} />
|
||||||
|
<h3 className="mt-2.5 font-serif text-[16px] leading-[1.15] font-semibold text-slate-950">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1.5 text-[10px] leading-[1.5] text-slate-600">
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 grid grid-cols-3 gap-7 border-t border-teal-700/25 pt-5">
|
||||||
|
{[
|
||||||
|
[
|
||||||
|
"On the device, not the cloud",
|
||||||
|
"Summarization and medication Q&A run on the phone. No health data goes to a server for AI, so there is no new breach surface and no BAA.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Facts first, not guesswork",
|
||||||
|
"Drug facts, classes, and interaction pairs come from bundled reference data. The model helps phrase the answer; it does not invent it.",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Owned by the patient",
|
||||||
|
"One record that spans every provider and pharmacy, that the patient controls, carries, and decides how to share.",
|
||||||
|
],
|
||||||
|
].map(([title, body]) => (
|
||||||
|
<div key={title}>
|
||||||
|
<p className="font-serif text-[14px] font-semibold text-slate-950">
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1.5 text-[9.5px] leading-[1.5] text-slate-600">
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PrintPage>
|
||||||
|
|
||||||
|
<PrintPage page={2}>
|
||||||
|
<section className="pt-3">
|
||||||
|
<p className="northwell-print-label">Who it helps</p>
|
||||||
|
<h2 className="northwell-print-title">Every capability pays off twice</h2>
|
||||||
|
<p className="northwell-print-lede max-w-[660px]">
|
||||||
|
The features that make Medscribe genuinely useful to the patient are
|
||||||
|
the same ones that move the numbers Walgreens is measured on.
|
||||||
|
Cross-provider by design; one central pharmacy in practice.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-[0.92fr_1.08fr] gap-8">
|
||||||
|
<p className="text-[9px] font-bold tracking-[0.18em] text-slate-500 uppercase">
|
||||||
|
For the patient
|
||||||
|
</p>
|
||||||
|
<p className="text-[9px] font-bold tracking-[0.18em] text-teal-700 uppercase">
|
||||||
|
For Walgreens
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-1">
|
||||||
|
{payoffs.map((row) => (
|
||||||
|
<div
|
||||||
|
key={row.bizTitle}
|
||||||
|
className="grid grid-cols-[0.92fr_1.08fr] gap-8 border-t border-slate-200 py-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-serif text-[14px] leading-[1.2] font-semibold text-slate-950">
|
||||||
|
{row.patientTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-[9.5px] leading-[1.45] text-slate-600">
|
||||||
|
{row.patient}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="border-l border-teal-600/30 pl-8">
|
||||||
|
<h3 className="font-serif text-[14px] leading-[1.2] font-semibold text-teal-900">
|
||||||
|
{row.bizTitle}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-[9.5px] leading-[1.45] text-slate-600">
|
||||||
|
{row.biz}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-5 bg-teal-50/70 px-5 py-3 text-[10px] leading-[1.5] text-slate-700">
|
||||||
|
<span className="font-semibold text-slate-900">
|
||||||
|
It sits beside the Walgreens stack, not on top of it.{" "}
|
||||||
|
</span>
|
||||||
|
Dispensing, myWalgreens, and the Walgreens app keep doing their jobs.
|
||||||
|
Medscribe is the patient-owned layer that feeds value back in with
|
||||||
|
consent. A partnership unlocks co-branding, refill routing, and
|
||||||
|
adherence reporting against Star and EQuIPP measures.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section className="mt-6 grid grid-cols-[1.12fr_0.88fr] items-start gap-9 border-t border-slate-300 pt-5">
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">The ask</p>
|
||||||
|
<h3 className="mt-3 font-serif text-[20px] leading-[1.22] font-semibold text-slate-950">
|
||||||
|
A short conversation about a record that moves adherence and keeps
|
||||||
|
refills at Walgreens.
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-[10.5px] leading-[1.5] text-slate-600">
|
||||||
|
Live in beta on iOS and Android today. The core patient experience
|
||||||
|
ships now; the business layer is what a partnership builds together.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="northwell-print-label">Who is behind it</p>
|
||||||
|
<h3 className="mt-3 font-serif text-[19px] font-semibold text-slate-950">
|
||||||
|
Sean O'Connor
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-[10.5px] leading-[1.5] text-slate-600">
|
||||||
|
A Bucknell computer science and engineering graduate who built
|
||||||
|
Medscribe after watching his family lose the full picture of his
|
||||||
|
grandfather's care.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</PrintPage>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
export type ClaimStatus = "built" | "beta-verified" | "roadmap";
|
||||||
|
|
||||||
|
export type ProductClaim = {
|
||||||
|
publicCopy: string;
|
||||||
|
status: ClaimStatus;
|
||||||
|
evidence: string;
|
||||||
|
verifiedOn: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const productRelease = {
|
||||||
|
stage: "Internal beta",
|
||||||
|
platforms: "iOS and Android",
|
||||||
|
verifiedOn: "August 20, 2026",
|
||||||
|
drugFacts: 203,
|
||||||
|
rxNormTerms: 33_185,
|
||||||
|
interactionPairs: 1_150,
|
||||||
|
flaggedForReview: 76,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const productClaims = {
|
||||||
|
localAi: {
|
||||||
|
publicCopy:
|
||||||
|
"Visit transcription and supported AI tasks run on the device, without cloud AI inference.",
|
||||||
|
status: "built",
|
||||||
|
evidence: "App architecture and release-path audit",
|
||||||
|
verifiedOn: "2026-08-20",
|
||||||
|
},
|
||||||
|
localRecord: {
|
||||||
|
publicCopy:
|
||||||
|
"The primary record is stored locally on the person's device, with no Medscribe cloud account.",
|
||||||
|
status: "built",
|
||||||
|
evidence: "Local SQLite data architecture",
|
||||||
|
verifiedOn: "2026-08-20",
|
||||||
|
},
|
||||||
|
medicationFacts: {
|
||||||
|
publicCopy:
|
||||||
|
"Medication facts and known interaction pairs are read from bundled reference data rather than model memory.",
|
||||||
|
status: "built",
|
||||||
|
evidence: "Bundled data release manifest and deterministic answer paths",
|
||||||
|
verifiedOn: "2026-08-20",
|
||||||
|
},
|
||||||
|
caregiverSharing: {
|
||||||
|
publicCopy:
|
||||||
|
"A person can manually export an encrypted snapshot targeted to a caregiver's public key.",
|
||||||
|
status: "built",
|
||||||
|
evidence:
|
||||||
|
"QR pairing, safety-number display, encrypted export, ciphertext integrity, and wrong-recipient rejection",
|
||||||
|
verifiedOn: "2026-08-20",
|
||||||
|
},
|
||||||
|
recordFreshness: {
|
||||||
|
publicCopy:
|
||||||
|
"Medication schedules can carry source, confirmation, timezone, and freshness state in the beta.",
|
||||||
|
status: "built",
|
||||||
|
evidence: "Medication schedule and Know Me freshness implementation",
|
||||||
|
verifiedOn: "2026-08-20",
|
||||||
|
},
|
||||||
|
broaderProvenance: {
|
||||||
|
publicCopy:
|
||||||
|
"Source and freshness for conditions, allergies, baseline, and visit-derived changes are planned next.",
|
||||||
|
status: "roadmap",
|
||||||
|
evidence: "Current product roadmap",
|
||||||
|
verifiedOn: "2026-08-20",
|
||||||
|
},
|
||||||
|
} satisfies Record<string, ProductClaim>;
|
||||||
|
|
||||||
|
export const publicRoutes = [
|
||||||
|
"/",
|
||||||
|
"/how-it-works",
|
||||||
|
"/privacy",
|
||||||
|
"/beta",
|
||||||
|
] as const;
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
/** Shared beta / early-access copy for the marketing site. */
|
/** Shared beta / early-access copy for the marketing site. */
|
||||||
|
|
||||||
export const BETA_SUMMARY =
|
export const BETA_SUMMARY =
|
||||||
"A beta is available on iOS (TestFlight) and Android (Google Play internal testing). Everything runs privately on the device, so no health data leaves the phone.";
|
"Medscribe is in internal beta on iOS and Android. Visit transcription and supported AI tasks run on the device, without cloud AI inference.";
|
||||||
|
|
||||||
export const BETA_CTA_TITLE = "Try Medscribe";
|
export const BETA_CTA_TITLE = "Request beta access";
|
||||||
|
|
||||||
export const BETA_CTA_BODY =
|
export const BETA_CTA_BODY =
|
||||||
"The app is in beta on iOS and Android. Reach out for an invite if you're managing medications, visits, or emergency context for someone you love.";
|
"The beta is for people managing repeated visits or several medications, and for family caregivers willing to review the record and report errors.";
|
||||||
|
|
||||||
export const BETA_INVITE_MAILTO =
|
export const BETA_INVITE_MAILTO =
|
||||||
"mailto:sean@soconnor.dev?subject=Medscribe%20beta%20invite%20request&body=Hi%20Sean%2C%0D%0A%0D%0AI%27d%20like%20a%20beta%20invite%20to%20try%20Medscribe%20(iOS%20or%20Android).%20I%27m%20managing%20medications%2C%20visits%2C%20or%20emergency%20context%20for%20someone%20I%20care%20for.%0D%0A%0D%0AThanks%21";
|
"mailto:sean@soconnor.dev?subject=Medscribe%20beta%20access&body=Hi%20Sean%2C%0D%0A%0D%0AI%27m%20interested%20in%20the%20Medscribe%20beta.%0D%0A%0D%0APlatform%20(iOS%20or%20Android)%3A%0D%0AI%27m%20testing%20as%20(patient%2C%20caregiver%2C%20or%20both)%3A%0D%0AI%20can%20test%20with%20a%20caregiver%20(yes%20or%20no)%3A%0D%0A%0D%0APlease%20do%20not%20include%20diagnoses%2C%20medication%20names%2C%20or%20other%20health%20details%20in%20this%20email.%0D%0A%0D%0AThanks%21";
|
||||||
|
|
||||||
export const BETA_FOOTER_NOTE =
|
export const BETA_FOOTER_NOTE =
|
||||||
"Beta on iOS (TestFlight) and Android (Google Play internal testing).";
|
"Beta on iOS (TestFlight) and Android (Google Play internal testing).";
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: var(--font-instrument-sans), ui-sans-serif, system-ui, sans-serif;
|
--font-sans:
|
||||||
|
var(--font-instrument-sans), ui-sans-serif, system-ui, sans-serif;
|
||||||
--font-serif: var(--font-playfair), ui-serif, Georgia, serif;
|
--font-serif: var(--font-playfair), ui-serif, Georgia, serif;
|
||||||
|
|
||||||
/* Light palette: mirrors the app's `lightColors` (constants/colors.ts). */
|
/* Light palette: mirrors the app's `lightColors` (constants/colors.ts). */
|
||||||
@@ -53,7 +54,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-canvas font-sans text-foreground antialiased;
|
@apply bg-canvas text-foreground font-sans antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: color-mix(in srgb, var(--color-primary) 24%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +93,31 @@
|
|||||||
animation: fade-in-up 0.5s ease-out 0.3s both;
|
animation: fade-in-up 0.5s ease-out 0.3s both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Long-form homepage chapters render as they approach the viewport. */
|
||||||
|
.pitch-lazy {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: auto 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Branded one-page PDF sheet: a green-to-white gradient washed over a faint
|
* Branded one-page PDF sheet: a green-to-white gradient washed over a faint
|
||||||
* teal grid. Only rendered for print (the on-screen page mirrors the homepage).
|
* teal grid. Only rendered for print (the on-screen page mirrors the homepage).
|
||||||
@@ -97,42 +127,147 @@
|
|||||||
linear-gradient(150deg, #ccfbf1 0%, #f0fdfa 30%, #ffffff 64%),
|
linear-gradient(150deg, #ccfbf1 0%, #f0fdfa 30%, #ffffff 64%),
|
||||||
linear-gradient(rgba(15, 118, 110, 0.07) 1px, transparent 1px),
|
linear-gradient(rgba(15, 118, 110, 0.07) 1px, transparent 1px),
|
||||||
linear-gradient(90deg, rgba(15, 118, 110, 0.06) 1px, transparent 1px);
|
linear-gradient(90deg, rgba(15, 118, 110, 0.06) 1px, transparent 1px);
|
||||||
background-size: auto, 22px 22px, 22px 22px;
|
background-size:
|
||||||
|
auto,
|
||||||
|
22px 22px,
|
||||||
|
22px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page {
|
||||||
|
size: letter;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
@page {
|
:root {
|
||||||
margin: 0.25in;
|
color-scheme: light !important;
|
||||||
size: letter;
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
print-color-adjust: exact !important;
|
||||||
|
-webkit-print-color-adjust: exact !important;
|
||||||
|
animation: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
background: #ffffff !important;
|
width: 8.5in !important;
|
||||||
print-color-adjust: exact;
|
margin: 0 !important;
|
||||||
-webkit-print-color-adjust: exact;
|
padding: 0 !important;
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: #0f172a !important;
|
color: #0f172a !important;
|
||||||
|
background: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.print-hidden {
|
.print-hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.print-page {
|
.northwell-print-only {
|
||||||
max-width: none !important;
|
display: block !important;
|
||||||
padding: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.print-page * {
|
.northwell-print-page {
|
||||||
box-shadow: none !important;
|
position: relative;
|
||||||
animation: none !important;
|
box-sizing: border-box;
|
||||||
|
width: 8.5in;
|
||||||
|
height: 11in;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0.42in 0.55in 0.46in;
|
||||||
|
break-after: page;
|
||||||
|
page-break-after: always;
|
||||||
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.print-sheet {
|
.northwell-print-page:last-child {
|
||||||
print-color-adjust: exact;
|
break-after: auto;
|
||||||
-webkit-print-color-adjust: exact;
|
page-break-after: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-wash {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-header,
|
||||||
|
.northwell-print-content,
|
||||||
|
.northwell-print-footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.42in;
|
||||||
|
right: 0.55in;
|
||||||
|
left: 0.55in;
|
||||||
|
display: flex;
|
||||||
|
height: 0.43in;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-content {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.85in;
|
||||||
|
right: 0.55in;
|
||||||
|
left: 0.55in;
|
||||||
|
height: 9.43in;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-page--continuation .northwell-print-content {
|
||||||
|
top: 0.42in;
|
||||||
|
height: 9.86in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-footer {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.55in;
|
||||||
|
bottom: 0.46in;
|
||||||
|
left: 0.55in;
|
||||||
|
display: flex;
|
||||||
|
height: 0.26in;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: center;
|
||||||
|
border-top: 1px solid #cbd5e1;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 8px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-label {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: #0f766e;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-title {
|
||||||
|
margin-top: 0.12in;
|
||||||
|
font-family: var(--font-playfair), Georgia, serif;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 1.08;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.northwell-print-lede {
|
||||||
|
margin-top: 0.12in;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #475569;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||