More changes that I would want my example to have I think

This commit is contained in:
2025-08-28 16:04:14 -05:00
parent 44d2ba3c5e
commit b5a726e359
26 changed files with 963 additions and 57 deletions

View File

@@ -1,26 +0,0 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -1,8 +1,11 @@
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
import '@/styles/globals.css';
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
import ConvexClientProvider from '@/components/ConvexClientProvider';
import { ConvexClientProvider, ThemeProvider } from '@/components/providers';
import PlausibleProvider from 'next-plausible';
import { generateMetadata } from '@/lib/metadata';
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
@@ -13,13 +16,7 @@ const geistMono = Geist_Mono({
subsets: ['latin'],
});
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
icons: {
icon: '/convex.svg',
},
};
export const metadata: Metadata = generateMetadata();
export default function RootLayout({
children,
@@ -28,13 +25,26 @@ export default function RootLayout({
}>) {
return (
<ConvexAuthNextjsServerProvider>
<PlausibleProvider
domain='techtracker.gbrown.org'
customDomain='https://plausible.gbrown.org'
>
<html lang='en'>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ConvexClientProvider>{children}</ConvexClientProvider>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<ConvexClientProvider>{children}</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
</PlausibleProvider>
</ConvexAuthNextjsServerProvider>
);
}

View File

@@ -1,6 +1,6 @@
'use client';
import { Preloaded, useMutation, usePreloadedQuery } from 'convex/react';
import { type Preloaded, useMutation, usePreloadedQuery } from 'convex/react';
import { api } from '~/convex/_generated/api';
export default function Home({

View File

@@ -2,15 +2,15 @@
import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs';
import { ConvexReactClient } from 'convex/react';
import { ReactNode } from 'react';
import { type ReactNode } from 'react';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export default function ConvexClientProvider({
export const ConvexClientProvider = ({
children,
}: {
children: ReactNode;
}) {
}) => {
return (
<ConvexAuthNextjsProvider client={convex}>
{children}

View File

@@ -0,0 +1,69 @@
'use client';
import { useEffect, useState, type ComponentProps } from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui';
import { cn } from '@/lib/utils';
const ThemeProvider = ({
children,
...props
}: ComponentProps<typeof NextThemesProvider>) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
};
type ThemeToggleProps = {
size?: number;
buttonProps?: Omit<ComponentProps<typeof Button>, 'onClick'>;
};
const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => {
const { setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<Button {...buttonProps}>
<span style={{ height: `${size}rem`, width: `${size}rem` }} />
</Button>
);
}
const toggleTheme = () => {
if (resolvedTheme === 'dark') setTheme('light');
else setTheme('dark');
};
return (
<Button
variant='outline'
size='icon'
{...buttonProps}
onClick={toggleTheme}
className={cn('cursor-pointer', buttonProps?.className)}
>
<Sun
style={{ height: `${size}rem`, width: `${size}rem` }}
className='rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0'
/>
<Moon
style={{ height: `${size}rem`, width: `${size}rem` }}
className='absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100'
/>
</Button>
);
};
export { ThemeProvider, ThemeToggle, type ThemeToggleProps };

View File

@@ -0,0 +1,2 @@
export { ConvexClientProvider } from './ConvexClientProvider';
export { ThemeProvider, ThemeToggle, type ThemeToggleProps } from './ThemeProvider';

View File

@@ -0,0 +1,59 @@
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 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1 @@
export { Button, buttonVariants } from './button';

369
src/lib/metadata.ts Normal file
View File

@@ -0,0 +1,369 @@
import type { Metadata } from 'next';
import * as Sentry from '@sentry/nextjs';
export const generateMetadata = (): Metadata => {
return {
title: {
template: '%s | Tech Tracker',
default: 'Tech Tracker',
},
description:
'App used by COG IT employees to \
update their status throughout the day.',
applicationName: 'Tech Tracker',
keywords:
'Tech Tracker, City of Gulfport, Information Technology, T3 Template, ' +
'Next.js, Supabase, Tailwind, TypeScript, React, T3, Gib',
authors: [{ name: 'Gib', url: 'https://gbrown.org' }],
creator: 'Gib Brown',
publisher: 'Gib Brown',
formatDetection: {
email: false,
address: false,
telephone: false,
},
robots: {
index: true,
follow: true,
nocache: false,
googleBot: {
index: true,
follow: true,
noimageindex: false,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
icons: {
icon: [
{ url: '/favicon.ico', type: 'image/x-icon', sizes: 'any' },
{
url: '/favicon-16.png',
type: 'image/png',
sizes: '16x16',
},
{
url: '/favicon-32.png',
type: 'image/png',
sizes: '32x32',
},
{ url: '/favicon.png', type: 'image/png', sizes: '96x96' },
{
url: '/favicon.ico',
type: 'image/x-icon',
sizes: 'any',
media: '(prefers-color-scheme: dark)',
},
{
url: '/favicon-16.png',
type: 'image/png',
sizes: '16x16',
media: '(prefers-color-scheme: dark)',
},
{
url: '/favicon-32.png',
type: 'image/png',
sizes: '32x32',
media: '(prefers-color-scheme: dark)',
},
{
url: '/favicon.png',
type: 'image/png',
sizes: '96x96',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon-36.png',
type: 'image/png',
sizes: '36x36',
},
{
url: '/appicon/icon-48.png',
type: 'image/png',
sizes: '48x48',
},
{
url: '/appicon/icon-72.png',
type: 'image/png',
sizes: '72x72',
},
{
url: '/appicon/icon-96.png',
type: 'image/png',
sizes: '96x96',
},
{
url: '/appicon/icon-144.png',
type: 'image/png',
sizes: '144x144',
},
{
url: '/appicon/icon.png',
type: 'image/png',
sizes: '192x192',
},
{
url: '/appicon/icon-36.png',
type: 'image/png',
sizes: '36x36',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon-48.png',
type: 'image/png',
sizes: '48x48',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon-72.png',
type: 'image/png',
sizes: '72x72',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon-96.png',
type: 'image/png',
sizes: '96x96',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon-144.png',
type: 'image/png',
sizes: '144x144',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon.png',
type: 'image/png',
sizes: '192x192',
media: '(prefers-color-scheme: dark)',
},
],
shortcut: [
{
url: '/appicon/icon-36.png',
type: 'image/png',
sizes: '36x36',
},
{
url: '/appicon/icon-48.png',
type: 'image/png',
sizes: '48x48',
},
{
url: '/appicon/icon-72.png',
type: 'image/png',
sizes: '72x72',
},
{
url: '/appicon/icon-96.png',
type: 'image/png',
sizes: '96x96',
},
{
url: '/appicon/icon-144.png',
type: 'image/png',
sizes: '144x144',
},
{
url: '/appicon/icon.png',
type: 'image/png',
sizes: '192x192',
},
{
url: '/appicon/icon-36.png',
type: 'image/png',
sizes: '36x36',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon-48.png',
type: 'image/png',
sizes: '48x48',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon-72.png',
type: 'image/png',
sizes: '72x72',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon-96.png',
type: 'image/png',
sizes: '96x96',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon-144.png',
type: 'image/png',
sizes: '144x144',
media: '(prefers-color-scheme: dark)',
},
{
url: '/appicon/icon.png',
type: 'image/png',
sizes: '192x192',
media: '(prefers-color-scheme: dark)',
},
],
apple: [
{
url: 'appicon/icon-57.png',
type: 'image/png',
sizes: '57x57',
},
{
url: 'appicon/icon-60.png',
type: 'image/png',
sizes: '60x60',
},
{
url: 'appicon/icon-72.png',
type: 'image/png',
sizes: '72x72',
},
{
url: 'appicon/icon-76.png',
type: 'image/png',
sizes: '76x76',
},
{
url: 'appicon/icon-114.png',
type: 'image/png',
sizes: '114x114',
},
{
url: 'appicon/icon-120.png',
type: 'image/png',
sizes: '120x120',
},
{
url: 'appicon/icon-144.png',
type: 'image/png',
sizes: '144x144',
},
{
url: 'appicon/icon-152.png',
type: 'image/png',
sizes: '152x152',
},
{
url: 'appicon/icon-180.png',
type: 'image/png',
sizes: '180x180',
},
{
url: 'appicon/icon.png',
type: 'image/png',
sizes: '192x192',
},
{
url: 'appicon/icon-57.png',
type: 'image/png',
sizes: '57x57',
media: '(prefers-color-scheme: dark)',
},
{
url: 'appicon/icon-60.png',
type: 'image/png',
sizes: '60x60',
media: '(prefers-color-scheme: dark)',
},
{
url: 'appicon/icon-72.png',
type: 'image/png',
sizes: '72x72',
media: '(prefers-color-scheme: dark)',
},
{
url: 'appicon/icon-76.png',
type: 'image/png',
sizes: '76x76',
media: '(prefers-color-scheme: dark)',
},
{
url: 'appicon/icon-114.png',
type: 'image/png',
sizes: '114x114',
media: '(prefers-color-scheme: dark)',
},
{
url: 'appicon/icon-120.png',
type: 'image/png',
sizes: '120x120',
media: '(prefers-color-scheme: dark)',
},
{
url: 'appicon/icon-144.png',
type: 'image/png',
sizes: '144x144',
media: '(prefers-color-scheme: dark)',
},
{
url: 'appicon/icon-152.png',
type: 'image/png',
sizes: '152x152',
media: '(prefers-color-scheme: dark)',
},
{
url: 'appicon/icon-180.png',
type: 'image/png',
sizes: '180x180',
media: '(prefers-color-scheme: dark)',
},
{
url: 'appicon/icon.png',
type: 'image/png',
sizes: '192x192',
media: '(prefers-color-scheme: dark)',
},
],
other: [
{
rel: 'apple-touch-icon-precomposed',
url: '/appicon/icon-precomposed.png',
type: 'image/png',
sizes: '180x180',
},
],
},
other: {
...Sentry.getTraceData(),
},
appleWebApp: {
title: 'Tech Tracker',
statusBarStyle: 'black-translucent',
startupImage: [
'/icons/apple/splash-768x1004.png',
{
url: '/icons/apple/splash-1536x2008.png',
media: '(device-width: 768px) and (device-height: 1024px)',
},
],
},
verification: {
google: 'google',
yandex: 'yandex',
yahoo: 'yahoo',
},
category: 'technology',
/*
appLinks: {
ios: {
url: 'https://techtracker.gbrown.org/ios',
app_store_id: 'com.gbrown.techtracker',
},
android: {
package: 'https://techtracker.gbrown.org/android',
app_name: 'app_t3_template',
},
web: {
url: 'https://techtracker.gbrown.org',
should_fallback: true,
},
},
*/
};
};

View File

@@ -0,0 +1,201 @@
import { type NextRequest, NextResponse } from 'next/server';
// In-memory stores for tracking IPs (use Redis in production)
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
const bannedIPs = new Set<string>();
// Ban Arctic Wolf Explicitly
bannedIPs.add('::ffff:10.0.1.49');
// Suspicious patterns that indicate malicious activity
const MALICIOUS_PATTERNS = [
// Your existing patterns
/web-inf/i,
/\.jsp/i,
/\.php/i,
/puttest/i,
/WEB-INF/i,
/\.xml$/i,
/perl/i,
/xampp/i,
/phpwebgallery/i,
/FileManager/i,
/standalonemanager/i,
/h2console/i,
/WebAdmin/i,
/login_form\.php/i,
/%2e/i,
/%u002e/i,
/\.%00/i,
/\.\./,
/lcgi/i,
// New patterns from your logs
/\/appliance\//i,
/bomgar/i,
/netburner-logo/i,
/\/ui\/images\//i,
/logon_merge/i,
/logon_t\.gif/i,
/login_top\.gif/i,
/theme1\/images/i,
/\.well-known\/acme-challenge\/.*\.jpg$/i,
/\.well-known\/pki-validation\/.*\.jpg$/i,
// Path traversal and system file access patterns
/\/etc\/passwd/i,
/\/etc%2fpasswd/i,
/\/etc%5cpasswd/i,
/\/\/+etc/i,
/\\\\+.*etc/i,
/%2f%2f/i,
/%5c%5c/i,
/\/\/+/,
/\\\\+/,
/%00/i,
/%23/i,
// Encoded path traversal attempts
/%2e%2e/i,
/%252e/i,
/%c0%ae/i,
/%c1%9c/i,
];
// Suspicious HTTP methods
const SUSPICIOUS_METHODS = ['TRACE', 'PUT', 'DELETE', 'PATCH'];
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const MAX_ATTEMPTS = 10; // Max suspicious requests per window
const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
// 404 rate limiting settings
const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes
const MAX_404_ATTEMPTS = 10; // Max 404s before ban
const getClientIP = (request: NextRequest): string => {
const forwarded = request.headers.get('x-forwarded-for');
const realIP = request.headers.get('x-real-ip');
const cfConnectingIP = request.headers.get('cf-connecting-ip');
if (forwarded) return (forwarded.split(',')[0] ?? '').trim();
if (realIP) return realIP;
if (cfConnectingIP) return cfConnectingIP;
return request.headers.get('host') ?? 'unknown';
};
const isPathSuspicious = (pathname: string): boolean => {
return MALICIOUS_PATTERNS.some((pattern) => pattern.test(pathname));
};
const isMethodSuspicious = (method: string): boolean => {
return SUSPICIOUS_METHODS.includes(method);
};
const updateIPAttempts = (ip: string): boolean => {
const now = Date.now();
const attempts = ipAttempts.get(ip);
if (!attempts || now - attempts.lastAttempt > RATE_LIMIT_WINDOW) {
ipAttempts.set(ip, { count: 1, lastAttempt: now });
return false;
}
attempts.count++;
attempts.lastAttempt = now;
if (attempts.count > MAX_ATTEMPTS) {
bannedIPs.add(ip);
ipAttempts.delete(ip);
setTimeout(() => {
bannedIPs.delete(ip);
}, BAN_DURATION);
return true;
}
return false;
};
const update404Attempts = (ip: string): boolean => {
const now = Date.now();
const attempts = ip404Attempts.get(ip);
if (!attempts || now - attempts.lastAttempt > RATE_404_WINDOW) {
ip404Attempts.set(ip, { count: 1, lastAttempt: now });
return false;
}
attempts.count++;
attempts.lastAttempt = now;
if (attempts.count > MAX_404_ATTEMPTS) {
bannedIPs.add(ip);
ip404Attempts.delete(ip);
console.log(
`🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`,
);
setTimeout(() => {
bannedIPs.delete(ip);
}, BAN_DURATION);
return true;
}
return false;
};
export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
const { pathname } = request.nextUrl;
const method = request.method;
const ip = getClientIP(request);
// Check if IP is already banned
if (bannedIPs.has(ip)) {
return new NextResponse('Access denied.', { status: 403 });
}
const isSuspiciousPath = isPathSuspicious(pathname);
const isSuspiciousMethod = isMethodSuspicious(method);
// Handle suspicious activity
if (isSuspiciousPath || isSuspiciousMethod) {
const shouldBan = updateIPAttempts(ip);
if (shouldBan) {
console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
return new NextResponse('Access denied - IP banned. Please fuck off.', {
status: 403,
});
}
return new NextResponse('Not Found', { status: 404 });
}
return null;
};
// Call this function when you detect a 404 response
export const handle404Response = (
request: NextRequest,
): NextResponse | null => {
const ip = getClientIP(request);
if (bannedIPs.has(ip)) {
return new NextResponse('Access denied.', { status: 403 });
}
const shouldBan = update404Attempts(ip);
if (shouldBan) {
return new NextResponse('Access denied - IP banned for excessive 404s.', {
status: 403,
});
}
return null;
};

20
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,20 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
};
export const ccn = ({
context,
className,
on = '',
off = '',
}: {
context: boolean;
className: string;
on: string;
off: string;
}) => {
return twMerge(className, context ? on : off);
};

167
src/styles/globals.css Normal file
View File

@@ -0,0 +1,167 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-sans), ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
:root {
--background: oklch(0.9227 0.0011 17.1793);
--foreground: oklch(0.2840 0.0220 262.4967);
--card: oklch(0.9699 0.0013 106.4238);
--card-foreground: oklch(0.2840 0.0220 262.4967);
--popover: oklch(0.9699 0.0013 106.4238);
--popover-foreground: oklch(0.2840 0.0220 262.4967);
--primary: oklch(0.6378 0.1247 281.2150);
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.8682 0.0026 48.7143);
--secondary-foreground: oklch(0.4507 0.0152 255.5845);
--muted: oklch(0.9227 0.0011 17.1793);
--muted-foreground: oklch(0.5551 0.0147 266.6154);
--accent: oklch(0.9409 0.0164 322.6966);
--accent-foreground: oklch(0.3774 0.0189 260.6754);
--destructive: oklch(0.6322 0.1310 21.4751);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0.8682 0.0026 48.7143);
--input: oklch(0.8682 0.0026 48.7143);
--ring: oklch(0.6378 0.1247 281.2150);
--chart-1: oklch(0.6378 0.1247 281.2150);
--chart-2: oklch(0.5608 0.1433 283.1275);
--chart-3: oklch(0.5008 0.1358 283.9499);
--chart-4: oklch(0.4372 0.1108 283.4322);
--chart-5: oklch(0.3928 0.0817 282.8932);
--sidebar: oklch(0.8682 0.0026 48.7143);
--sidebar-foreground: oklch(0.2840 0.0220 262.4967);
--sidebar-primary: oklch(0.6378 0.1247 281.2150);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.9409 0.0164 322.6966);
--sidebar-accent-foreground: oklch(0.3774 0.0189 260.6754);
--sidebar-border: oklch(0.8682 0.0026 48.7143);
--sidebar-ring: oklch(0.6378 0.1247 281.2150);
--font-sans: Inter, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 1.0rem;
--shadow-2xs: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.09);
--shadow-xs: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.09);
--shadow-sm: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 1px 2px 3px hsl(240 1.9608% 60% / 0.18);
--shadow: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 1px 2px 3px hsl(240 1.9608% 60% / 0.18);
--shadow-md: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 2px 4px 3px hsl(240 1.9608% 60% / 0.18);
--shadow-lg: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 4px 6px 3px hsl(240 1.9608% 60% / 0.18);
--shadow-xl: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.18), 2px 8px 10px 3px hsl(240 1.9608% 60% / 0.18);
--shadow-2xl: 2px 2px 10px 4px hsl(240 1.9608% 60% / 0.45);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
.dark {
--background: oklch(0.2236 0.0049 67.5717);
--foreground: oklch(0.9301 0.0075 260.7315);
--card: oklch(0.2793 0.0057 56.1503);
--card-foreground: oklch(0.9301 0.0075 260.7315);
--popover: oklch(0.2793 0.0057 56.1503);
--popover-foreground: oklch(0.9301 0.0075 260.7315);
--primary: oklch(0.7223 0.0946 279.6746);
--primary-foreground: oklch(0.2236 0.0049 67.5717);
--secondary: oklch(0.3352 0.0055 56.2080);
--secondary-foreground: oklch(0.8726 0.0059 264.5296);
--muted: oklch(0.2793 0.0057 56.1503);
--muted-foreground: oklch(0.7176 0.0111 261.7826);
--accent: oklch(0.3889 0.0053 56.2463);
--accent-foreground: oklch(0.8726 0.0059 264.5296);
--destructive: oklch(0.6322 0.1310 21.4751);
--destructive-foreground: oklch(0.2236 0.0049 67.5717);
--border: oklch(0.3352 0.0055 56.2080);
--input: oklch(0.3352 0.0055 56.2080);
--ring: oklch(0.7223 0.0946 279.6746);
--chart-1: oklch(0.7223 0.0946 279.6746);
--chart-2: oklch(0.6378 0.1247 281.2150);
--chart-3: oklch(0.5608 0.1433 283.1275);
--chart-4: oklch(0.5008 0.1358 283.9499);
--chart-5: oklch(0.4372 0.1108 283.4322);
--sidebar: oklch(0.3352 0.0055 56.2080);
--sidebar-foreground: oklch(0.9301 0.0075 260.7315);
--sidebar-primary: oklch(0.7223 0.0946 279.6746);
--sidebar-primary-foreground: oklch(0.2236 0.0049 67.5717);
--sidebar-accent: oklch(0.3889 0.0053 56.2463);
--sidebar-accent-foreground: oklch(0.8726 0.0059 264.5296);
--sidebar-border: oklch(0.3352 0.0055 56.2080);
--sidebar-ring: oklch(0.7223 0.0946 279.6746);
--font-sans: Inter, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 1.0rem;
--shadow-2xs: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.09);
--shadow-xs: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.09);
--shadow-sm: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 1px 2px 3px hsl(0 0% 10.1961% / 0.18);
--shadow: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 1px 2px 3px hsl(0 0% 10.1961% / 0.18);
--shadow-md: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 2px 4px 3px hsl(0 0% 10.1961% / 0.18);
--shadow-lg: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 4px 6px 3px hsl(0 0% 10.1961% / 0.18);
--shadow-xl: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.18), 2px 8px 10px 3px hsl(0 0% 10.1961% / 0.18);
--shadow-2xl: 2px 2px 10px 4px hsl(0 0% 10.1961% / 0.45);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}