initial commit
@@ -0,0 +1,14 @@
|
|||||||
|
# Since the ".env" file is gitignored, you can use the ".env.example" file to
|
||||||
|
# build a new ".env" file when you clone the repo. Keep this file up-to-date
|
||||||
|
# when you add new variables to `.env`.
|
||||||
|
|
||||||
|
# This file will be committed to version control, so make sure not to have any
|
||||||
|
# secrets in it. If you are cloning this repo, create a copy of this file named
|
||||||
|
# ".env" and populate it with your secrets.
|
||||||
|
|
||||||
|
# When adding additional environment variables, the schema in "/src/env.js"
|
||||||
|
# should be updated accordingly.
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# SERVERVAR="foo"
|
||||||
|
# NEXT_PUBLIC_CLIENTVAR="bar"
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# database
|
||||||
|
/prisma/db.sqlite
|
||||||
|
/prisma/db.sqlite-journal
|
||||||
|
db.sqlite
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# idea files
|
||||||
|
.idea
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Medscribe Web
|
||||||
|
|
||||||
|
Marketing homepage for [Medscribe](../README.md) — a privacy-first, on-device medical companion for iOS.
|
||||||
|
|
||||||
|
Built with [create-t3-app](https://create.t3.gg/) (Next.js + TypeScript + Tailwind). Single-page site; no database or API routes.
|
||||||
|
|
||||||
|
Uses **bun** exclusively (`bun.lock`).
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run build
|
||||||
|
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).
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
1. Run the app on **iPhone 17 Pro** (or 16 Pro) simulator or device.
|
||||||
|
2. Capture each screen (⌘S in Simulator saves to Desktop).
|
||||||
|
3. Drop PNGs into `public/screenshots/` and update paths in `src/app/page.tsx` / `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.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: import.meta.dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: [".next"],
|
||||||
|
},
|
||||||
|
...compat.extends("next/core-web-vitals"),
|
||||||
|
{
|
||||||
|
files: ["**/*.ts", "**/*.tsx"],
|
||||||
|
extends: [
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/array-type": "off",
|
||||||
|
"@typescript-eslint/consistent-type-definitions": "off",
|
||||||
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
"warn",
|
||||||
|
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{ argsIgnorePattern: "^_" },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/require-await": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": [
|
||||||
|
"error",
|
||||||
|
{ checksVoidReturn: { attributes: false } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: true,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
|
||||||
|
* for Docker builds.
|
||||||
|
*/
|
||||||
|
import "./src/env.js";
|
||||||
|
|
||||||
|
/** @type {import("next").NextConfig} */
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "medscribe-web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "next build",
|
||||||
|
"check": "eslint . && tsc --noEmit",
|
||||||
|
"dev": "next dev --turbo",
|
||||||
|
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
|
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"preview": "next build && next start",
|
||||||
|
"start": "next start",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "^1.3.0",
|
||||||
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"drizzle-kit": "^0.31.10",
|
||||||
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"lucide-react": "^1.21.0",
|
||||||
|
"next": "^16.2.9",
|
||||||
|
"react": "^19.2.7",
|
||||||
|
"react-device-mockup": "^1.0.0",
|
||||||
|
"react-dom": "^19.2.7",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@tailwindcss/postcss": "^4.0.15",
|
||||||
|
"@types/node": "^20.14.10",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"eslint": "^9.23.0",
|
||||||
|
"eslint-config-next": "^15.2.3",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"tailwindcss": "^4.0.15",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
|
"typescript-eslint": "^8.27.0"
|
||||||
|
},
|
||||||
|
"ct3aMetadata": {
|
||||||
|
"initVersion": "7.40.0"
|
||||||
|
},
|
||||||
|
"packageManager": "bun@1.3.14"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const postcssConfig = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default postcssConfig;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
|
||||||
|
const prettierConfig = {
|
||||||
|
plugins: ["prettier-plugin-tailwindcss"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default prettierConfig;
|
||||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Medscribe">
|
||||||
|
<rect width="64" height="64" rx="14" fill="#0f766e"/>
|
||||||
|
<path fill="#ffffff" d="M10 31h13v6H10z"/>
|
||||||
|
<path fill="#ffffff" d="M20 35l5-16 7 2-6 17z"/>
|
||||||
|
<path fill="#ffffff" d="M25 21l7-2 8 28-7 2z"/>
|
||||||
|
<path fill="#ffffff" d="M34 47l6-16 7 2-7 17z"/>
|
||||||
|
<path fill="#ffffff" d="M42 31h12v6H42z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
@@ -0,0 +1,30 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 162" fill="currentColor" role="img" aria-label="Medscribe">
|
||||||
|
<g transform="translate(0.000000,162.000000) scale(0.100000,-0.100000)">
|
||||||
|
<path d="M1005 1598 c-18 -16 -66 -122 -163 -358 l-138 -335 -328 -5 -328 -5
|
||||||
|
-24 -28 c-31 -36 -31 -78 0 -114 l24 -28 370 0 c370 0 370 0 394 22 14 14 62
|
||||||
|
115 118 253 68 165 97 226 102 213 7 -17 238 -1137 238 -1153 0 -19 55 -60 81
|
||||||
|
-60 56 0 73 29 215 375 75 182 141 336 146 343 7 8 45 12 122 12 132 0 256 19
|
||||||
|
349 53 37 13 67 21 67 17 0 -16 -71 -247 -125 -406 -34 -99 -56 -178 -52 -188
|
||||||
|
12 -32 158 -76 175 -52 4 6 19 38 32 71 162 413 559 899 721 883 53 -5 74 -44
|
||||||
|
73 -136 -1 -105 -21 -185 -121 -484 -48 -142 -83 -263 -80 -273 8 -20 57 -41
|
||||||
|
118 -50 l46 -7 60 124 c80 164 300 494 397 596 81 86 160 103 207 44 32 -41
|
||||||
|
22 -152 -27 -307 -36 -109 -39 -130 -39 -230 0 -102 2 -114 27 -156 33 -56 63
|
||||||
|
-77 134 -92 136 -28 299 54 420 214 l39 50 16 -55 c23 -75 59 -134 114 -185
|
||||||
|
57 -53 121 -81 208 -90 366 -39 585 238 512 650 -8 46 -42 169 -75 273 -33
|
||||||
|
104 -65 219 -71 254 -11 61 -7 170 7 234 5 21 1 30 -18 42 -33 22 -81 5 -107
|
||||||
|
-38 -23 -37 -51 -136 -51 -181 0 -42 -93 -207 -189 -335 -105 -142 -160 -198
|
||||||
|
-198 -206 -50 -10 -119 -83 -137 -145 -9 -29 -16 -63 -16 -77 0 -56 -161 -247
|
||||||
|
-235 -278 -51 -21 -121 -18 -145 6 -46 46 -40 127 29 354 60 198 71 285 47
|
||||||
|
368 -28 97 -95 137 -211 125 -133 -14 -266 -123 -406 -335 -49 -74 -89 -132
|
||||||
|
-89 -129 0 3 5 18 11 34 24 63 59 224 68 308 17 166 -17 282 -93 316 -23 10
|
||||||
|
-57 19 -77 19 -165 1 -401 -196 -627 -524 -34 -49 -62 -86 -62 -82 0 3 16 68
|
||||||
|
36 144 60 233 51 292 -48 292 -59 0 -117 -27 -149 -70 -62 -80 -86 -103 -131
|
||||||
|
-126 -81 -41 -169 -56 -365 -63 -178 -6 -183 -7 -207 -31 -14 -14 -64 -123
|
||||||
|
-117 -254 -68 -165 -95 -223 -101 -210 -4 11 -59 275 -123 588 -64 313 -120
|
||||||
|
577 -126 587 -23 43 -84 51 -124 17z m3892 -999 c5 -142 4 -164 -16 -229 -17
|
||||||
|
-55 -33 -84 -68 -121 -65 -70 -118 -93 -198 -86 -144 11 -245 126 -245 276 0
|
||||||
|
71 17 116 56 150 44 39 79 40 132 1 l42 -30 17 27 c10 16 18 46 18 71 0 36 -5
|
||||||
|
48 -31 70 -17 15 -39 32 -49 37 -16 10 -12 19 46 91 36 43 96 126 134 183 l69
|
||||||
|
103 43 -193 c39 -170 45 -214 50 -350z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 348 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 668 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 217 KiB |
@@ -0,0 +1,174 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { PrintButton } from "~/components/print-button";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Medscribe Executive Summary",
|
||||||
|
description:
|
||||||
|
"One-page executive summary for Medscribe, a private medical context app for families managing complex care.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const proofPoints = [
|
||||||
|
"iOS TestFlight build",
|
||||||
|
"Visit capture and local transcription",
|
||||||
|
"Medication OCR, reminders, and history",
|
||||||
|
"Search and grounded medication questions",
|
||||||
|
"Emergency-ready profile direction",
|
||||||
|
"Full founder ownership",
|
||||||
|
];
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
label: "Problem",
|
||||||
|
title: "Families hold the context care teams need most.",
|
||||||
|
body:
|
||||||
|
"In complex care, the real story often lives outside the chart: what normal looks like, which medication is time-critical, what changed after the last visit, and who can explain it under pressure. Portals store records, but families still perform the handoff from memory.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Product",
|
||||||
|
title: "A private context layer for the person, not the institution.",
|
||||||
|
body:
|
||||||
|
"Medscribe keeps visits, medications, baseline, allergies, contacts, reminders, summaries, and questions under the person they belong to. The goal is not another medical inbox. It is the family-held record that makes someone easier to understand when care gets complicated.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Wedge",
|
||||||
|
title: "Emergency-ready, caregiver-centered, private by architecture.",
|
||||||
|
body:
|
||||||
|
"The market has AI scribes, medication trackers, caregiver portals, and EHR-adjacent tools. Medscribe's wedge is the portable layer families control before an institution is ready: current context, source trails, time-critical medication visibility, and on-device AI where possible.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const opportunities = [
|
||||||
|
{
|
||||||
|
title: "B2C",
|
||||||
|
body: "A caregiver app for families managing aging parents, chronic illness, disability, or high-friction medication routines.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "IP / partnership",
|
||||||
|
body: "A private family-context layer that could matter to care navigation, home health, senior care, pharmacy, or patient-engagement platforms.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Impact",
|
||||||
|
body: "Fewer blank handoffs, safer medication moments, and more confident advocates when someone cannot fully speak for themselves.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ExecutiveSummaryPage() {
|
||||||
|
return (
|
||||||
|
<main className="mx-auto max-w-[8.5in] px-4 py-6 sm:px-6 print:p-0">
|
||||||
|
<div className="print-hidden mb-5 flex items-center justify-between gap-4">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-sm font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
|
||||||
|
>
|
||||||
|
Back to site
|
||||||
|
</Link>
|
||||||
|
<PrintButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article className="summary-sheet relative overflow-hidden rounded-lg border border-border-soft bg-surface px-8 py-7 shadow-[0_12px_40px_rgba(15,23,42,0.08)] print:h-[10.5in] print:rounded-none print:border-0 print:px-[0.34in] print:py-[0.3in] print:shadow-none">
|
||||||
|
<header className="relative z-10 grid gap-6 border-b border-teal-900/10 pb-5 lg:grid-cols-[minmax(0,1fr)_190px] print:grid-cols-[minmax(0,1fr)_1.45in] print:gap-4 print:pb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-semibold tracking-[0.2em] text-primary uppercase print:text-[8.5px]">
|
||||||
|
Executive Summary
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-2 font-serif text-5xl leading-none font-semibold tracking-tight text-foreground print:text-[28px]">
|
||||||
|
Medscribe
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 font-serif text-2xl leading-tight font-semibold tracking-tight text-foreground print:text-[16px]">
|
||||||
|
Be known when care gets complicated.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 max-w-2xl text-base leading-relaxed text-slate-700 print:mt-2 print:text-[10px] print:leading-[1.45]">
|
||||||
|
Medscribe is a private medical context app for families managing
|
||||||
|
complex care. It helps the people closest to a patient keep the
|
||||||
|
living record current: medications, baseline, visits, contacts,
|
||||||
|
allergies, emergency facts, and the source trail behind what changed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center lg:justify-end print:justify-end">
|
||||||
|
<div className="h-[360px] w-[166px] overflow-hidden rounded-[24px] border-[6px] border-slate-950 bg-white shadow-[0_18px_45px_rgba(15,23,42,0.18)] print:h-[2.9in] print:w-[1.34in] print:rounded-[18px] print:border-[4px] print:shadow-[0_10px_22px_rgba(15,23,42,0.16)]">
|
||||||
|
<Image
|
||||||
|
src="/screenshots/home.png"
|
||||||
|
alt="Medscribe home screen"
|
||||||
|
width={1206}
|
||||||
|
height={2622}
|
||||||
|
priority
|
||||||
|
unoptimized
|
||||||
|
className="h-full w-full object-cover object-top"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="relative z-10 grid gap-4 py-5 md:grid-cols-3 print:gap-2 print:py-3">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<div key={section.label} className="space-y-2 print:space-y-1">
|
||||||
|
<p className="text-[11px] font-semibold tracking-[0.16em] text-primary uppercase print:text-[8px]">
|
||||||
|
{section.label}
|
||||||
|
</p>
|
||||||
|
<h2 className="font-serif text-xl leading-tight font-semibold tracking-tight text-foreground print:text-[12px]">
|
||||||
|
{section.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm leading-relaxed text-slate-700 print:text-[8.8px] print:leading-[1.38]">
|
||||||
|
{section.body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="relative z-10 grid gap-4 border-t border-teal-900/10 py-5 md:grid-cols-[0.95fr_1.05fr] print:gap-3 print:py-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-semibold tracking-[0.16em] text-primary uppercase print:text-[8px]">
|
||||||
|
Current State
|
||||||
|
</p>
|
||||||
|
<h2 className="mt-2 font-serif text-2xl leading-tight font-semibold tracking-tight text-foreground print:mt-1 print:text-[14px]">
|
||||||
|
Built enough to show, focused enough to sharpen.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-slate-700 print:text-[8.8px] print:leading-[1.38]">
|
||||||
|
Medscribe is owned and built by Sean O'Connor. Bucknell
|
||||||
|
MedTech Entrepreneurial Fellows contributed market discovery and
|
||||||
|
pitch feedback; they are not founders, owners, or the technical
|
||||||
|
team. The near-term product spine is an emergency-ready profile
|
||||||
|
that can travel with the family across fragmented care settings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2 sm:grid-cols-2 print:grid-cols-2 print:gap-1.5">
|
||||||
|
{proofPoints.map((point) => (
|
||||||
|
<div
|
||||||
|
key={point}
|
||||||
|
className="rounded-md border border-teal-900/10 bg-white/78 px-3 py-2 text-sm font-medium text-slate-800 print:px-2 print:py-1.5 print:text-[8.4px]"
|
||||||
|
>
|
||||||
|
{point}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="relative z-10 grid gap-3 border-t border-teal-900/10 pt-5 md:grid-cols-3 print:gap-2 print:pt-3">
|
||||||
|
{opportunities.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.title}
|
||||||
|
className="rounded-lg bg-teal-950 px-4 py-3 text-white print:rounded-md print:px-2.5 print:py-2"
|
||||||
|
>
|
||||||
|
<h2 className="font-serif text-lg font-semibold tracking-tight print:text-[11px]">
|
||||||
|
{item.title}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1.5 text-sm leading-relaxed text-white/78 print:text-[8.4px] print:leading-[1.32]">
|
||||||
|
{item.body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer className="relative z-10 mt-5 flex flex-col gap-2 border-t border-teal-900/10 pt-4 text-xs text-slate-600 sm:flex-row sm:items-center sm:justify-between print:mt-3 print:pt-2 print:text-[8px]">
|
||||||
|
<p>Sean O'Connor / Founder, full owner</p>
|
||||||
|
<p>Private complex-care context / iOS TestFlight</p>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import "~/styles/globals.css";
|
||||||
|
|
||||||
|
import { type Metadata } from "next";
|
||||||
|
import { Instrument_Sans, Playfair_Display } from "next/font/google";
|
||||||
|
|
||||||
|
import { ScreenBackground } from "~/components/screen-background";
|
||||||
|
import { SiteFooter } from "~/components/site-footer";
|
||||||
|
import { SiteHeader } from "~/components/site-header";
|
||||||
|
|
||||||
|
const instrumentSans = Instrument_Sans({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-instrument-sans",
|
||||||
|
});
|
||||||
|
|
||||||
|
const playfair = Playfair_Display({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-playfair",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Medscribe — be known when care gets complicated",
|
||||||
|
description:
|
||||||
|
"A private, emergency-ready medical context app for families managing complex care. Keep visits, medications, baseline, allergies, and caregiver handoffs current on device.",
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: "/favicon.ico", sizes: "any" },
|
||||||
|
{ url: "/favicon.svg", type: "image/svg+xml" },
|
||||||
|
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||||
|
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||||
|
],
|
||||||
|
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: "Medscribe",
|
||||||
|
description:
|
||||||
|
"Be known when care gets complicated. Private medical context for families managing complex care.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className={`${instrumentSans.variable} ${playfair.variable}`}>
|
||||||
|
<body className="min-h-screen">
|
||||||
|
<ScreenBackground />
|
||||||
|
<SiteHeader />
|
||||||
|
<main>{children}</main>
|
||||||
|
<SiteFooter />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
ChevronRight,
|
||||||
|
ClipboardList,
|
||||||
|
HeartPulse,
|
||||||
|
Lock,
|
||||||
|
Mic,
|
||||||
|
Pill,
|
||||||
|
Shield,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { FeatureShowcase } from "~/components/feature-showcase";
|
||||||
|
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 {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "~/components/ui/card";
|
||||||
|
|
||||||
|
const quickActions = [
|
||||||
|
{
|
||||||
|
icon: HeartPulse,
|
||||||
|
title: "Know Me card",
|
||||||
|
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 = [
|
||||||
|
{
|
||||||
|
icon: ClipboardList,
|
||||||
|
title: "Keep the record current",
|
||||||
|
body: "Capture visits, scan labels, and keep conditions, medications, allergies, contacts, and caregiver notes in one portable record.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: AlertTriangle,
|
||||||
|
title: "Make the critical context obvious",
|
||||||
|
body: "The emergency view prioritizes the facts clinicians need fast: baseline, time-critical meds, active conditions, allergies, and who to call.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Shield,
|
||||||
|
title: "Stay private by architecture",
|
||||||
|
body: "Transcription, OCR, and medication chat run locally. No cloud AI processing. Sharing should be explicit, revocable, and encrypted.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const impact = [
|
||||||
|
{
|
||||||
|
title: "Fewer blank handoffs",
|
||||||
|
body: "When a caregiver is overwhelmed, the important facts are already assembled: conditions, medications, allergies, baseline, contacts, and recent changes.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Safer medication moments",
|
||||||
|
body: "Time-critical schedules and dose history stay visible, so a missed or delayed medication is easier to catch before it becomes a crisis.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "More confident families",
|
||||||
|
body: "Caregivers can see what changed, where it came from, and what still needs a clinician or pharmacist instead of guessing from memory.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const today = new Date().toLocaleDateString("en-US", {
|
||||||
|
weekday: "long",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
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">
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="grid items-center gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(230px,280px)] lg:gap-14">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="animate-fade-in-up space-y-4">
|
||||||
|
<SectionLabel>Private complex-care context</SectionLabel>
|
||||||
|
<h1 className="font-serif text-4xl leading-[1.05] font-semibold tracking-tight text-foreground sm:text-5xl lg:text-6xl">
|
||||||
|
Medscribe
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-xl font-serif text-3xl leading-tight font-semibold tracking-tight text-foreground sm:text-4xl">
|
||||||
|
Be known when care gets complicated
|
||||||
|
</p>
|
||||||
|
<p className="max-w-xl text-base leading-relaxed text-muted-foreground sm:text-lg">
|
||||||
|
Medscribe is a private medical context app for families managing
|
||||||
|
complex care. It keeps visits, medications, baseline, contacts,
|
||||||
|
allergies, and emergency facts ready before the chart catches up.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{today}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="animate-fade-in-up-delay-1 flex flex-wrap gap-3">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<a href="/executive-summary">
|
||||||
|
Read executive summary
|
||||||
|
<ChevronRight className="size-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline" size="lg">
|
||||||
|
<a href="#features">See how it works</a>
|
||||||
|
</Button>
|
||||||
|
</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 className="animate-fade-in-up-delay-3 flex justify-center lg:justify-end">
|
||||||
|
<div className="flex w-fit items-start justify-center">
|
||||||
|
<PhoneMockup
|
||||||
|
src="/screenshots/home.png"
|
||||||
|
alt="Medscribe home dashboard"
|
||||||
|
priority
|
||||||
|
width={190}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Strategic spine */}
|
||||||
|
<section className="animate-fade-in-up space-y-5">
|
||||||
|
<SectionLabel>The story</SectionLabel>
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:items-start">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
|
||||||
|
Be known before the handoff breaks
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-xl leading-relaxed text-muted-foreground">
|
||||||
|
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/20 bg-primary 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>
|
||||||
|
<CardTitle className={action.featured ? "text-white" : ""}>
|
||||||
|
{action.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription
|
||||||
|
className={action.featured ? "text-white/72" : ""}
|
||||||
|
>
|
||||||
|
{action.meta}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Interactive feature showcase */}
|
||||||
|
<section id="features" className="animate-fade-in-up scroll-mt-24 space-y-5">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<SectionLabel>The record</SectionLabel>
|
||||||
|
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
|
||||||
|
The pieces that keep the story current
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<FeatureShowcase />
|
||||||
|
</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>
|
||||||
|
<p className="max-w-2xl text-muted-foreground">
|
||||||
|
The crowded space already has visit recorders, medication apps, and
|
||||||
|
caregiver portals. Medscribe's wedge is the private, portable
|
||||||
|
context layer families control before an institution is ready.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
{pillars.map((pillar) => {
|
||||||
|
const Icon = pillar.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={pillar.title} className="card-hover">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex size-10 items-center justify-center rounded-[10px] bg-primary-light">
|
||||||
|
<Icon className="size-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle>{pillar.title}</CardTitle>
|
||||||
|
<CardDescription>{pillar.body}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="border-primary/15 bg-primary-light/40">
|
||||||
|
<CardContent className="flex items-start gap-4 pt-5">
|
||||||
|
<Lock className="mt-0.5 size-5 shrink-0 text-primary" />
|
||||||
|
<p className="text-sm leading-relaxed text-foreground">
|
||||||
|
All AI models run on your device. No health data is uploaded for
|
||||||
|
processing. The medication chatbot stays in drug-information
|
||||||
|
territory — it defers dosing and diagnosis questions to your
|
||||||
|
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>
|
||||||
|
<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>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Impact */}
|
||||||
|
<section id="impact" className="animate-fade-in-up scroll-mt-24 space-y-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SectionLabel>Potential impact</SectionLabel>
|
||||||
|
<h2 className="font-serif text-3xl font-semibold tracking-tight sm:text-4xl">
|
||||||
|
Less panic, more context, better handoffs
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-2xl text-muted-foreground">
|
||||||
|
The goal is simple: make the person easier to understand at the exact
|
||||||
|
moment their care team has the least context.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
{impact.map((item) => (
|
||||||
|
<Card key={item.title} className="card-hover">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{item.title}</CardTitle>
|
||||||
|
<CardDescription>{item.body}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<section className="animate-fade-in-up">
|
||||||
|
<Card className="overflow-hidden border-primary/20 bg-primary 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'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 hover:bg-white/90"
|
||||||
|
>
|
||||||
|
<a href="https://soconnor.dev">
|
||||||
|
Request an invite
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
CalendarDays,
|
||||||
|
Camera,
|
||||||
|
ContactRound,
|
||||||
|
Home,
|
||||||
|
MessageCircle,
|
||||||
|
Shield,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { PhoneMockup } from "~/components/phone-mockup";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
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.",
|
||||||
|
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.",
|
||||||
|
image: "/screenshots/emergency-record.png",
|
||||||
|
alt: "Medscribe emergency record with critical care context",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "visits",
|
||||||
|
label: "Visits",
|
||||||
|
icon: CalendarDays,
|
||||||
|
title: "Visit recordings that refresh the record",
|
||||||
|
description:
|
||||||
|
"A visit can become searchable context, so a medication change or follow-up instruction does not depend on memory alone.",
|
||||||
|
image: "/screenshots/visit-timeline.png",
|
||||||
|
alt: "Medscribe visit timeline with appointment cards",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "meds",
|
||||||
|
label: "Meds",
|
||||||
|
icon: Camera,
|
||||||
|
title: "Medication OCR, reminders, and history",
|
||||||
|
description:
|
||||||
|
"Prescription labels, reminders, refill alerts, and dose history help the family see what is supposed to happen and what actually happened.",
|
||||||
|
image: "/screenshots/medications.png",
|
||||||
|
alt: "Medscribe medications screen with active medications",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ask",
|
||||||
|
label: "Ask",
|
||||||
|
icon: MessageCircle,
|
||||||
|
title: "Medication questions grounded in the record",
|
||||||
|
description:
|
||||||
|
"Plain-language questions can draw from the person's record and bundled FDA facts, while keeping diagnosis and dosing with clinicians.",
|
||||||
|
image: "/screenshots/chat.png",
|
||||||
|
alt: "Medscribe Ask chat answering a question about blood pressure medications",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "privacy",
|
||||||
|
label: "Privacy",
|
||||||
|
icon: Shield,
|
||||||
|
title: "Private by architecture, not policy",
|
||||||
|
description:
|
||||||
|
"The sensitive work happens on device, so the family record is not another cloud inbox waiting to be mined.",
|
||||||
|
image: "/screenshots/settings.png",
|
||||||
|
alt: "Medscribe settings showing on-device AI models and privacy notice",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function FeatureShowcase() {
|
||||||
|
const [activeId, setActiveId] = useState<(typeof features)[number]["id"]>("home");
|
||||||
|
const active = features.find((f) => f.id === activeId) ?? features[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(170px,220px)] lg:gap-10">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{features.map((feature) => {
|
||||||
|
const Icon = feature.icon;
|
||||||
|
const isActive = feature.id === activeId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={feature.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveId(feature.id)}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-all",
|
||||||
|
isActive
|
||||||
|
? "border-primary bg-primary text-white shadow-sm"
|
||||||
|
: "border-border-soft bg-surface text-muted-foreground hover:border-primary/20 hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
{feature.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<h3 className="font-serif text-2xl font-semibold tracking-tight text-foreground">
|
||||||
|
{active.title}
|
||||||
|
</h3>
|
||||||
|
<p className="max-w-xl text-sm leading-relaxed text-muted-foreground sm:text-base">
|
||||||
|
{active.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full max-w-[220px] items-start justify-center rounded-lg bg-surface-soft/70 p-4 shadow-[0_12px_38px_rgba(15,23,42,0.08)] lg:-mt-16 lg:justify-self-end">
|
||||||
|
<PhoneMockup
|
||||||
|
src={active.image}
|
||||||
|
alt={active.alt}
|
||||||
|
width={146}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface MedscribeLogoProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MedscribeLogo({ className }: MedscribeLogoProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 512 162"
|
||||||
|
role="img"
|
||||||
|
aria-label="Medscribe"
|
||||||
|
className={cn("fill-current", className)}
|
||||||
|
>
|
||||||
|
<g transform="translate(0 162) scale(0.1 -0.1)">
|
||||||
|
<path d="M1005 1598 c-18 -16 -66 -122 -163 -358 l-138 -335 -328 -5 -328 -5 -24 -28 c-31 -36 -31 -78 0 -114 l24 -28 370 0 c370 0 370 0 394 22 14 14 62 115 118 253 68 165 97 226 102 213 7 -17 238 -1137 238 -1153 0 -19 55 -60 81 -60 56 0 73 29 215 375 75 182 141 336 146 343 7 8 45 12 122 12 132 0 256 19 349 53 37 13 67 21 67 17 0 -16 -71 -247 -125 -406 -34 -99 -56 -178 -52 -188 12 -32 158 -76 175 -52 4 6 19 38 32 71 162 413 559 899 721 883 53 -5 74 -44 73 -136 -1 -105 -21 -185 -121 -484 -48 -142 -83 -263 -80 -273 8 -20 57 -41 118 -50 l46 -7 60 124 c80 164 300 494 397 596 81 86 160 103 207 44 32 -41 22 -152 -27 -307 -36 -109 -39 -130 -39 -230 0 -102 2 -114 27 -156 33 -56 63 -77 134 -92 136 -28 299 54 420 214 l39 50 16 -55 c23 -75 59 -134 114 -185 57 -53 121 -81 208 -90 366 -39 585 238 512 650 -8 46 -42 169 -75 273 -33 104 -65 219 -71 254 -11 61 -7 170 7 234 5 21 1 30 -18 42 -33 22 -81 5 -107 -38 -23 -37 -51 -136 -51 -181 0 -42 -93 -207 -189 -335 -105 -142 -160 -198 -198 -206 -50 -10 -119 -83 -137 -145 -9 -29 -16 -63 -16 -77 0 -56 -161 -247 -235 -278 -51 -21 -121 -18 -145 6 -46 46 -40 127 29 354 60 198 71 285 47 368 -28 97 -95 137 -211 125 -133 -14 -266 -123 -406 -335 -49 -74 -89 -132 -89 -129 0 3 5 18 11 34 24 63 59 224 68 308 17 166 -17 282 -93 316 -23 10 -57 19 -77 19 -165 1 -401 -196 -627 -524 -34 -49 -62 -86 -62 -82 0 3 16 68 36 144 60 233 51 292 -48 292 -59 0 -117 -27 -149 -70 -62 -80 -86 -103 -131 -126 -81 -41 -169 -56 -365 -63 -178 -6 -183 -7 -207 -31 -14 -14 -64 -123 -117 -254 -68 -165 -95 -223 -101 -210 -4 11 -59 275 -123 588 -64 313 -120 577 -126 587 -23 43 -84 51 -124 17z m3892 -999 c5 -142 4 -164 -16 -229 -17 -55 -33 -84 -68 -121 -65 -70 -118 -93 -198 -86 -144 11 -245 126 -245 276 0 71 17 116 56 150 44 39 79 40 132 1 l42 -30 17 27 c10 16 18 46 18 71 0 36 -5 48 -31 70 -17 15 -39 32 -49 37 -16 10 -12 19 46 91 36 43 96 126 134 183 l69 103 43 -193 c39 -170 45 -214 50 -350z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { IPhoneMockup } from "react-device-mockup";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
interface PhoneMockupProps {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
className?: string;
|
||||||
|
priority?: boolean;
|
||||||
|
/** Rendered mockup width in px. @default 280 */
|
||||||
|
width?: number;
|
||||||
|
/** Use "simulator" only for captures that already include device chrome. */
|
||||||
|
captureMode?: "simulator" | "raw";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PhoneMockup({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
className,
|
||||||
|
priority,
|
||||||
|
width = 280,
|
||||||
|
captureMode = "raw",
|
||||||
|
}: PhoneMockupProps) {
|
||||||
|
const isSimulatorCapture = captureMode === "simulator";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("mx-auto w-fit drop-shadow-[0_18px_45px_rgba(15,23,42,0.14)]", className)}>
|
||||||
|
<IPhoneMockup
|
||||||
|
screenWidth={width}
|
||||||
|
screenType="island"
|
||||||
|
frameColor="#111827"
|
||||||
|
statusbarColor="#f8fafc"
|
||||||
|
hideStatusBar
|
||||||
|
hideNavBar
|
||||||
|
transparentNavBar
|
||||||
|
>
|
||||||
|
<div className="relative h-full w-full overflow-hidden bg-surface">
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
width={1206}
|
||||||
|
height={2622}
|
||||||
|
priority={priority}
|
||||||
|
unoptimized
|
||||||
|
className={cn(
|
||||||
|
"h-full w-full object-cover",
|
||||||
|
isSimulatorCapture
|
||||||
|
? "scale-[1.22] object-[50%_58%]"
|
||||||
|
: "object-top",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</IPhoneMockup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Printer } from "lucide-react";
|
||||||
|
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
|
||||||
|
export function PrintButton() {
|
||||||
|
return (
|
||||||
|
<Button type="button" onClick={() => window.print()} className="print-hidden">
|
||||||
|
<Printer className="size-4" />
|
||||||
|
Print PDF
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export function ScreenBackground() {
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed inset-0 -z-10 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-canvas"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(to right, rgba(15,23,42,0.055) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, rgba(15,23,42,0.055) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: "28px 28px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-x-0 top-0 h-[260px] bg-gradient-to-b from-primary-light/24 to-transparent" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<p className="text-xs font-semibold tracking-widest text-muted-foreground uppercase">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export function SiteFooter() {
|
||||||
|
return (
|
||||||
|
<footer className="print-hidden border-t border-border-soft bg-surface/70">
|
||||||
|
<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>
|
||||||
|
<p className="font-serif text-base font-semibold text-foreground">Medscribe</p>
|
||||||
|
<p className="mt-1 max-w-md leading-relaxed">
|
||||||
|
Be known when care gets complicated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Built by{" "}
|
||||||
|
<a
|
||||||
|
href="https://soconnor.dev"
|
||||||
|
className="text-foreground underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
Sean O'Connor
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { FileText } from "lucide-react";
|
||||||
|
|
||||||
|
import { MedscribeLogo } from "~/components/medscribe-logo";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{ href: "/#features", label: "Features" },
|
||||||
|
{ href: "/#privacy", label: "Privacy" },
|
||||||
|
{ href: "/#impact", label: "Impact" },
|
||||||
|
{ href: "/executive-summary", label: "Summary" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SiteHeader() {
|
||||||
|
return (
|
||||||
|
<header className="print-hidden sticky top-3 z-50 px-3 sm:px-6">
|
||||||
|
<div className="mx-auto flex h-14 max-w-5xl 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
|
||||||
|
href="/"
|
||||||
|
className="flex items-center text-foreground transition-opacity hover:opacity-80"
|
||||||
|
aria-label="Medscribe home"
|
||||||
|
>
|
||||||
|
<MedscribeLogo className="h-7 w-[88px]" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<nav className="hidden items-center gap-5 text-sm text-muted-foreground md:flex">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
className="transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Button asChild size="sm" className="h-8 rounded-full px-3">
|
||||||
|
<a href="/executive-summary">
|
||||||
|
<FileText className="size-4" />
|
||||||
|
<span className="hidden sm:inline">Executive summary</span>
|
||||||
|
<span className="sm:hidden">Summary</span>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary-light text-primary",
|
||||||
|
outline: "border border-border-soft bg-surface text-muted-foreground",
|
||||||
|
success: "bg-success-light text-success",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||||
|
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
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",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-white shadow-sm hover:bg-primary/90",
|
||||||
|
outline:
|
||||||
|
"border border-border-soft bg-surface text-foreground hover:bg-surface-soft",
|
||||||
|
secondary:
|
||||||
|
"bg-primary-light text-primary hover:bg-primary-light/80",
|
||||||
|
ghost: "text-muted-foreground hover:bg-surface-soft hover:text-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-11 px-5",
|
||||||
|
sm: "h-9 px-4 text-xs",
|
||||||
|
lg: "h-12 px-6 text-base",
|
||||||
|
icon: "size-11",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border border-border-soft bg-surface text-foreground shadow-[0_1px_4px_rgba(0,0,0,0.05)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div className={cn("flex flex-col gap-1.5 p-5", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
|
||||||
|
return (
|
||||||
|
<h3
|
||||||
|
className={cn("font-serif text-lg font-semibold leading-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<p className={cn("text-sm leading-relaxed text-muted-foreground", className)} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return <div className={cn("px-5 pb-5", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardTitle, CardDescription, CardContent };
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { createEnv } from "@t3-oss/env-nextjs";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const env = createEnv({
|
||||||
|
/**
|
||||||
|
* Specify your server-side environment variables schema here. This way you can ensure the app
|
||||||
|
* isn't built with invalid env vars.
|
||||||
|
*/
|
||||||
|
server: {
|
||||||
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify your client-side environment variables schema here. This way you can ensure the app
|
||||||
|
* isn't built with invalid env vars. To expose them to the client, prefix them with
|
||||||
|
* `NEXT_PUBLIC_`.
|
||||||
|
*/
|
||||||
|
client: {
|
||||||
|
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
|
||||||
|
* middlewares) or client-side so we need to destruct manually.
|
||||||
|
*/
|
||||||
|
runtimeEnv: {
|
||||||
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
* useful for Docker builds.
|
||||||
|
*/
|
||||||
|
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||||
|
/**
|
||||||
|
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
|
||||||
|
* `SOME_VAR=''` will throw an error.
|
||||||
|
*/
|
||||||
|
emptyStringAsUndefined: true,
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: var(--font-instrument-sans), ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-serif: var(--font-playfair), ui-serif, Georgia, serif;
|
||||||
|
|
||||||
|
--color-canvas: #f8fafc;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-soft: #f1f5f9;
|
||||||
|
--color-primary: #0f766e;
|
||||||
|
--color-primary-light: #ccfbf1;
|
||||||
|
--color-foreground: #0f172a;
|
||||||
|
--color-muted-foreground: #64748b;
|
||||||
|
--color-border-soft: #e2e8f0;
|
||||||
|
--color-success: #047857;
|
||||||
|
--color-success-light: #d1fae5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-canvas font-sans text-foreground antialiased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover {
|
||||||
|
@apply transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_8px_24px_rgba(15,23,42,0.08)];
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fade-in-up 0.5s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up-delay-1 {
|
||||||
|
animation: fade-in-up 0.5s ease-out 0.1s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up-delay-2 {
|
||||||
|
animation: fade-in-up 0.5s ease-out 0.2s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up-delay-3 {
|
||||||
|
animation: fade-in-up 0.5s ease-out 0.3s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-sheet {
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(240, 253, 250, 0.92), rgba(255, 255, 255, 0.96) 38%, rgba(236, 253, 245, 0.9)),
|
||||||
|
linear-gradient(rgba(15, 118, 110, 0.07) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(15, 118, 110, 0.06) 1px, transparent 1px);
|
||||||
|
background-size: auto, 22px 22px, 22px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
@page {
|
||||||
|
margin: 0.25in;
|
||||||
|
size: letter;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background: #ffffff !important;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #0f172a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-page {
|
||||||
|
max-width: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-page * {
|
||||||
|
box-shadow: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-sheet {
|
||||||
|
print-color-adjust: exact;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Base Options: */
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
/* Strictness */
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"checkJs": true,
|
||||||
|
/* Bundled projects */
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"ES2022"
|
||||||
|
],
|
||||||
|
"noEmit": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"incremental": true,
|
||||||
|
/* Path Aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.cjs",
|
||||||
|
"**/*.js",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"generated"
|
||||||
|
]
|
||||||
|
}
|
||||||