Author SHA1 Message Date
soconnor 81109f36ea feat: launch public product website 2026-08-20 20:13:35 -04:00
soconnor d3c30859b2 finish up marketing push for northwell 2026-07-13 18:36:20 -04:00
soconnorandCursor 2f8dcd66ad Refocus homepage as Northwell exec summary with float layout.
Single-page strategic pitch for health-system readers; phone mockup wraps in newspaper-style columns; retire multi-page nav and redirect /executive-summary to home.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 14:53:23 -04:00
soconnor a590a506f8 Sharpen exec summary for the Epic-transition reader; tighten flow
- Executive summary "For health systems" now leads with Epic explicitly and
  the three cards are framed as what the EHR structurally can't do, feeding
  context back into Epic, and patient-owned persistence — aimed at a reader
  whose world is the Epic transition.
- Merge the redundant "Where we fit" section into "Why this is different"
  (kept its one strong highlight card), dropping the now-unused Badge import.
- Homepage: add a one-line "works alongside the systems a care team already
  uses, not instead of them" bridge in the differentiation section, keeping
  the hero purely consumer.
2026-07-10 00:48:53 -04:00
soconnor 86544c260a Add founder note to executive summary
Bill's intro email to Northwell primes the reader for the personal story
behind the app; the exec summary is the page that link lands on, and it
only had a bare signature. Adds the same founder line as the homepage
plus a fuller signature (Bucknell, CS&E). Screen version only — the
print one-pager's fixed layout is left untouched.
2026-07-06 23:12:22 -04:00
soconnor c70b50b867 Add founder story, tighten homepage flow
- A brief, grounded note under "The story scatters" about why Medscribe
  exists — the first real, personal detail on the public site rather than
  only the composite family narrative.
- Drop the "What travels with them" screenshot strip: it re-showed screens
  already covered by the interactive feature tour just above it, adding
  scroll length without new information.
2026-07-06 18:02:06 -04:00
soconnor 7e1f1d6388 Unpublish /landscape for now
Redirects to home, drops it from nav, and removes the dangling exec-summary
link. Content stays in the file (not deleted) for an easy revert later.
2026-07-06 17:57:37 -04:00
soconnorandCursor bbdef0faa6 Add Umami analytics and broaden beta copy for Android.
Optional NEXT_PUBLIC_UMAMI_* env vars load tracking when set; marketing pages now mention iOS TestFlight and Google Play internal testing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 17:30:03 -04:00
soconnorandClaude Opus 4.8 d6040e2a09 Remove em-dashes; resolve on-device/sharing tension; concretize health-system levers
- Strip em-dashes from exec-summary copy.
- "An ongoing touchstone": clarify the record stays on-device and the
  patient chooses what to share back (consented, revocable), resolving the
  apparent contradiction with "nothing is uploaded."
- "A branded surface": name the levers a health system is measured on
  (in-network retention, adherence/follow-through, avoidable readmissions)
  instead of an unspecified "revenue-generating" claim.
- Retune print spacing/leading to keep the one-pager on a single page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:37:13 -04:00
soconnorandClaude Opus 4.8 d90d2ad9eb Add preview dev-server launch config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:32:43 -04:00
soconnorandClaude Opus 4.8 572d8eaa94 Broaden exec-summary frame beyond emergency-ready; trim print padding
Medscribe is an everyday companion — capture (record + searchable
transcription), understand (plain-language medication chat grounded in the
record + FDA facts), and manage (OCR, reminders, refills) — that is ALSO
ready to hand to a caregiver, a new specialist, or an ER. Reframe the
snapshot, problem, and lead differentiator so the emergency handoff reads
as the sharp end of the product, not its whole definition.

Also reduce the print one-pager's outside padding (0.5in -> 0.4in sides,
0.26in -> 0.22in top) so it uses the page better. Still one page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:50:47 -04:00
soconnorandClaude Opus 4.8 dcb70f9188 Add "For health systems" and "Who it's for"; rebuild print one-pager
- Add a "For health systems" section (screen + print): ongoing touchstone /
  retention, branded per-provider revenue surface, complements (not competes
  with) the EHR.
- Add a full-width "Who it's for" section to the print PDF.
- Rebuild the print one-pager to fit one page: drop scenarios/impact from
  print, remove the silent overflow-hidden clip, tune typography/spacing,
  bottom-pin the footer, add slight padding above section headers.
- Generalize the scenario card so it doesn't read as diabetes-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:48:13 -04:00
soconnorandClaude Opus 4.8 03cf4f9a86 Add solution-landscape page and "Where we fit" section
New /landscape page maps the competitive field by category (with named
examples and the gap each leaves), an AlignCare callout, and the combined
job no incumbent owns. Add a compact, competitor-free "Where we fit"
section to the executive summary linking to it, and a Landscape nav item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:14:34 -04:00
soconnorandClaude Opus 4.8 a7a5174c7f Add "Under the hood" moat section to executive summary
Frame the defensible tech as product differentiation (no business
framing): a fine-tuned on-device model, measured safety (0→97% label
extraction, 100% unsafe-prompt refusals), and compounding local context.
Added to both the screen layout and the print one-pager.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:21:16 -04:00
45 changed files with 4750 additions and 636 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "medscribe-web",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3000
}
]
}
+4
View File
@@ -16,3 +16,7 @@
# Password required to view the site (login page at /login). # Password required to view the site (login page at /login).
# Leave empty/unset to disable the gate. # Leave empty/unset to disable the gate.
SITE_PASSWORD="" SITE_PASSWORD=""
# Umami analytics (optional). Omit NEXT_PUBLIC_UMAMI_WEBSITE_ID to disable tracking.
# NEXT_PUBLIC_UMAMI_WEBSITE_ID="your-website-uuid"
# NEXT_PUBLIC_UMAMI_SCRIPT_URL="https://cloud.umami.is/script.js"
+1
View File
@@ -20,6 +20,7 @@ next-env.d.ts
# production # production
/build /build
/output/
# misc # misc
.DS_Store .DS_Store
+47 -8
View File
@@ -1,8 +1,10 @@
# Medscribe Web # Medscribe Web
Marketing homepage for [Medscribe](../README.md) — a privacy-first, on-device medical companion for iOS. 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,35 @@ 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)
Set in `.env` (see `.env.example`):
- `NEXT_PUBLIC_UMAMI_WEBSITE_ID` — website UUID from your Umami dashboard
- `NEXT_PUBLIC_UMAMI_SCRIPT_URL` — optional; defaults to `https://cloud.umami.is/script.js` (use your self-hosted URL if needed)
Leave the website ID unset to disable tracking (default for local dev).
## Build ## Build
```bash ```bash
@@ -22,14 +53,22 @@ 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
Export **in-app content only** from the simulator or device — no simulator chrome or macOS capture toolbar. A normal in-app iOS status bar/tab bar is fine. Export **in-app content only** from the simulator or device — no simulator chrome or macOS capture toolbar. A normal in-app status bar/tab bar is fine.
1. Run the app on **iPhone 17 Pro** (or 16 Pro) simulator or 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). 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`.
+1059
View File
File diff suppressed because it is too large Load Diff
+51
View File
@@ -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=="],
} }
} }
+1
View File
@@ -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"
+14
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+108
View File
@@ -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();
}
+66
View File
@@ -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();
}
+226
View File
@@ -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>
</>
);
}
+32
View File
@@ -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>
</>
);
}
+155 -52
View File
@@ -1,14 +1,17 @@
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";
import { PhoneMockup } from "~/components/phone-mockup"; import { PhoneMockup } from "~/components/phone-mockup";
import { PrintButton } from "~/components/print-button"; import { PrintButton } from "~/components/print-button";
import { SectionLabel } from "~/components/section-label"; import { SectionLabel } from "~/components/section-label";
import { BETA_FOOTER_NOTE, BETA_SUMMARY } from "~/lib/beta";
import { import {
Card, Card,
CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
@@ -18,27 +21,31 @@ 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 =
"Your medical truth, in your pocket, and only where you say it goes."; "Your medical truth, in your pocket, and only where you say it goes.";
const SNAPSHOT = const SNAPSHOT =
"Medscribe is a private, emergency-ready medical record for families managing complex care. Patients and caregivers keep medications, visits, timing, and baseline current day to day, so a clinician or provider can be handed exactly what matters when the system does not know the patient."; "Medscribe is a private, patient-owned medical record and companion for people managing ongoing care and the families beside them. It captures visits, makes medications and instructions easy to understand and keep current day to day, and stays ready to hand to a caregiver, a new specialist, or an ER. Everything runs on the device; nothing is uploaded.";
const PROBLEM = const PROBLEM =
"Someone managing a serious condition ends up somewhere new: an ER outside their network, a covering doctor, a hospital that runs different software. Their records don't transfer. The team assesses them on how they present, not on their baseline, their exact medication timing, or what changed at their last visit. The family is there and they know, but they can't produce a clear, structured record under pressure."; "Care is fragmented and hard to carry. Patients forget most of what they're told in a visit, juggle medications with no clear picture of what each is for, and lose track of what changed and why. Then they end up somewhere new: a covering doctor, a specialist, an ER outside their network, and none of it travels with them. The family knows, but can't produce a clear, current record under pressure.";
const PROBLEM_BROADER = const PROBLEM_BROADER =
"This is not a rare edge case. It happens every day, to patients with Parkinson's, diabetes, heart disease, dementia, and a hundred other conditions that require precise, continuous management across a fragmented system. The failure is not that families don't care. The failure is that the patient's context does not travel with them."; "This is not a rare edge case. It happens every day, to patients with Parkinson's, diabetes, heart disease, dementia, and a hundred other conditions that require precise, continuous management across a fragmented system. The failure is not that families don't care. The failure is that the patient's context does not travel with them.";
const WHO_ITS_FOR =
"Anyone managing serious, ongoing care across a fragmented health system, and the families who coordinate it. It spans conditions and ages, and eventually touches almost everyone. The failure is simple: the patient's context does not travel with them.";
const DIFFERENTIATION = const DIFFERENTIATION =
"The market is full of AI visit recorders, medication trackers, health-record apps, and emergency QR cards, each owning one slice. None owns the whole job. In Medscribe, the daily tools are the maintenance system for one emergency-ready record."; "The market is full of AI visit recorders, medication trackers, health-record apps, and emergency QR cards, each owning one slice. None owns the whole job. In Medscribe, the daily tools are the maintenance system for one emergency-ready record.";
const differentiators = [ const differentiators = [
{ {
title: "The record is the product", title: "The record is the product",
body: "Recording, scanning, reminders, and Q&A keep one private, emergency-ready record accurate. Competitors ship visit summaries; we ship a record you can hand over in a crisis.", body: "Recording, scanning, reminders, and Q&A all feed one private record you actually keep current, useful every day and ready to hand over when it counts. Competitors ship visit summaries; we ship the record you own and carry.",
}, },
{ {
title: "Private by architecture", title: "Private by architecture",
@@ -50,6 +57,24 @@ const differentiators = [
}, },
]; ];
const MOAT =
"The privacy promise is easy to claim. The execution is not. Medscribe runs a fine-tuned medical model entirely on the phone, built and measured rather than bolted on.";
const moat = [
{
title: "A model tuned for this job",
body: "A compact, multi-task model handles label scanning, structured extraction, and safe medication Q&A on the device. Fine-tuning lifted prescription-label field extraction from 0% to 97% in evaluation.",
},
{
title: "Safety you can measure",
body: "Each release is scored against a held-out set for grounding and safe refusals. Unsafe-prompt refusals reached 100% after tuning, so “defers to a clinician” is a tested behavior, not a hope.",
},
{
title: "Context that compounds",
body: "Real visit transcripts, a real medication list, and bundled drug facts fuse into one local record, value that grows with use and is hard to replicate without the same on-device capture.",
},
];
const scenarios = [ const scenarios = [
{ {
title: "The ER that doesn't know you", title: "The ER that doesn't know you",
@@ -60,8 +85,8 @@ const scenarios = [
body: "An adult child manages a parent's medications, appointments, and history. When they're not there, the context disappears: a sibling visits, a home aide covers, a shift changes. Critical details live in one person's memory.", body: "An adult child manages a parent's medications, appointments, and history. When they're not there, the context disappears: a sibling visits, a home aide covers, a shift changes. Critical details live in one person's memory.",
}, },
{ {
title: "The new diabetic, overwhelmed", title: "The newly diagnosed, overwhelmed",
body: "A newly diagnosed patient leaves with insulin timing, diet changes, and follow-ups they half-remember. By the next visit, the provider can't tell what was understood, tried, or missed.", body: "After a new diagnosis, a patient leaves with a changed medication schedule, lifestyle instructions, and follow-ups they half-remember. By the next visit, the provider can't tell what was understood, tried, or missed.",
}, },
]; ];
@@ -80,6 +105,24 @@ const impact = [
}, },
]; ];
const FOR_HEALTH_SYSTEMS =
"Epic is the definitive record of care inside the network. What no institutional EHR is built to be is the record the patient owns and carries everywhere else: the out-of-network ER, the specialist on another Epic instance, the months between visits at home. That out-of-hospital stretch is a structural blind spot, not a flaw in any one system. Medscribe fills it on the patient's own device, and — only with the patient's consent — hands that context back, so the gap becomes an ongoing connection instead of a black box.";
const forHealthSystems = [
{
title: "Reaches where the EHR can't",
body: "The record lives on the patient's device and travels with them: the out-of-network ER, the covering specialist, the weeks at home between visits. It stays current exactly where an institutional system loses sight of the patient.",
},
{
title: "Feeds context back into Epic",
body: "With the patient's consent, the out-of-network and between-visit context Medscribe captures returns into the workflows you already run. It raises the value of the EHR investment rather than competing for budget or attention.",
},
{
title: "Patient-owned, so it persists",
body: "A branded, patient-facing surface that works the levers systems are measured on — in-network retention, medication adherence, avoidable readmissions. Because the record belongs to the patient, it holds even if the system changes EHRs.",
},
];
const printLightTokens = { const printLightTokens = {
"--color-foreground": "#0f172a", "--color-foreground": "#0f172a",
"--color-primary": "#0f766e", "--color-primary": "#0f766e",
@@ -90,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 ===================== */}
@@ -170,6 +218,36 @@ export default function ExecutiveSummaryPage() {
</Card> </Card>
))} ))}
</div> </div>
<Card className="border-primary-button/20 bg-primary-button text-white">
<CardContent className="pt-5">
<p className="leading-relaxed">
Medscribe connects the daily loop to the emergency handoff: one
private record that stays current through normal use and is ready
when the system does not know the patient.
</p>
</CardContent>
</Card>
</section>
{/* Under the hood */}
<section className="space-y-6">
<SectionLabel>Under the hood</SectionLabel>
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
Easy to claim. Hard to copy.
</h2>
<p className="max-w-4xl leading-relaxed text-muted-foreground">
{MOAT}
</p>
<div className="grid gap-4 md:grid-cols-3">
{moat.map((item) => (
<Card key={item.title} className="card-hover">
<CardHeader>
<CardTitle className="text-lg">{item.title}</CardTitle>
<CardDescription>{item.body}</CardDescription>
</CardHeader>
</Card>
))}
</div>
</section> </section>
{/* Impact */} {/* Impact */}
@@ -195,12 +273,31 @@ export default function ExecutiveSummaryPage() {
</div> </div>
</section> </section>
{/* For health systems */}
<section className="space-y-6">
<SectionLabel>For health systems</SectionLabel>
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
An ongoing touchstone, not another portal.
</h2>
<p className="max-w-4xl leading-relaxed text-muted-foreground">
{FOR_HEALTH_SYSTEMS}
</p>
<div className="grid gap-4 md:grid-cols-3">
{forHealthSystems.map((item) => (
<Card key={item.title} className="card-hover">
<CardHeader>
<CardTitle className="text-lg">{item.title}</CardTitle>
<CardDescription>{item.body}</CardDescription>
</CardHeader>
</Card>
))}
</div>
</section>
{/* Beta */} {/* Beta */}
<section className="flex flex-col gap-5 rounded-xl border border-border-soft bg-surface p-6 sm:flex-row sm:items-center sm:justify-between sm:p-8"> <section className="flex flex-col gap-5 rounded-xl border border-border-soft bg-surface p-6 sm:flex-row sm:items-center sm:justify-between sm:p-8">
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground"> <p className="max-w-xl text-sm leading-relaxed text-muted-foreground">
A beta is available on iOS through TestFlight. Everything runs {BETA_SUMMARY} If you&apos;d like to try it, reach out.
privately on the device, so no health data leaves the phone. If
you&apos;d like to try it, reach out.
</p> </p>
<PrintButton size="lg" className="shrink-0" /> <PrintButton size="lg" className="shrink-0" />
</section> </section>
@@ -213,29 +310,29 @@ export default function ExecutiveSummaryPage() {
{/* ===================== PRINT: dense one-page PDF ===================== */} {/* ===================== PRINT: dense one-page PDF ===================== */}
<article <article
style={printLightTokens} style={printLightTokens}
className="print-sheet hidden overflow-hidden print:flex print:h-[10.5in] print:flex-col print:justify-between print:px-[0.4in] print:py-[0.25in] print:text-[11pt] print:leading-[1.25]" className="print-sheet hidden print:flex print:h-[10.5in] print:flex-col print:px-[0.4in] print:pt-[0.22in] print:pb-[0.1in] print:text-[11pt] print:leading-[1.2]"
> >
{/* Header */} {/* Header */}
<header className="grid grid-cols-[minmax(0,1fr)_1.1in] items-stretch gap-3 border-b border-teal-900/10 pb-2"> <header className="grid grid-cols-[minmax(0,1fr)_0.94in] items-stretch gap-4 border-b border-teal-900/10 pb-1.5">
<div className="flex flex-col justify-between py-0.5"> <div className="flex flex-col justify-between py-0.5">
<p className="text-[11pt] font-semibold tracking-[0.15em] text-primary uppercase"> <p className="text-[10pt] font-semibold tracking-[0.18em] text-primary uppercase">
Executive Summary · June 2026 Executive Summary · June 2026
</p> </p>
<div className="mt-2 flex items-center gap-2.5"> <div className="mt-1.5 flex items-center gap-2.5">
<MedscribeLogo className="h-[20pt] w-auto text-foreground" /> <MedscribeLogo className="h-[20pt] w-auto text-foreground" />
<h1 className="font-serif text-[22pt] leading-none font-semibold tracking-tight text-foreground"> <h1 className="font-serif text-[22pt] leading-none font-semibold tracking-tight text-foreground">
Medscribe Medscribe
</h1> </h1>
</div> </div>
<p className="mt-2 font-serif text-[12pt] leading-snug font-medium tracking-tight text-foreground italic"> <p className="mt-1.5 font-serif text-[12pt] leading-snug font-medium tracking-tight text-foreground italic">
{TAGLINE} {TAGLINE}
</p> </p>
<p className="mt-3 text-[11pt] leading-[1.25] text-slate-700"> <p className="mt-2 text-[11pt] leading-[1.2] text-slate-700">
{SNAPSHOT} {SNAPSHOT}
</p> </p>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<div className="h-[2in] w-[0.94in] overflow-hidden rounded-[11px] border-[3px] border-slate-950 bg-white shadow-[0_8px_18px_rgba(15,23,42,0.14)]"> <div className="h-[1.78in] w-[0.88in] overflow-hidden rounded-[11px] border-[3px] border-slate-950 bg-white shadow-[0_8px_18px_rgba(15,23,42,0.14)]">
<Image <Image
src="/screenshots/home.png" src="/screenshots/home.png"
alt="Medscribe home screen" alt="Medscribe home screen"
@@ -250,47 +347,35 @@ export default function ExecutiveSummaryPage() {
</header> </header>
{/* The Problem */} {/* The Problem */}
<section className="border-b border-teal-900/10 py-1"> <section className="border-b border-teal-900/10 pt-1 pb-1">
<h2 className="font-serif text-[12pt] font-semibold tracking-tight text-foreground"> <h2 className="font-serif text-[12.5pt] font-semibold tracking-tight text-foreground">
The Problem The Problem
</h2> </h2>
<p className="mt-0.5 text-[11pt] leading-[1.25] text-slate-700"> <p className="mt-1 text-[11pt] leading-[1.2] text-slate-700">
{PROBLEM} {PROBLEM_BROADER} {PROBLEM}
</p> </p>
</section> </section>
{/* Who This Is For */} {/* Who It's For */}
<section className="border-b border-teal-900/10 py-1"> <section className="border-b border-teal-900/10 pt-1 pb-1">
<h2 className="font-serif text-[12pt] font-semibold tracking-tight text-foreground"> <h2 className="font-serif text-[12.5pt] font-semibold tracking-tight text-foreground">
Who This Is For Who It&apos;s For
</h2> </h2>
<div className="mt-0.5 grid grid-cols-3 gap-x-4"> <p className="mt-1 text-[11pt] leading-[1.2] text-slate-700">
{scenarios.map((scenario) => ( {WHO_ITS_FOR}
<div key={scenario.title}>
<p className="text-[11pt] font-semibold leading-[1.25] text-foreground">
{scenario.title}
</p> </p>
<p className="text-[11pt] leading-[1.25] text-slate-700">
{scenario.body}
</p>
</div>
))}
</div>
</section> </section>
{/* Why This Is Different */} {/* Why This Is Different */}
<section className="border-b border-teal-900/10 py-1"> <section className="border-b border-teal-900/10 pt-1 pb-1">
<h2 className="font-serif text-[12pt] font-semibold tracking-tight text-foreground"> <h2 className="font-serif text-[12.5pt] font-semibold tracking-tight text-foreground">
Why This Is Different Why This Is Different
</h2> </h2>
<p className="mt-0.5 text-[11pt] leading-[1.25] text-slate-700"> <div className="mt-1.5 grid grid-cols-3 gap-x-6">
{DIFFERENTIATION}
</p>
<div className="mt-1 grid grid-cols-3 gap-x-4">
{differentiators.map((item) => ( {differentiators.map((item) => (
<p <p
key={item.title} key={item.title}
className="text-[11pt] leading-[1.25] text-slate-700" className="text-[11pt] leading-[1.2] text-slate-700"
> >
<span className="font-semibold text-foreground"> <span className="font-semibold text-foreground">
{item.title}. {item.title}.
@@ -301,16 +386,16 @@ export default function ExecutiveSummaryPage() {
</div> </div>
</section> </section>
{/* Impact */} {/* Under The Hood */}
<section className="py-1"> <section className="border-b border-teal-900/10 pt-1 pb-1">
<h2 className="font-serif text-[12pt] font-semibold tracking-tight text-foreground"> <h2 className="font-serif text-[12.5pt] font-semibold tracking-tight text-foreground">
Impact Under The Hood
</h2> </h2>
<div className="mt-0.5 grid grid-cols-3 gap-x-4"> <div className="mt-1 grid grid-cols-3 gap-x-6">
{impact.map((item) => ( {moat.map((item) => (
<p <p
key={item.title} key={item.title}
className="text-[11pt] leading-[1.25] text-slate-700" className="text-[11pt] leading-[1.2] text-slate-700"
> >
<span className="font-semibold text-foreground"> <span className="font-semibold text-foreground">
{item.title}. {item.title}.
@@ -321,11 +406,29 @@ export default function ExecutiveSummaryPage() {
</div> </div>
</section> </section>
<footer className="flex items-center justify-between border-t border-teal-900/10 pt-1 text-[11pt] text-slate-500"> {/* For Health Systems */}
<p>Sean O&apos;Connor</p> <section className="pt-1">
<p className="text-slate-400"> <h2 className="font-serif text-[12.5pt] font-semibold tracking-tight text-foreground">
A beta is available on iOS through TestFlight. For Health Systems
</h2>
<div className="mt-1 grid grid-cols-3 gap-x-6">
{forHealthSystems.map((item) => (
<p
key={item.title}
className="text-[11pt] leading-[1.2] text-slate-700"
>
<span className="font-semibold text-foreground">
{item.title}.
</span>{" "}
{item.body}
</p> </p>
))}
</div>
</section>
<footer className="mt-auto flex items-center justify-between border-t border-teal-900/10 pt-1.5 text-[10pt] text-slate-500">
<p>Sean O&apos;Connor</p>
<p className="text-slate-400">{BETA_FOOTER_NOTE}</p>
</footer> </footer>
</article> </article>
</> </>
+268
View File
@@ -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&apos;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&apos;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>
</>
);
}
+167
View File
@@ -0,0 +1,167 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { SectionLabel } from "~/components/section-label";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
export const metadata: Metadata = {
title: "Solution Landscape",
description:
"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 =
"The space is crowded. Visit recorders, medication apps, health-record vaults, emergency IDs, and caregiver tools all exist and are improving. But each owns one slice. None owns the whole job: a private, patient-owned medical context that stays current through daily use and is ready to hand over in a crisis.";
const categories = [
{
category: "Patient AI visit notetakers",
examples: "Kin, AlignCare, Emilia, PatientScribe, Medcorder",
gap: "Capture and summarize the visit, but the summary is the product. It is not turned into an emergency-ready record a family can hand over.",
},
{
category: "Medication trackers",
examples: "Medisafe, MyTherapy, Dosecast",
gap: "Know the pill list and reminders, but never heard the visit. No grounded understanding of why a medication is on the list or what changed.",
},
{
category: "Personal health records",
examples: "KeepMD, Available Health, Replicoo, Guava, DrOwl",
gap: "Store and aggregate documents, but are passive. Nothing keeps them current, and a broad PHR is undifferentiated.",
},
{
category: "Emergency IDs and QR cards",
examples: "Apple Medical ID, MedicAlert, tenderID, Epic Share Everywhere",
gap: "A static snapshot with no daily maintenance loop, so the card is only as accurate as the last manual edit.",
},
{
category: "Caregiver coordination",
examples: "Innerhive, Alula, Connected Caregiver, Caring Village",
gap: "Tend toward tasks, calendars, and messaging. They coordinate people, not the integrity of the medical context itself.",
},
{
category: "Provider-side scribes",
examples: "Abridge, Nuance DAX, Suki",
gap: "Built for the clinician's chart and billing, in clinical language. Not for the patient or the family carrying context across systems.",
},
];
const bundle = [
"Patient- and caregiver-owned mobile app",
"On-device visit transcription",
"Medication OCR and intake tracking",
"Facts-first medication Q&A grounded in trusted drug data",
"Current medications and visit context fused into one local record",
"An emergency “know me” handoff",
"Caregiver sharing designed for complex-care families",
"End-to-end encrypted, peer-to-peer sharing as a core architecture",
];
export default function LandscapePage() {
// Unpublished for now — not part of what external contacts (e.g. the
// Northwell introduction) should see. Content is kept in place, not
// deleted, so this is a one-line revert when we want it public again.
redirect("/");
return (
<div className="mx-auto max-w-6xl space-y-14 px-4 py-10 sm:px-6 lg:space-y-20 lg:py-14">
{/* Hero */}
<section className="space-y-5">
<SectionLabel>Solution landscape</SectionLabel>
<h1 className="max-w-3xl font-serif text-4xl leading-[1.05] font-semibold tracking-tight text-foreground sm:text-5xl">
The field, and the gap
</h1>
<p className="max-w-3xl text-base leading-relaxed text-muted-foreground sm:text-lg">
{INTRO}
</p>
</section>
{/* Category map */}
<section className="space-y-6">
<div className="space-y-2">
<SectionLabel>Who owns which slice</SectionLabel>
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
Six categories, each solving part of it
</h2>
</div>
<div className="grid gap-4 md:grid-cols-2">
{categories.map((item) => (
<Card key={item.category} className="card-hover">
<CardHeader className="gap-1.5">
<CardTitle className="text-lg">{item.category}</CardTitle>
<p className="text-xs font-medium tracking-wide text-primary">
{item.examples}
</p>
<CardDescription className="pt-1">{item.gap}</CardDescription>
</CardHeader>
</Card>
))}
</div>
</section>
{/* Closest competitor */}
<section className="space-y-4">
<SectionLabel>The closest direct competitor</SectionLabel>
<Card className="border-primary/15 bg-primary-light/40">
<CardContent className="space-y-3 pt-5 text-sm leading-relaxed text-foreground">
<p>
AlignCare matches the consumer bundle on paper: visit recording,
summaries, an AI assistant, medication reminders and logging,
documents, and caregiver sharing. It is real competition, not a
strawman.
</p>
<p>
The openings it leaves: its app-store disclosures indicate cloud
collection and tracking rather than on-device processing; it has no
first-class emergency handoff for a patient who cannot explain
themselves; and its medication answers are not described as
deterministic, on-device, facts-first rendering. Those are
Medscribe&apos;s wedges.
</p>
</CardContent>
</Card>
</section>
{/* The unowned bundle */}
<section className="space-y-6">
<div className="space-y-2">
<SectionLabel>What no one owns as one bundle</SectionLabel>
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
The combined job is the opening
</h2>
<p className="max-w-3xl text-muted-foreground">
Each box above has partial substitutes. No single incumbent unites
all of this into one private, patient-owned record:
</p>
</div>
<Card>
<CardContent className="pt-5">
<ul className="grid gap-x-8 gap-y-3 sm:grid-cols-2">
{bundle.map((point) => (
<li
key={point}
className="flex items-start gap-3 text-sm leading-snug text-foreground"
>
<span className="mt-1.5 size-1.5 shrink-0 rounded-full bg-primary" />
<span>{point}</span>
</li>
))}
</ul>
</CardContent>
</Card>
<p className="max-w-3xl leading-relaxed text-muted-foreground">
That intersection, the daily maintenance loop that keeps an
emergency-ready record current and private by architecture, is the lane
Medscribe is built to own.
</p>
</section>
</div>
);
}
+7 -1
View File
@@ -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 />
</> </>
); );
+455 -278
View File
@@ -1,345 +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 { 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 themkept
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">On TestFlight</Badge>
<Badge variant="outline">iOS</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&apos;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&apos;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 viewnot 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>
</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 todaynot 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&apos;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. to remember medicine.
</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 </Button>
clinician or pharmacist.
</p>
</CardContent>
</Card>
</section>
{/* Screenshot strip */}
<section className="animate-fade-in-up space-y-6">
<SectionLabel>What travels with them</SectionLabel>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{[
{ src: "/screenshots/emergency-record.png", label: "Emergency record" },
{ src: "/screenshots/appointment-summary.png", label: "Visit summary" },
{ src: "/screenshots/medications.png", label: "Medication list" },
{ src: "/screenshots/chat.png", label: "Medication questions" },
].map((shot) => (
<Card key={shot.src} className="card-hover overflow-hidden">
<div className="flex min-h-[270px] items-start justify-center bg-surface-soft p-4">
<PhoneMockup src={shot.src} alt={shot.label} width={112} />
</div> </div>
<p className="border-t border-border-soft px-4 py-3 text-center text-sm font-medium text-muted-foreground">
{shot.label}
</p>
</Card>
))}
</div> </div>
</section> </section>
{/* Impact */} <section className="pitch-lazy flex min-h-[82svh] items-center px-4 py-24 sm:px-6 lg:py-32">
<section id="impact" className="animate-fade-in-up scroll-mt-24 space-y-6"> <div className="mx-auto w-full max-w-6xl overflow-hidden rounded-[3rem] border border-teal-900/10 bg-[linear-gradient(145deg,#ccfbf1_0%,#f0fdfa_44%,#ffffff_100%)] px-6 py-16 text-center shadow-[0_35px_110px_rgba(15,118,110,0.15)] sm:px-10 lg:px-20 lg:py-24 dark:border-teal-300/10 dark:bg-[linear-gradient(145deg,#134e4a_0%,#0f2930_48%,#111827_100%)]">
<div className="space-y-3"> <p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
<SectionLabel>Potential impact</SectionLabel> The right beta participant
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl"> </p>
Less panic, more context, better handoffs <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> </h2>
<p className="max-w-2xl text-muted-foreground"> <p className="text-muted-foreground mx-auto mt-8 max-w-2xl text-lg leading-relaxed sm:text-xl">
The goal is simple: make the person easier to understand at the exact This is an internal beta. Expect close feedback, manual review,
moment their care team has the least context. rough edges, and possible resetsnot guaranteed continuity.
</p> </p>
</div> <ul className="mx-auto mt-9 grid max-w-4xl gap-3 text-left md:grid-cols-3">
{betaFit.map((item) => (
<div className="grid gap-4 md:grid-cols-3"> <li
{impact.map((item) => ( key={item}
<Card key={item.title} className="card-hover"> className="border-primary/15 bg-surface/65 text-foreground flex items-start gap-3 rounded-2xl border p-4 text-sm leading-relaxed"
<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">Try Medscribe on iOS</CardTitle>
<CardDescription className="text-white/75">
The app is live on TestFlight. Reach out for an invite if
you&apos;re managing medications, visits, or emergency context for
someone you love.
</CardDescription>
</div>
<Button
asChild
size="lg"
variant="outline"
className="border-white/30 bg-white text-primary-button hover:bg-white/90"
> >
<a href="mailto:sean@soconnor.dev?subject=Medscribe%20TestFlight%20invite%20request&body=Hi%20Sean%2C%0D%0A%0D%0AI%27d%20like%20a%20TestFlight%20invite%20to%20try%20Medscribe.%20I%27m%20managing%20medications%2C%20visits%2C%20or%20emergency%20context%20for%20someone%20I%20care%20for.%0D%0A%0D%0AThanks%21"> <span className="bg-primary-light mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full">
Request an invite <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> </a>
</Button> </Button>
</CardHeader> <Button asChild size="lg" variant="outline">
</Card> <Link href="/beta">What to expect</Link>
</section> </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>
</>
); );
} }
+388
View File
@@ -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&apos;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>
</>
);
}
+33
View File
@@ -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 -6
View File
@@ -3,6 +3,8 @@ import "~/styles/globals.css";
import { type Metadata } from "next"; import { type Metadata } from "next";
import { Instrument_Sans, Playfair_Display } from "next/font/google"; import { Instrument_Sans, Playfair_Display } from "next/font/google";
import { UmamiAnalytics } from "~/components/umami-analytics";
const instrumentSans = Instrument_Sans({ const instrumentSans = Instrument_Sans({
subsets: ["latin"], subsets: ["latin"],
variable: "--font-instrument-sans", variable: "--font-instrument-sans",
@@ -15,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" },
@@ -27,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",
}, },
}; };
@@ -41,8 +46,14 @@ 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
<body className="min-h-screen">{children}</body> lang="en"
className={`${instrumentSans.variable} ${playfair.variable}`}
>
<body className="min-h-screen">
{children}
<UmamiAnalytics />
</body>
</html> </html>
); );
} }
+19
View File
@@ -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",
],
},
};
}
+308
View File
@@ -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&apos;s whole
medical picture.
</h1>
<p className="mt-5 text-[15px] leading-[1.55] text-slate-600">
Real medical AI on the patient&apos;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&apos;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&apos;s care.
</p>
</div>
</section>
</PrintPage>
</div>
);
}
+107 -87
View File
@@ -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 persons 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>
+381
View File
@@ -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&apos;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&apos;s device and under the patient&apos;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&amp;A run on the phone.
Caregiver sharing is end-to-end encrypted, so a server is not
asked to read the patient&apos;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&apos;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 &ldquo;AI in
healthcare,&rdquo; 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&apos;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&apos;s care. The product is live in beta on iOS and
Android today.
</p>
</div>
</section>
</PrintPage>
</div>
);
}
+11 -21
View File
@@ -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>
); );
} }
+39
View File
@@ -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,
)}
/>
);
}
+15 -2
View File
@@ -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>
); );
+55 -7
View File
@@ -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&apos;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&apos;Connor Sean O&apos;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">
&copy; {new Date().getFullYear()} Hadlock Holdings LLC
</p>
</div> </div>
</div> </div>
</footer> </footer>
+47 -106
View File
@@ -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>
); );
} }
+1 -1
View File
@@ -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: {
+25
View File
@@ -0,0 +1,25 @@
import Script from "next/script";
import { env } from "~/env";
const DEFAULT_UMAMI_SCRIPT_URL = "https://cloud.umami.is/script.js";
/**
* Privacy-friendly page analytics via Umami. Enabled when
* `NEXT_PUBLIC_UMAMI_WEBSITE_ID` is set in `.env`.
*/
export function UmamiAnalytics() {
const websiteId = env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
if (!websiteId) return null;
const src = env.NEXT_PUBLIC_UMAMI_SCRIPT_URL ?? DEFAULT_UMAMI_SCRIPT_URL;
return (
<Script
defer
src={src}
data-website-id={websiteId}
strategy="afterInteractive"
/>
);
}
+308
View File
@@ -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&apos;s whole
medical picture.
</h1>
<p className="mt-5 text-[15px] leading-[1.55] text-slate-600">
Real medical AI on the patient&apos;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&apos;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&apos;s care.
</p>
</div>
</section>
</PrintPage>
</div>
);
}
+71
View File
@@ -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;
+6 -2
View File
@@ -18,7 +18,10 @@ export const env = createEnv({
* `NEXT_PUBLIC_`. * `NEXT_PUBLIC_`.
*/ */
client: { client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(), /** Umami website ID — omit to disable analytics. */
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
/** Umami script URL (defaults to Umami Cloud). */
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.string().url().optional(),
}, },
/** /**
@@ -28,7 +31,8 @@ export const env = createEnv({
runtimeEnv: { runtimeEnv: {
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
SITE_PASSWORD: process.env.SITE_PASSWORD, SITE_PASSWORD: process.env.SITE_PASSWORD,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
}, },
/** /**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
+15
View File
@@ -0,0 +1,15 @@
/** Shared beta / early-access copy for the marketing site. */
export const BETA_SUMMARY =
"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 = "Request beta access";
export const BETA_CTA_BODY =
"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 =
"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 =
"Beta on iOS (TestFlight) and Android (Google Play internal testing).";
+156 -21
View File
@@ -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;
} }
} }