diff --git a/frontend/app/globals.css b/frontend/app/globals.css index a7fa2cd..d07f067 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -14,30 +14,25 @@ --paper: #FBFCF8; --mist: #EEF4FF; - --background: #FFFFFF; - --foreground: #08111F; - --card: #FFFFFF; - --card-foreground: #08111F; - --popover: #FFFFFF; - --popover-foreground: #08111F; + --background: #08111F; + --foreground: #FFFFFF; + --card: rgba(255, 255, 255, 0.03); + --card-foreground: #FFFFFF; + --popover: #08111F; + --popover-foreground: #FFFFFF; --primary-foreground: #FFFFFF; - --secondary: #EEF4FF; - --secondary-foreground: #08111F; - --muted: #F2F5FA; - --muted-foreground: #647087; - --accent: #DCEBFF; - --accent-foreground: #08111F; + --secondary: #172033; + --secondary-foreground: #FFFFFF; + --muted: #172033; + --muted-foreground: #94A3B8; + --accent: #245BFF; + --accent-foreground: #FFFFFF; --destructive: #D92D20; --destructive-foreground: #FFFFFF; - --border: #DCE4EF; - --input: #DCE4EF; + --border: rgba(255, 255, 255, 0.1); + --input: rgba(255, 255, 255, 0.1); --ring: #245BFF; - --chart-1: #245BFF; - --chart-2: #12B76A; - --chart-3: #F79009; - --chart-4: #7A5AF8; - --chart-5: #E31B54; - --radius: 0.65rem; + --radius: 1rem; --sidebar: #FBFCF8; --sidebar-foreground: #08111F; --sidebar-primary: #245BFF; @@ -122,29 +117,68 @@ h1, h2, - h3 { - font-family: var(--font-serif); - font-weight: 400; + h3, h4, h5, h6 { + font-family: var(--font-sans); + font-weight: 500; letter-spacing: -0.02em; text-wrap: balance; } h1 { - font-size: clamp(3.25rem, 9vw, 7rem); - line-height: 0.92; + font-size: clamp(3.5rem, 10vw, 8.5rem); + line-height: 0.88; + letter-spacing: -0.04em; } h2 { - font-size: clamp(2.1rem, 5vw, 4rem); - line-height: 1.02; + font-size: clamp(2.5rem, 6vw, 4.5rem); + line-height: 0.95; + letter-spacing: -0.03em; } h3 { - line-height: 1.18; + font-size: clamp(1.5rem, 3vw, 2.25rem); + line-height: 1.1; + letter-spacing: -0.02em; } p { - line-height: 1.7; + line-height: 1.6; + color: var(--color-muted-foreground); + } + + .text-gradient { + background: linear-gradient(to right, var(--color-primary), var(--color-primary-light)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + + .glass-card { + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.07); + } + + .mesh-gradient { + background-color: #FBFCF8; + background-image: + radial-gradient(at 0% 0%, hsla(225, 100%, 57%, 0.03) 0, transparent 50%), + radial-gradient(at 50% 0%, hsla(192, 100%, 77%, 0.05) 0, transparent 50%), + radial-gradient(at 100% 0%, hsla(225, 100%, 57%, 0.03) 0, transparent 50%); + } + + .noise-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + content: ""; + opacity: 0.02; + z-index: 9999; + pointer-events: none; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); } } @@ -189,6 +223,7 @@ .hero-title { animation-delay: 260ms; + font-family: var(--font-serif); } .hero-copy { @@ -365,20 +400,7 @@ .privacy-band, .pipeline-panel, .scenario-card { - border: 1px solid var(--border); - border-radius: 22px; - background: linear-gradient(145deg, #ffffff, #f8fbff); - box-shadow: 0 20px 50px rgba(8, 17, 31, 0.07); -} - -.surface-card { - transition: border-color 220ms ease, transform 220ms ease, box-shadow 220ms ease; -} - -.surface-card:hover { - border-color: rgba(36, 91, 255, 0.38); - transform: translateY(-3px); - box-shadow: 0 24px 60px rgba(36, 91, 255, 0.1); + /* Retained structural overrides if needed, but styling moved to Tailwind components */ } .scenario-grid { @@ -465,49 +487,49 @@ backdrop-filter: blur(18px); } -.section-reveal .stagger-grid > *, +.section-reveal .stagger-grid>*, .section-reveal .pipeline-step, .section-reveal .command-row { opacity: 0; transform: translateY(16px); } -.section-reveal.visible .stagger-grid > *, +.section-reveal.visible .stagger-grid>*, .section-reveal.visible .pipeline-step, .section-reveal.visible .command-row { animation: section-item-rise 540ms cubic-bezier(0.16, 1, 0.3, 1) forwards; } -.section-reveal.visible .stagger-grid > :nth-child(1), +.section-reveal.visible .stagger-grid> :nth-child(1), .section-reveal.visible .pipeline-step:nth-child(1), .section-reveal.visible .command-row:nth-child(1) { animation-delay: 80ms; } -.section-reveal.visible .stagger-grid > :nth-child(2), +.section-reveal.visible .stagger-grid> :nth-child(2), .section-reveal.visible .pipeline-step:nth-child(2), .section-reveal.visible .command-row:nth-child(2) { animation-delay: 150ms; } -.section-reveal.visible .stagger-grid > :nth-child(3), +.section-reveal.visible .stagger-grid> :nth-child(3), .section-reveal.visible .pipeline-step:nth-child(3), .section-reveal.visible .command-row:nth-child(3) { animation-delay: 220ms; } -.section-reveal.visible .stagger-grid > :nth-child(4), +.section-reveal.visible .stagger-grid> :nth-child(4), .section-reveal.visible .pipeline-step:nth-child(4), .section-reveal.visible .command-row:nth-child(4) { animation-delay: 290ms; } -.section-reveal.visible .stagger-grid > :nth-child(5), +.section-reveal.visible .stagger-grid> :nth-child(5), .section-reveal.visible .command-row:nth-child(5) { animation-delay: 360ms; } -.section-reveal.visible .stagger-grid > :nth-child(6), +.section-reveal.visible .stagger-grid> :nth-child(6), .section-reveal.visible .command-row:nth-child(6) { animation-delay: 430ms; } @@ -544,7 +566,7 @@ padding: 18px; } -.pipeline-step + .pipeline-step { +.pipeline-step+.pipeline-step { border-top: 1px solid var(--border); } @@ -621,9 +643,11 @@ 0% { box-shadow: 0 0 0 0 rgba(139, 232, 255, 0.32); } + 70% { box-shadow: 0 0 0 18px rgba(139, 232, 255, 0); } + 100% { box-shadow: 0 0 0 0 rgba(139, 232, 255, 0); } @@ -634,6 +658,7 @@ opacity: 0; transform: translateY(18px); } + to { opacity: 1; transform: translateY(0); @@ -645,6 +670,7 @@ opacity: 0; transform: translateX(26px) scale(0.985); } + to { opacity: 1; transform: translateX(0) scale(1); @@ -656,6 +682,7 @@ opacity: 0; transform: translateY(14px); } + to { opacity: 1; transform: translateY(0); @@ -667,6 +694,7 @@ opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); @@ -674,11 +702,13 @@ } @keyframes wave-breathe { + 0%, 100% { transform: scaleY(0.72); opacity: 0.62; } + 45% { transform: scaleY(1.08); opacity: 1; @@ -689,6 +719,7 @@ from { background-position: 0 0, 0 0; } + to { background-position: 52px 52px, 52px 52px; } @@ -698,6 +729,7 @@ from { transform: scaleY(0); } + to { transform: scaleY(1); } @@ -708,6 +740,7 @@ opacity: 0; transform: translateY(20px) rotateX(3deg); } + to { opacity: 1; transform: translateY(0) rotateX(0); @@ -718,12 +751,27 @@ from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } +} + +.animate-float { + animation: float 6s ease-in-out infinite; +} + +.delay-100 { animation-delay: 100ms; } +.delay-200 { animation-delay: 200ms; } +.delay-300 { animation-delay: 300ms; } + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { @@ -742,7 +790,7 @@ .command-card, .scenario-card, .section-reveal, - .section-reveal .stagger-grid > *, + .section-reveal .stagger-grid>*, .section-reveal .pipeline-step, .section-reveal .command-row { opacity: 1 !important; @@ -763,4 +811,4 @@ grid-template-columns: 1fr; gap: 8px; } -} +} \ No newline at end of file diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index e1154f0..934e5f9 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -3,13 +3,13 @@ import { Instrument_Serif, Inter } from 'next/font/google' import { Analytics } from '@vercel/analytics/next' import './globals.css' -const instrumentSerif = Instrument_Serif({ +const instrumentSerif = Instrument_Serif({ subsets: ["latin"], weight: "400", variable: "--font-serif" }); -const inter = Inter({ +const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 7c60cdf..7af8f76 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,4 +1,3 @@ -import type { CSSProperties } from "react" import { ArrowRight, Brain, @@ -21,16 +20,8 @@ import { } from "lucide-react" import { HeroSection } from "@/components/hero-section" import { FooterSection } from "@/components/footer-section" -import { SectionWrapper } from "@/components/section-wrapper" - -const actionExamples = [ - "Search the web for the latest Kotlin Android updates.", - "Create a GitHub issue titled login crash.", - "Draft an email to Maya about the invoice.", - "Clean this sentence and make it sound professional.", - "Save my email address as a snippet.", - "Send a Slack message saying I'll be five minutes late.", -] +import { Section, Label, Heading, Subtext } from "@/components/layout-system" +import { cn } from "@/lib/utils" const features = [ { @@ -62,312 +53,189 @@ const pipeline = [ { label: "Execute", body: "Composio or local tools complete the task." }, ] -const integrations = [ - { icon: Mail, name: "Gmail", command: "Draft an email to Alex about the project update." }, - { icon: CalendarDays, name: "Calendar", command: "What meetings do I have tomorrow?" }, - { icon: Github, name: "GitHub", command: "Create an issue for the login button bug." }, - { icon: MessageSquare, name: "Slack", command: "Tell the team I'm joining late." }, - { icon: Code2, name: "Notion", command: "Create a note from this idea." }, - { icon: Search, name: "Web Search", command: "Find the best local AI models for Android." }, -] - -const realLifeMoments = [ +const useCases = [ { - icon: TrainFront, label: "Crowded commute", title: "Review a PR while standing in a packed train.", - scene: "You do not have a seat, your laptop is closed, and the train is loud. Open the Android mic bubble, use an earbud mic, and say what you want reviewed.", + desc: "You do not have a seat, your laptop is closed, and the train is loud. Open the Android mic bubble, use an earbud mic, and say what you want reviewed.", command: "Review the auth PR, summarize the risky files, and draft one comment about token refresh.", outcome: "Invoke transcribes, classifies the GitHub action, drafts the review note, and asks before posting.", - proof: "Noise-aware capture + confirmation", }, { icon: Headphones, label: "Shared office", title: "Whisper a polished reply without disturbing anyone.", - scene: "You are in an open office or library and need to answer quickly. Speak quietly into a close mic and let Invoke clean the wording.", + desc: "You are in an open office or library and need to answer quickly. Speak quietly into a close mic and let Invoke clean the wording.", command: "Reply to Sarah that I can join tomorrow, make it warm and professional.", outcome: "Invoke drafts the message with your work style preset and keeps private mode local-first.", - proof: "Whisper-friendly dictation", }, { icon: Plane, label: "Between gates", title: "Turn a passing thought into an organized task before it disappears.", - scene: "You are walking through an airport with one hand free. Instead of opening Notion, Todoist, and Calendar, capture the whole workflow by voice.", + desc: "You are walking through an airport with one hand free. Instead of opening Notion, Todoist, and Calendar, capture the whole workflow by voice.", command: "Save this as a product idea, create a follow-up task for Friday, and search examples of Android voice bubbles.", outcome: "Invoke routes each part to the right connected tool and shows a clear action summary.", - proof: "Multi-step routing", }, ] +const integrations = [ + { icon: Mail, name: "Gmail" }, + { icon: CalendarDays, name: "Calendar" }, + { icon: Github, name: "GitHub" }, + { icon: MessageSquare, name: "Slack" }, + { icon: Code2, name: "Notion" }, + { icon: Search, name: "Web Search" }, +] + function ProblemSection() { return ( - -
-
- The Problem -

Typing, switching apps, and copying text slows everything down.

-
-
-

- Most productivity tools still make you open five windows, type a prompt, copy the result, and paste it somewhere else. -

-

- Dictation apps stop at text. Invoke goes further: it understands what you meant, picks the right route, and helps finish the action. -

-
+
+
+
+ + Typing and switching apps slows you down. + + Most productivity tools still make you open five windows, type a prompt, copy the result, and paste it somewhere else. Invoke understands what you meant and helps finish the action locally. +
- +
) } -function FeatureSection() { +function SolutionSection() { return ( - -
- What Invoke Does -

Use your voice like a command bar.

-

- Invoke understands natural speech and turns it into structured actions. Dictate anywhere, clean up rough thoughts, save reusable snippets, and trigger workflows across connected apps. -

+
+
+ + Voice as a command bar. + + Invoke turns natural speech into structured actions. Dictate anywhere, clean up rough thoughts, and trigger workflows across all your connected apps instantly. +
-
- {features.map((feature) => { - const Icon = feature.icon - return ( -
-
- -
-

{feature.title}

-

{feature.body}

-
- ) - })} -
- +
) } -function RealLifeSection() { +function FeaturesSection() { return ( - -
-
- Real Life -

Built for the moments where typing falls apart.

+
+
+
+ + Built for speed and absolute privacy.
-

- Invoke is not only for quiet desks. It is for commutes, shared spaces, airport gates, and rushed work moments where voice is the fastest input you still control. -

-
- -
- {realLifeMoments.map((moment, index) => { - const Icon = moment.icon - return ( -
-
-
-
- -
-
{moment.proof}
-
-
- {moment.label} -

{moment.title}

-

{moment.scene}

-
"{moment.command}"
-
- - {moment.outcome} -
+
+ {features.map((feature, idx) => ( +
+
+
-
- ) - })} -
- - ) -} - -function HowItWorksSection() { - return ( - -
-
- How It Works -

Speak naturally. Invoke routes your words into actions.

-

- The core architecture is simple: voice input, speech-to-text, local intent classification, tool execution, then a result or completed action. -

-
-
- {pipeline.map((step, index) => ( -
-
{String(index + 1).padStart(2, "0")}
-
-

{step.label}

-

{step.body}

+
+

{feature.title}

+

{feature.body}

))}
- +
) } -function LocalAiSection() { +function UseCaseSection() { return ( - -
-
- Local AI -

Powered by Qwen 3 0.6B through Ollama.

-

- Invoke does not use Qwen as a big chatbot. It uses the model for one focused job: understanding what the user wants and converting speech into structured actions. -

-
- {["Small enough to run locally", "Fast for intent classification", "No cloud LLM required for routing", "Upgradeable for larger models"].map((item) => ( -
- - {item} -
- ))} -
+
+
+
+ + Everywhere typing falls apart.
-
-
- Intent payload - local -
-
{`User:
-"Search the web for OpenAI official website"
-
-Qwen 3 0.6B:
-{
-  tool: "COMPOSIO_SEARCH_WEB",
-  parameters: {
-    query: "OpenAI official website"
-  }
-}
-
-Invoke:
-execute(action)`}
+
+ {useCases.map((moment) => ( +
+
+ {moment.label} +

{moment.title}

+

{moment.desc}

+ + See how it works + +
+ +
+
+
+
+
+

Command

+
+

"{moment.command}"

+
+ +

{moment.outcome}

+
+
+
+
+ ))}
- +
) } function IntegrationsSection() { return ( - -
-
- Integrations -

One voice layer for the tools you already use.

+
+
+
+ + One layer for all your tools.
-

- Composio lets Invoke connect spoken intent to real app actions, from developer workflows to everyday messages. -

-
-
- {integrations.map((integration) => { - const Icon = integration.icon - return ( -
-
-
- -
-

{integration.name}

+
+ {integrations.map((item) => ( +
+
+
-

"{integration.command}"

-
- ) - })} -
- - ) -} - -function PlatformsSection() { - return ( - -
- Desktop + Android -

One assistant across computer and phone.

-
-
-
- -

Windows desktop app

-

Tauri desktop app with global shortcuts, local Whisper transcription, Ollama settings, Qwen 3 0.6B model support, and Composio actions.

-
-
- -

Android voice bubble

-

Kotlin Android app concept with floating mic access, permissions onboarding, privacy mode, dictionary, styles, snippets, and advanced setup when needed.

-
-
-
- ) -} - -function PrivacySection() { - return ( - -
-
- - Privacy -

Private by design.

+ {item.name} +
+ ))}
-

- Invoke supports local AI through Ollama, so core intent classification can run on your own machine. Privacy mode keeps data stored on your device. Cloud sync and app integrations are optional and controlled by the user. -

- + ) } -function ExamplesSection() { +function CallToAction() { return ( - -
- Examples -

Real commands, real outcomes.

-
-
- {actionExamples.map((command, index) => ( -
- {String(index + 1).padStart(2, "0")} -

"{command}"

- -
- ))} +
+
+ Private by design. + + Invoke supports local AI through Ollama, so core intent classification can run on your own machine. + +
- +
) } export default function Home() { return ( -
+
- - - - + + + - - - +
) diff --git a/frontend/components/card.tsx b/frontend/components/card.tsx new file mode 100644 index 0000000..d68bddf --- /dev/null +++ b/frontend/components/card.tsx @@ -0,0 +1,28 @@ +import { ReactNode } from "react" +import { cn } from "@/lib/utils" + +interface CardProps extends React.HTMLAttributes { + children: ReactNode + variant?: "primary" | "secondary" | "dark" +} + +export function Card({ children, className, variant = "primary", ...props }: CardProps) { + return ( +
+ {children} +
+ ) +} diff --git a/frontend/components/feature-grid.tsx b/frontend/components/feature-grid.tsx new file mode 100644 index 0000000..a68723e --- /dev/null +++ b/frontend/components/feature-grid.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from "react" +import { cn } from "@/lib/utils" + +interface FeatureGridProps extends React.HTMLAttributes { + children: ReactNode + columns?: 2 | 3 | 4 +} + +export function FeatureGrid({ children, className, columns = 2, ...props }: FeatureGridProps) { + return ( +
+ {children} +
+ ) +} diff --git a/frontend/components/hero-section.tsx b/frontend/components/hero-section.tsx index 5782df7..594ebfb 100644 --- a/frontend/components/hero-section.tsx +++ b/frontend/components/hero-section.tsx @@ -1,7 +1,7 @@ "use client" import type { CSSProperties } from "react" -import { ArrowDown, ArrowRight, CheckCircle2, Github, Mic2, Sparkles, Workflow } from "lucide-react" +import { ArrowRight, Github, Mic2, Sparkles, CheckCircle2 } from "lucide-react" const navItems = [ { label: "Problem", href: "#problem" }, @@ -24,137 +24,111 @@ const commandRail = [ tool: "GITHUB_CREATE_ISSUE", status: "queued", }, - { - speech: "Search the web for Android privacy changes.", - tool: "COMPOSIO_SEARCH_WEB", - status: "done", - }, ] export function HeroSection() { return ( -
- {/* HERO MOTION STORYBOARD - * 0ms static shell visible, no blocked CTA - * 80ms brand/nav settles down - * 160ms voice-action badge fades up - * 260ms headline rises in - * 420ms body copy and CTAs follow - * 520ms console slides in from right - * 700ms command cards stagger inside the console - */} -
+
+
- +
- +
Invoke
-
-
-
-
- +
+
+
+ Voice actions for every app
-

- Speak once. -
- Invoke handles the rest. +

+ Speak once.
+ Invoke
+ handles the rest.

-

- Turn speech into messages, notes, snippets, searches, and app actions. Use local AI through Ollama, connect tools through Composio, and keep control of your data with privacy-first setup. +

+ Turn speech into messages, notes, snippets, searches, and app actions. Use local AI to bridge the gap between intent and execution.

- -
-
-
- - - -
- local endpoint active -
- -
-
-
-

Listening

-

Natural speech to structured actions

-
-
- +
+
+
+
+
+
+
+
+
+ local endpoint active
-
- -
-
- {commandRail.map((item, index) => ( -
-

"{item.speech}"

-
- {item.tool} - - - {item.status} - +
+
+
+
+

Listening

+

Natural speech to structured actions

+
+
+ +
+
+ -
- ))} -
-
- - Voice input > Whisper > Qwen 3 0.6B > Composio action +
+ {commandRail.map((item, i) => ( +
+

"{item.speech}"

+
+ {item.tool} +
+ + {item.status} +
+
+
+ ))} +
+
+
- - - -
) } diff --git a/frontend/components/layout-system.tsx b/frontend/components/layout-system.tsx new file mode 100644 index 0000000..cc4a240 --- /dev/null +++ b/frontend/components/layout-system.tsx @@ -0,0 +1,58 @@ +import { ReactNode } from "react" +import { cn } from "@/lib/utils" + +export function Container({ children, className }: { children: ReactNode; className?: string }) { + return ( +
+ {children} +
+ ) +} + +interface SectionProps { + children: ReactNode + id?: string + className?: string + variant?: "default" | "muted" +} + +export function Section({ children, id, className, variant = "default" }: SectionProps) { + return ( +
+ + {children} + +
+ ) +} + +export function Label({ children, className }: { children: ReactNode; className?: string }) { + return ( + + {children} + + ) +} + +export function Heading({ children, className }: { children: ReactNode; className?: string }) { + return ( +

+ {children} +

+ ) +} + +export function Subtext({ children, className }: { children: ReactNode; className?: string }) { + return ( +

+ {children} +

+ ) +} diff --git a/frontend/components/section-wrapper.tsx b/frontend/components/section-wrapper.tsx index 91a46b7..5bf315f 100644 --- a/frontend/components/section-wrapper.tsx +++ b/frontend/components/section-wrapper.tsx @@ -10,11 +10,11 @@ interface SectionWrapperProps { variant?: "default" | "alternate" | "royal" } -export function SectionWrapper({ - children, - className, +export function SectionWrapper({ + children, + className, id, - variant = "default" + variant = "default" }: SectionWrapperProps) { const sectionRef = useRef(null) diff --git a/frontend/components/section.tsx b/frontend/components/section.tsx new file mode 100644 index 0000000..b0d0349 --- /dev/null +++ b/frontend/components/section.tsx @@ -0,0 +1,64 @@ +"use client" + +import { useEffect, useRef, ReactNode } from "react" +import { cn } from "@/lib/utils" + +interface SectionProps extends React.HTMLAttributes { + children: ReactNode + variant?: "default" | "alternate" | "royal" | "transparent" + containerClass?: string +} + +export function Section({ + children, + className, + id, + variant = "default", + containerClass, + ...props +}: SectionProps) { + const sectionRef = useRef(null) + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add("visible") + } + }) + }, + { threshold: 0.1 } + ) + + if (sectionRef.current) { + observer.observe(sectionRef.current) + } + + return () => observer.disconnect() + }, []) + + const variantStyles = { + default: "bg-background", + alternate: "bg-muted/40", + royal: "bg-royal-blue text-white", + transparent: "bg-transparent", + } + + return ( +
+
+ {children} +
+
+ ) +} diff --git a/frontend/public/app-mockup.png b/frontend/public/app-mockup.png new file mode 100644 index 0000000..f38f417 Binary files /dev/null and b/frontend/public/app-mockup.png differ