initial commit
This commit is contained in:
@@ -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 };
|
||||
+40
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user