algocoach 0.1.4 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -21
- package/README.md +173 -66
- package/dist/assets/index-Cdh7Qj-d.css +1 -0
- package/dist/assets/{index-D5iezweF.js → index-PSyWQaoU.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/server/db/schema.ts +20 -1
- package/server/db/setup.ts +27 -0
- package/server/index.ts +5 -0
- package/server/local-dev/.gitkeep +0 -0
- package/server/middleware/rate-limit.ts +7 -0
- package/server/routes/onboard.ts +0 -1
- package/server/routes/plan.ts +204 -22
- package/server/services/ai.ts +8 -1
- package/dist/assets/index-C4ELaZwf.css +0 -1
- package/server/local-dev/test-ai.ts +0 -88
package/dist/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-PSyWQaoU.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cdh7Qj-d.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/db/schema.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
sqliteTable,
|
|
3
3
|
text as sqliteText,
|
|
4
4
|
integer,
|
|
5
|
+
real,
|
|
5
6
|
} from "drizzle-orm/sqlite-core"
|
|
6
7
|
import { jsonText, textArray } from "./custom-types"
|
|
7
8
|
|
|
@@ -114,7 +115,25 @@ export const dailyPlan = sqliteTable("daily_plan", {
|
|
|
114
115
|
date: integer("date", { mode: "timestamp_ms" }).notNull(),
|
|
115
116
|
weekNumber: integer("week_number").notNull(),
|
|
116
117
|
topic: text("topic").notNull(),
|
|
117
|
-
problems: jsonText<{ title: string; titleSlug: string; difficulty: string; topicTags: string[]; leetcodeUrl: string; acRate: number; status?: string; completedAt?: string | null }[]>()("problems").notNull(),
|
|
118
|
+
problems: jsonText<{ title: string; titleSlug: string; difficulty: string; topicTags: string[]; leetcodeUrl: string; acRate: number; status?: string; completedAt?: string | null; isReview?: boolean }[]>()("problems").notNull(),
|
|
119
|
+
explanation: text("explanation"),
|
|
120
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => new Date()),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export const problemReview = sqliteTable("problem_review", {
|
|
124
|
+
id: text("id").primaryKey(),
|
|
125
|
+
userId: text("user_id").notNull().references(() => user.id),
|
|
126
|
+
problemId: text("problem_id").notNull(),
|
|
127
|
+
problemName: text("problem_name").notNull(),
|
|
128
|
+
difficulty: text("difficulty").notNull(),
|
|
129
|
+
topics: jsonText<string[]>()("topics").notNull(),
|
|
130
|
+
leetcodeUrl: text("leetcode_url").notNull(),
|
|
131
|
+
acRate: real("ac_rate").notNull(),
|
|
132
|
+
interval: integer("interval").notNull().default(1),
|
|
133
|
+
easinessFactor: real("easiness_factor").notNull().default(2.5),
|
|
134
|
+
nextReviewAt: integer("next_review_at", { mode: "timestamp_ms" }).notNull(),
|
|
135
|
+
lastReviewedAt: integer("last_reviewed_at", { mode: "timestamp_ms" }),
|
|
136
|
+
reviewCount: integer("review_count").notNull().default(0),
|
|
118
137
|
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => new Date()),
|
|
119
138
|
})
|
|
120
139
|
|
package/server/db/setup.ts
CHANGED
|
@@ -122,6 +122,7 @@ export function createTables(db: Database) {
|
|
|
122
122
|
"week_number" integer NOT NULL,
|
|
123
123
|
"topic" text NOT NULL,
|
|
124
124
|
"problems" text NOT NULL,
|
|
125
|
+
"explanation" text,
|
|
125
126
|
"created_at" integer NOT NULL
|
|
126
127
|
)
|
|
127
128
|
`)
|
|
@@ -137,8 +138,34 @@ export function createTables(db: Database) {
|
|
|
137
138
|
"updated_at" integer NOT NULL
|
|
138
139
|
)
|
|
139
140
|
`)
|
|
141
|
+
db.run(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS "problem_review" (
|
|
143
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
144
|
+
"user_id" text NOT NULL REFERENCES "user"("id"),
|
|
145
|
+
"problem_id" text NOT NULL,
|
|
146
|
+
"problem_name" text NOT NULL,
|
|
147
|
+
"difficulty" text NOT NULL,
|
|
148
|
+
"topics" text NOT NULL,
|
|
149
|
+
"leetcode_url" text NOT NULL,
|
|
150
|
+
"ac_rate" real NOT NULL,
|
|
151
|
+
"interval" integer NOT NULL DEFAULT 1,
|
|
152
|
+
"easiness_factor" real NOT NULL DEFAULT 2.5,
|
|
153
|
+
"next_review_at" integer NOT NULL,
|
|
154
|
+
"last_reviewed_at" integer,
|
|
155
|
+
"review_count" integer NOT NULL DEFAULT 0,
|
|
156
|
+
"created_at" integer NOT NULL
|
|
157
|
+
)
|
|
158
|
+
`)
|
|
140
159
|
|
|
141
160
|
// Add unique indexes for tables that might already exist without them
|
|
142
161
|
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_leetcode_account_user_id ON leetcode_account(user_id)")
|
|
143
162
|
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_roadmap_plan_user_id ON roadmap_plan(user_id)")
|
|
163
|
+
|
|
164
|
+
// Performance indexes for frequently queried columns
|
|
165
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_daily_plan_user_date ON daily_plan(user_id, date)")
|
|
166
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_daily_progress_user_problem ON daily_progress(user_id, problem_id)")
|
|
167
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_roadmap_job_user ON roadmap_job(user_id)")
|
|
168
|
+
|
|
169
|
+
// Migrations for existing databases (safe to run multiple times)
|
|
170
|
+
try { db.run("ALTER TABLE daily_plan ADD COLUMN explanation text") } catch {}
|
|
144
171
|
}
|
package/server/index.ts
CHANGED
|
@@ -12,6 +12,11 @@ import fs from 'fs'
|
|
|
12
12
|
|
|
13
13
|
const app = new Hono()
|
|
14
14
|
|
|
15
|
+
app.onError((err, c) => {
|
|
16
|
+
console.error(err)
|
|
17
|
+
return c.json({ success: false, error: err.message || 'Internal server error' }, 500)
|
|
18
|
+
})
|
|
19
|
+
|
|
15
20
|
const productionUrl = process.env.BETTER_AUTH_URL || ''
|
|
16
21
|
|
|
17
22
|
app.use('/*', cors({
|
|
File without changes
|
|
@@ -7,6 +7,13 @@ interface Entry {
|
|
|
7
7
|
|
|
8
8
|
const store = new Map<string, Entry>()
|
|
9
9
|
|
|
10
|
+
setInterval(() => {
|
|
11
|
+
const now = Date.now()
|
|
12
|
+
for (const [key, entry] of store) {
|
|
13
|
+
if (now > entry.resetAt) store.delete(key)
|
|
14
|
+
}
|
|
15
|
+
}, 60_000)
|
|
16
|
+
|
|
10
17
|
export function rateLimit(maxRequests: number, windowMs: number) {
|
|
11
18
|
return createMiddleware(async (c, next) => {
|
|
12
19
|
const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown'
|
package/server/routes/onboard.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { db } from '../db'
|
|
|
4
4
|
import { userPreferences, roadmapPlan } from '../db/schema'
|
|
5
5
|
import { authMiddleware } from '../middleware/auth'
|
|
6
6
|
import { eq } from 'drizzle-orm'
|
|
7
|
-
import { generateRoadmap } from '../services/ai'
|
|
8
7
|
|
|
9
8
|
const app = new Hono<{ Variables: { userId: string } }>()
|
|
10
9
|
app.use('/*', authMiddleware)
|
package/server/routes/plan.ts
CHANGED
|
@@ -1,15 +1,137 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
2
|
import { streamSSE } from 'hono/streaming'
|
|
3
|
+
import { z } from 'zod'
|
|
3
4
|
import { db } from '../db'
|
|
4
|
-
import { userPreferences, roadmapPlan, dailyPlan, dailyProgress, roadmapJob } from '../db/schema'
|
|
5
|
+
import { userPreferences, roadmapPlan, dailyPlan, dailyProgress, roadmapJob, problemReview } from '../db/schema'
|
|
5
6
|
import { authMiddleware } from '../middleware/auth'
|
|
6
|
-
import { eq, and,
|
|
7
|
+
import { eq, and, desc, sql } from 'drizzle-orm'
|
|
7
8
|
import { selectDailyProblems, startJobProcessing } from '../services/ai'
|
|
8
9
|
|
|
9
10
|
function sleep(ms: number) {
|
|
10
11
|
return new Promise((r) => setTimeout(r, ms))
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
async function getCurrentWeek(userId: string, weeks: any[]): Promise<number> {
|
|
15
|
+
if (!weeks.length) return 1
|
|
16
|
+
|
|
17
|
+
const allPlans = await db.query.dailyPlan.findMany({
|
|
18
|
+
where: eq(dailyPlan.userId, userId),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < weeks.length; i++) {
|
|
22
|
+
const week = weeks[i] as Record<string, unknown>
|
|
23
|
+
const weekNum = week.week as number
|
|
24
|
+
const targetCount = (week.problemsCount as number) || 1
|
|
25
|
+
const weekPlans = allPlans.filter((p: any) => p.weekNumber === weekNum)
|
|
26
|
+
const solvedCount = weekPlans.reduce((count, plan) => {
|
|
27
|
+
const problems = Array.isArray(plan.problems) ? plan.problems : []
|
|
28
|
+
return count + problems.filter((p: any) => (p as Record<string, unknown>).status === 'SOLVED').length
|
|
29
|
+
}, 0)
|
|
30
|
+
const percent = Math.round((solvedCount / targetCount) * 100)
|
|
31
|
+
if (percent < 100) return weekNum
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return weeks.length
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function scheduleReview(userId: string, problem: any, status: string) {
|
|
38
|
+
const existing = await db.query.problemReview.findFirst({
|
|
39
|
+
where: and(eq(problemReview.userId, userId), eq(problemReview.problemId, problem.titleSlug)),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
const dayMs = 86400000
|
|
44
|
+
|
|
45
|
+
if (status === 'SOLVED') {
|
|
46
|
+
const quality = existing ? 4 : 3
|
|
47
|
+
if (existing) {
|
|
48
|
+
let ef = existing.easinessFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
|
|
49
|
+
if (ef < 1.3) ef = 1.3
|
|
50
|
+
const newInterval = existing.reviewCount === 0 ? 1 : existing.reviewCount === 1 ? 6 : Math.round(existing.interval * ef)
|
|
51
|
+
await db.update(problemReview).set({
|
|
52
|
+
interval: newInterval,
|
|
53
|
+
easinessFactor: ef,
|
|
54
|
+
nextReviewAt: new Date(now + newInterval * dayMs),
|
|
55
|
+
lastReviewedAt: new Date(now),
|
|
56
|
+
reviewCount: existing.reviewCount + 1,
|
|
57
|
+
}).where(eq(problemReview.id, existing.id))
|
|
58
|
+
} else {
|
|
59
|
+
await db.insert(problemReview).values({
|
|
60
|
+
id: crypto.randomUUID(),
|
|
61
|
+
userId,
|
|
62
|
+
problemId: problem.titleSlug,
|
|
63
|
+
problemName: problem.title,
|
|
64
|
+
difficulty: problem.difficulty,
|
|
65
|
+
topics: problem.topicTags || [],
|
|
66
|
+
leetcodeUrl: problem.leetcodeUrl,
|
|
67
|
+
acRate: problem.acRate,
|
|
68
|
+
interval: 1,
|
|
69
|
+
easinessFactor: 2.5,
|
|
70
|
+
nextReviewAt: new Date(now + dayMs),
|
|
71
|
+
lastReviewedAt: new Date(now),
|
|
72
|
+
reviewCount: 1,
|
|
73
|
+
createdAt: new Date(),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
} else if (['TRIED', 'SKIPPED'].includes(status)) {
|
|
77
|
+
if (existing) {
|
|
78
|
+
let ef = existing.easinessFactor - 0.2
|
|
79
|
+
if (ef < 1.3) ef = 1.3
|
|
80
|
+
await db.update(problemReview).set({
|
|
81
|
+
interval: 1,
|
|
82
|
+
easinessFactor: ef,
|
|
83
|
+
nextReviewAt: new Date(now + dayMs),
|
|
84
|
+
lastReviewedAt: new Date(now),
|
|
85
|
+
reviewCount: existing.reviewCount + 1,
|
|
86
|
+
}).where(eq(problemReview.id, existing.id))
|
|
87
|
+
} else {
|
|
88
|
+
await db.insert(problemReview).values({
|
|
89
|
+
id: crypto.randomUUID(),
|
|
90
|
+
userId,
|
|
91
|
+
problemId: problem.titleSlug,
|
|
92
|
+
problemName: problem.title,
|
|
93
|
+
difficulty: problem.difficulty,
|
|
94
|
+
topics: problem.topicTags || [],
|
|
95
|
+
leetcodeUrl: problem.leetcodeUrl,
|
|
96
|
+
acRate: problem.acRate,
|
|
97
|
+
interval: 1,
|
|
98
|
+
easinessFactor: 1.3,
|
|
99
|
+
nextReviewAt: new Date(now + Math.round(dayMs / 2)),
|
|
100
|
+
lastReviewedAt: new Date(now),
|
|
101
|
+
reviewCount: 1,
|
|
102
|
+
createdAt: new Date(),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function getDueReviews(userId: string): Promise<any[]> {
|
|
109
|
+
const now = new Date()
|
|
110
|
+
const due = await db.query.problemReview.findMany({
|
|
111
|
+
where: and(eq(problemReview.userId, userId), sql`${problemReview.nextReviewAt} <= ${now}`),
|
|
112
|
+
orderBy: [sql`${problemReview.nextReviewAt} ASC`],
|
|
113
|
+
limit: 2,
|
|
114
|
+
})
|
|
115
|
+
return due.map((r) => ({
|
|
116
|
+
title: r.problemName,
|
|
117
|
+
titleSlug: r.problemId,
|
|
118
|
+
difficulty: r.difficulty,
|
|
119
|
+
topicTags: r.topics,
|
|
120
|
+
leetcodeUrl: r.leetcodeUrl,
|
|
121
|
+
acRate: r.acRate,
|
|
122
|
+
status: 'PENDING' as const,
|
|
123
|
+
completedAt: null,
|
|
124
|
+
isReview: true,
|
|
125
|
+
}))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const difficultySchema = z.enum(['EASY', 'MEDIUM', 'HARD', 'MIXED'])
|
|
129
|
+
const planStatusSchema = z.enum(['SOLVED', 'TRIED', 'SKIPPED', 'PENDING'])
|
|
130
|
+
const regenerateBodySchema = z.object({
|
|
131
|
+
slot: z.number().int().min(0).optional(),
|
|
132
|
+
easier: z.boolean().optional(),
|
|
133
|
+
})
|
|
134
|
+
|
|
13
135
|
function tryParseError(msg: string): string {
|
|
14
136
|
try {
|
|
15
137
|
const top = JSON.parse(msg)
|
|
@@ -38,7 +160,8 @@ app.get('/roadmap', async (c) => {
|
|
|
38
160
|
})
|
|
39
161
|
if (!plan) return c.json({ success: false, error: 'No roadmap found' }, 404)
|
|
40
162
|
const weeks = Array.isArray(plan.weeks) ? plan.weeks : []
|
|
41
|
-
|
|
163
|
+
const currentWeek = await getCurrentWeek(userId, weeks)
|
|
164
|
+
return c.json({ success: true, data: { ...plan, currentWeek, ready: weeks.length > 0 } })
|
|
42
165
|
} catch (err: any) {
|
|
43
166
|
return c.json({ success: false, error: err.message }, 500)
|
|
44
167
|
}
|
|
@@ -55,20 +178,47 @@ app.patch('/roadmap/advance', async (c) => {
|
|
|
55
178
|
const weeks = Array.isArray(plan.weeks) ? plan.weeks : []
|
|
56
179
|
if (!weeks.length) return c.json({ success: false, error: 'Roadmap not ready' }, 400)
|
|
57
180
|
|
|
58
|
-
|
|
181
|
+
const currentWeek = await getCurrentWeek(userId, weeks)
|
|
182
|
+
if (currentWeek >= weeks.length) {
|
|
59
183
|
return c.json({ success: false, error: 'Roadmap already completed' }, 400)
|
|
60
184
|
}
|
|
61
185
|
|
|
62
|
-
|
|
186
|
+
return c.json({ success: true, data: { ...plan, currentWeek, ready: true } })
|
|
187
|
+
} catch (err: any) {
|
|
188
|
+
return c.json({ success: false, error: err.message }, 500)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
app.patch('/roadmap/week/:weekNumber', async (c) => {
|
|
193
|
+
try {
|
|
194
|
+
const userId = c.get('userId')
|
|
195
|
+
const weekNumber = parseInt(c.req.param('weekNumber'), 10)
|
|
196
|
+
const body: any = await c.req.json()
|
|
197
|
+
const { problemsCount } = body
|
|
198
|
+
if (typeof problemsCount !== 'number' || problemsCount < 1 || problemsCount > 30) {
|
|
199
|
+
return c.json({ success: false, error: 'problemsCount must be a number between 1 and 30' }, 400)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const plan = await db.query.roadmapPlan.findFirst({
|
|
203
|
+
where: eq(roadmapPlan.userId, userId),
|
|
204
|
+
})
|
|
205
|
+
if (!plan) return c.json({ success: false, error: 'No roadmap found' }, 404)
|
|
206
|
+
|
|
207
|
+
const weeks = Array.isArray(plan.weeks) ? [...plan.weeks] : []
|
|
208
|
+
const idx = weeks.findIndex((w: any) => (w as Record<string, unknown>).week === weekNumber)
|
|
209
|
+
if (idx === -1) return c.json({ success: false, error: `Week ${weekNumber} not found` }, 404)
|
|
210
|
+
|
|
211
|
+
weeks[idx] = { ...(weeks[idx] as any), problemsCount }
|
|
212
|
+
|
|
63
213
|
await db.update(roadmapPlan).set({
|
|
64
|
-
|
|
214
|
+
weeks: weeks as any,
|
|
65
215
|
updatedAt: new Date(),
|
|
66
216
|
}).where(eq(roadmapPlan.userId, userId))
|
|
67
217
|
|
|
68
218
|
const updated = await db.query.roadmapPlan.findFirst({
|
|
69
219
|
where: eq(roadmapPlan.userId, userId),
|
|
70
220
|
})
|
|
71
|
-
return c.json({ success: true, data:
|
|
221
|
+
return c.json({ success: true, data: updated })
|
|
72
222
|
} catch (err: any) {
|
|
73
223
|
return c.json({ success: false, error: err.message }, 500)
|
|
74
224
|
}
|
|
@@ -234,7 +384,7 @@ app.get('/roadmap/progress', async (c) => {
|
|
|
234
384
|
targetCount: w.problemsCount,
|
|
235
385
|
assignedCount: total,
|
|
236
386
|
solvedCount: solved,
|
|
237
|
-
percent:
|
|
387
|
+
percent: w.problemsCount > 0 ? Math.round((solved / w.problemsCount) * 100) : 0,
|
|
238
388
|
}
|
|
239
389
|
})
|
|
240
390
|
|
|
@@ -258,7 +408,15 @@ app.get('/today', async (c) => {
|
|
|
258
408
|
),
|
|
259
409
|
})
|
|
260
410
|
if (!plan) return c.json({ success: false, exists: false }, 404)
|
|
261
|
-
|
|
411
|
+
|
|
412
|
+
const planRecord = await db.query.roadmapPlan.findFirst({
|
|
413
|
+
where: eq(roadmapPlan.userId, userId),
|
|
414
|
+
})
|
|
415
|
+
const weeks = Array.isArray(planRecord?.weeks) ? planRecord.weeks : []
|
|
416
|
+
const currentWeek = await getCurrentWeek(userId, weeks)
|
|
417
|
+
const topic = weeks[currentWeek - 1] ? (weeks[currentWeek - 1] as Record<string, unknown>)?.topic as string : plan.topic
|
|
418
|
+
|
|
419
|
+
return c.json({ success: true, exists: true, data: { ...plan, weekNumber: currentWeek, topic } })
|
|
262
420
|
} catch (err: any) {
|
|
263
421
|
return c.json({ success: false, error: err.message }, 500)
|
|
264
422
|
}
|
|
@@ -268,7 +426,8 @@ app.post('/today', async (c) => {
|
|
|
268
426
|
try {
|
|
269
427
|
const userId = c.get('userId')
|
|
270
428
|
const body: any = await c.req.json().catch(() => ({}))
|
|
271
|
-
const
|
|
429
|
+
const diffResult = difficultySchema.safeParse(body.difficulty)
|
|
430
|
+
const difficultyFilter = diffResult.success ? diffResult.data : 'MIXED'
|
|
272
431
|
|
|
273
432
|
const todayMs = Date.now() - (Date.now() % 86400000)
|
|
274
433
|
const tomorrowMs = todayMs + 86400000
|
|
@@ -289,7 +448,10 @@ app.post('/today', async (c) => {
|
|
|
289
448
|
|
|
290
449
|
const roadmap = Array.isArray(planRecord.weeks) ? planRecord.weeks : []
|
|
291
450
|
if (roadmap.length === 0) return c.json({ success: false, error: 'Roadmap is still being generated. Please try again shortly.' }, 400)
|
|
292
|
-
const currentWeek =
|
|
451
|
+
const currentWeek = await getCurrentWeek(userId, roadmap)
|
|
452
|
+
if (currentWeek > roadmap.length) {
|
|
453
|
+
return c.json({ success: false, error: 'Roadmap is complete. Generate a new roadmap to continue.' }, 400)
|
|
454
|
+
}
|
|
293
455
|
|
|
294
456
|
const allPlans = await db.query.dailyPlan.findMany({
|
|
295
457
|
where: eq(dailyPlan.userId, userId),
|
|
@@ -326,18 +488,22 @@ app.post('/today', async (c) => {
|
|
|
326
488
|
completedAt: null,
|
|
327
489
|
}))
|
|
328
490
|
|
|
491
|
+
const dueReviews = await getDueReviews(userId)
|
|
492
|
+
const allProblems = [...problems, ...dueReviews]
|
|
493
|
+
|
|
329
494
|
const entry = {
|
|
330
495
|
id: crypto.randomUUID(),
|
|
331
496
|
userId,
|
|
332
497
|
date: new Date(),
|
|
333
498
|
weekNumber: currentWeek,
|
|
334
499
|
topic: (roadmap[currentWeek - 1] as Record<string, unknown>)?.topic as string || '',
|
|
335
|
-
problems,
|
|
500
|
+
problems: allProblems,
|
|
501
|
+
explanation: task.explanation,
|
|
336
502
|
}
|
|
337
503
|
|
|
338
504
|
await db.insert(dailyPlan).values(entry)
|
|
339
505
|
|
|
340
|
-
return c.json({ success: true, data:
|
|
506
|
+
return c.json({ success: true, data: entry }, 201)
|
|
341
507
|
} catch (err: any) {
|
|
342
508
|
return c.json({ success: false, error: `Failed to generate daily plan: ${err.message}` }, 500)
|
|
343
509
|
}
|
|
@@ -348,10 +514,11 @@ app.patch('/today/:planId/problem/:slug', async (c) => {
|
|
|
348
514
|
const userId = c.get('userId')
|
|
349
515
|
const { planId, slug } = c.req.param()
|
|
350
516
|
const body: any = await c.req.json()
|
|
351
|
-
const
|
|
352
|
-
if (!
|
|
517
|
+
const statusResult = planStatusSchema.safeParse(body.status)
|
|
518
|
+
if (!statusResult.success) {
|
|
353
519
|
return c.json({ success: false, error: 'Invalid status. Use SOLVED, TRIED, SKIPPED, or PENDING.' }, 400)
|
|
354
520
|
}
|
|
521
|
+
const status = statusResult.data
|
|
355
522
|
|
|
356
523
|
const plan = await db.query.dailyPlan.findFirst({
|
|
357
524
|
where: and(eq(dailyPlan.id, planId), eq(dailyPlan.userId, userId)),
|
|
@@ -393,6 +560,8 @@ app.patch('/today/:planId/problem/:slug', async (c) => {
|
|
|
393
560
|
})
|
|
394
561
|
}
|
|
395
562
|
|
|
563
|
+
scheduleReview(userId, problem, status).catch(() => {})
|
|
564
|
+
|
|
396
565
|
return c.json({ success: true, data: { ...problem, status, completedAt: problem.completedAt } })
|
|
397
566
|
} catch (err: any) {
|
|
398
567
|
return c.json({ success: false, error: err.message }, 500)
|
|
@@ -404,8 +573,9 @@ app.post('/today/:planId/regenerate', async (c) => {
|
|
|
404
573
|
const userId = c.get('userId')
|
|
405
574
|
const { planId } = c.req.param()
|
|
406
575
|
const body: any = await c.req.json().catch(() => ({}))
|
|
407
|
-
const
|
|
408
|
-
const
|
|
576
|
+
const regenResult = regenerateBodySchema.safeParse(body)
|
|
577
|
+
const slot = regenResult.success ? regenResult.data.slot : undefined
|
|
578
|
+
const easier = regenResult.success ? regenResult.data.easier === true : false
|
|
409
579
|
|
|
410
580
|
const plan = await db.query.dailyPlan.findFirst({
|
|
411
581
|
where: and(eq(dailyPlan.id, planId), eq(dailyPlan.userId, userId)),
|
|
@@ -424,6 +594,7 @@ app.post('/today/:planId/regenerate', async (c) => {
|
|
|
424
594
|
where: eq(roadmapPlan.userId, userId),
|
|
425
595
|
})
|
|
426
596
|
const roadmap = Array.isArray(planRecord?.weeks) ? planRecord.weeks : []
|
|
597
|
+
const currentWeek = await getCurrentWeek(userId, roadmap)
|
|
427
598
|
|
|
428
599
|
const allPlans = await db.query.dailyPlan.findMany({
|
|
429
600
|
where: eq(dailyPlan.userId, userId),
|
|
@@ -452,7 +623,7 @@ app.post('/today/:planId/regenerate', async (c) => {
|
|
|
452
623
|
}
|
|
453
624
|
const result = await selectDailyProblems({
|
|
454
625
|
roadmap,
|
|
455
|
-
currentWeek
|
|
626
|
+
currentWeek,
|
|
456
627
|
progress: progress.map((p) => ({ problemId: p.problemId, status: p.status })),
|
|
457
628
|
dedupCount,
|
|
458
629
|
solvedSlugs,
|
|
@@ -468,7 +639,7 @@ app.post('/today/:planId/regenerate', async (c) => {
|
|
|
468
639
|
} else {
|
|
469
640
|
const result = await selectDailyProblems({
|
|
470
641
|
roadmap,
|
|
471
|
-
currentWeek
|
|
642
|
+
currentWeek,
|
|
472
643
|
progress: progress.map((p) => ({ problemId: p.problemId, status: p.status })),
|
|
473
644
|
dedupCount,
|
|
474
645
|
solvedSlugs,
|
|
@@ -503,7 +674,11 @@ app.get('/streak', async (c) => {
|
|
|
503
674
|
orderBy: [desc(dailyPlan.date)],
|
|
504
675
|
})
|
|
505
676
|
|
|
506
|
-
|
|
677
|
+
const progressRecords = await db.query.dailyProgress.findMany({
|
|
678
|
+
where: and(eq(dailyProgress.userId, userId), eq(dailyProgress.status, 'SOLVED')),
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
if (!plans.length && !progressRecords.length) {
|
|
507
682
|
return c.json({ success: true, data: { currentStreak: 0, longestStreak: 0, solvedToday: false } })
|
|
508
683
|
}
|
|
509
684
|
|
|
@@ -519,6 +694,13 @@ app.get('/streak', async (c) => {
|
|
|
519
694
|
if (hasSolved) solvedDates.add(dateStr)
|
|
520
695
|
}
|
|
521
696
|
|
|
697
|
+
for (const r of progressRecords) {
|
|
698
|
+
const d = new Date(r.date)
|
|
699
|
+
const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`
|
|
700
|
+
solvedDates.add(dateStr)
|
|
701
|
+
allDates.add(dateStr)
|
|
702
|
+
}
|
|
703
|
+
|
|
522
704
|
const today = new Date()
|
|
523
705
|
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`
|
|
524
706
|
const solvedToday = solvedDates.has(todayStr)
|
|
@@ -546,8 +728,8 @@ app.get('/streak', async (c) => {
|
|
|
546
728
|
} else {
|
|
547
729
|
const prev = new Date(sortedDates[i - 1])
|
|
548
730
|
const curr = new Date(sortedDates[i])
|
|
549
|
-
const diffMs = curr.
|
|
550
|
-
const diffDays = Math.
|
|
731
|
+
const diffMs = Date.UTC(curr.getFullYear(), curr.getMonth(), curr.getDate()) - Date.UTC(prev.getFullYear(), prev.getMonth(), prev.getDate())
|
|
732
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
551
733
|
if (diffDays === 1) {
|
|
552
734
|
tempStreak++
|
|
553
735
|
} else {
|
package/server/services/ai.ts
CHANGED
|
@@ -192,6 +192,13 @@ export async function selectDailyProblems(params: {
|
|
|
192
192
|
if (filtered.length) searchResults = filtered
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
if (!searchResults.length) {
|
|
196
|
+
return {
|
|
197
|
+
problems: [],
|
|
198
|
+
explanation: `No unsolved ${difficultyFilter.toLowerCase()} problems found for ${week.topic}. Try a different difficulty filter.`,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
195
202
|
const easy = searchResults.filter((p) => p.difficulty === "Easy").sort((a, b) => b.acRate - a.acRate)
|
|
196
203
|
const medium = searchResults.filter((p) => p.difficulty === "Medium").sort((a, b) => b.acRate - a.acRate)
|
|
197
204
|
const hard = searchResults.filter((p) => p.difficulty === "Hard").sort((a, b) => b.acRate - a.acRate)
|
|
@@ -364,5 +371,5 @@ function parseTopicToSlugs(topic: string): string[] {
|
|
|
364
371
|
if (slug) slugs.add(slug)
|
|
365
372
|
}
|
|
366
373
|
}
|
|
367
|
-
return [...slugs]
|
|
374
|
+
return [...slugs].filter(Boolean)
|
|
368
375
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:JetBrains Mono,Fira Code,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder{opacity:1;color:#9ca3af}textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--surface-950:255 255 255;--surface-900:250 250 250;--surface-800:243 244 246;--surface-700:229 231 235;--surface-600:209 213 219;--surface-500:156 163 175;--surface-400:107 114 128;--surface-300:75 85 99;--surface-200:31 41 55;--surface-100:17 24 39;--surface-50:3 7 12;--accent-50:239 246 255;--accent-100:219 234 254;--accent-200:191 219 254;--accent-300:147 197 253;--accent-400:96 165 250;--accent-500:59 130 246;--accent-600:37 99 235;--accent-700:29 78 216;--accent-800:30 64 175;--accent-900:30 58 138;--accent-950:23 37 84}.dark{--surface-50:212 212 212;--surface-100:200 200 200;--surface-200:180 180 180;--surface-300:156 156 156;--surface-400:120 120 120;--surface-500:100 100 100;--surface-600:80 80 80;--surface-700:60 60 60;--surface-800:45 45 45;--surface-900:30 30 30;--surface-950:20 20 20;--accent-50:240 240 240;--accent-100:224 224 224;--accent-200:200 200 200;--accent-300:176 176 176;--accent-400:150 150 150;--accent-500:120 120 120;--accent-600:100 100 100;--accent-700:80 80 80;--accent-800:60 60 60;--accent-900:45 45 45;--accent-950:35 35 35}html{scroll-behavior:smooth}body{--tw-bg-opacity:1;background-color:rgb(var(--surface-950) / var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(var(--surface-200) / var(--tw-text-opacity,1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Inter,system-ui,-apple-system,sans-serif}::selection{background-color:rgb(var(--accent-500) / .3);--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}::-webkit-scrollbar{width:.5rem}::-webkit-scrollbar-track{--tw-bg-opacity:1;background-color:rgb(var(--surface-800) / var(--tw-bg-opacity,1))}::-webkit-scrollbar-thumb{--tw-bg-opacity:1;background-color:rgb(var(--surface-600) / var(--tw-bg-opacity,1));border-radius:9999px}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity:1;background-color:rgb(var(--surface-500) / var(--tw-bg-opacity,1))}.glass-card{border-width:1px;border-color:rgb(var(--surface-600) / .5);background-color:rgb(var(--surface-800) / .6);--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);border-radius:1rem}.glass-card-hover{border-width:1px;border-color:rgb(var(--surface-600) / .5);background-color:rgb(var(--surface-800) / .6);--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);border-radius:1rem;transition-property:all;transition-duration:.3s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.glass-card-hover:hover{border-color:rgb(var(--accent-500) / .3);--tw-shadow:var(--tw-shadow-colored);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow);--tw-shadow-color:rgb(var(--accent-500) / .05)}.gradient-text{background-image:linear-gradient(to right, var(--tw-gradient-stops));--tw-gradient-from:#fff var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--accent-400) / 1) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), rgb(var(--accent-200) / 1) var(--tw-gradient-via-position), var(--tw-gradient-to);color:#0000;-webkit-background-clip:text;background-clip:text}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-4{inset:1rem}.-left-32{left:-8rem}.-right-32{right:-8rem}.bottom-1\/4{bottom:25%}.left-0{left:0}.left-3{left:.75rem}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.top-1\/4{top:25%}.z-40{z-index:40}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.ml-4{margin-left:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0\.5{height:.125rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-full{height:100%}.max-h-32{max-height:8rem}.max-h-60{max-height:15rem}.max-h-80{max-height:20rem}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-10{width:2.5rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-96{width:24rem}.w-full{width:100%}.min-w-0{min-width:0}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-7xl{max-width:80rem}.max-w-md{max-width:28rem}.max-w-xl{max-width:36rem}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate:-90deg;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:2s cubic-bezier(.4,0,.6,1) infinite pulse}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:1s linear infinite spin}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-16{gap:4rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.-space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(-.5rem * var(--tw-space-x-reverse));margin-left:calc(-.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-accent-500{--tw-border-opacity:1;border-color:rgb(var(--accent-500) / var(--tw-border-opacity,1))}.border-accent-500\/20{border-color:rgb(var(--accent-500) / .2)}.border-accent-500\/40{border-color:rgb(var(--accent-500) / .4)}.border-accent-500\/50{border-color:rgb(var(--accent-500) / .5)}.border-amber-500\/20{border-color:#f59e0b33}.border-emerald-500\/20{border-color:#10b98133}.border-red-500\/20{border-color:#ef444433}.border-surface-500\/30{border-color:rgb(var(--surface-500) / .3)}.border-surface-600{--tw-border-opacity:1;border-color:rgb(var(--surface-600) / var(--tw-border-opacity,1))}.border-surface-600\/30{border-color:rgb(var(--surface-600) / .3)}.border-surface-700{--tw-border-opacity:1;border-color:rgb(var(--surface-700) / var(--tw-border-opacity,1))}.border-surface-700\/30{border-color:rgb(var(--surface-700) / .3)}.border-surface-700\/50{border-color:rgb(var(--surface-700) / .5)}.border-surface-800{--tw-border-opacity:1;border-color:rgb(var(--surface-800) / var(--tw-border-opacity,1))}.border-surface-800\/50{border-color:rgb(var(--surface-800) / .5)}.border-surface-950{--tw-border-opacity:1;border-color:rgb(var(--surface-950) / var(--tw-border-opacity,1))}.border-transparent{border-color:#0000}.bg-accent-400{--tw-bg-opacity:1;background-color:rgb(var(--accent-400) / var(--tw-bg-opacity,1))}.bg-accent-500{--tw-bg-opacity:1;background-color:rgb(var(--accent-500) / var(--tw-bg-opacity,1))}.bg-accent-500\/10{background-color:rgb(var(--accent-500) / .1)}.bg-accent-500\/20{background-color:rgb(var(--accent-500) / .2)}.bg-accent-500\/30{background-color:rgb(var(--accent-500) / .3)}.bg-accent-500\/5{background-color:rgb(var(--accent-500) / .05)}.bg-accent-600{--tw-bg-opacity:1;background-color:rgb(var(--accent-600) / var(--tw-bg-opacity,1))}.bg-amber-500{--tw-bg-opacity:1;background-color:rgb(245 158 11/var(--tw-bg-opacity,1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-black\/60{background-color:#0009}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/10{background-color:#10b9811a}.bg-emerald-500\/5{background-color:#10b9810d}.bg-emerald-600{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.bg-green-500\/80{background-color:#22c55ecc}.bg-indigo-500\/10{background-color:#6366f11a}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-500\/80{background-color:#ef4444cc}.bg-surface-500\/30{background-color:rgb(var(--surface-500) / .3)}.bg-surface-600\/30{background-color:rgb(var(--surface-600) / .3)}.bg-surface-700{--tw-bg-opacity:1;background-color:rgb(var(--surface-700) / var(--tw-bg-opacity,1))}.bg-surface-700\/30{background-color:rgb(var(--surface-700) / .3)}.bg-surface-700\/50{background-color:rgb(var(--surface-700) / .5)}.bg-surface-800{--tw-bg-opacity:1;background-color:rgb(var(--surface-800) / var(--tw-bg-opacity,1))}.bg-surface-800\/10{background-color:rgb(var(--surface-800) / .1)}.bg-surface-800\/20{background-color:rgb(var(--surface-800) / .2)}.bg-surface-800\/30{background-color:rgb(var(--surface-800) / .3)}.bg-surface-800\/50{background-color:rgb(var(--surface-800) / .5)}.bg-surface-900{--tw-bg-opacity:1;background-color:rgb(var(--surface-900) / var(--tw-bg-opacity,1))}.bg-surface-900\/30{background-color:rgb(var(--surface-900) / .3)}.bg-surface-900\/50{background-color:rgb(var(--surface-900) / .5)}.bg-surface-900\/80{background-color:rgb(var(--surface-900) / .8)}.bg-surface-950{--tw-bg-opacity:1;background-color:rgb(var(--surface-950) / var(--tw-bg-opacity,1))}.bg-surface-950\/80{background-color:rgb(var(--surface-950) / .8)}.bg-yellow-500\/80{background-color:#eab308cc}.bg-gradient-to-b{background-image:linear-gradient(to bottom, var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right, var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right, var(--tw-gradient-stops))}.from-accent-400{--tw-gradient-from:rgb(var(--accent-400) / 1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--accent-400) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-accent-500{--tw-gradient-from:rgb(var(--accent-500) / 1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--accent-500) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-accent-500\/10{--tw-gradient-from:rgb(var(--accent-500) / .1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--accent-500) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-accent-500\/20{--tw-gradient-from:rgb(var(--accent-500) / .2) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--accent-500) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-accent-500\/5{--tw-gradient-from:rgb(var(--accent-500) / .05) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--accent-500) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-surface-300{--tw-gradient-from:rgb(var(--surface-300) / 1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--surface-300) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-surface-400{--tw-gradient-from:rgb(var(--surface-400) / 1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--surface-400) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-surface-500{--tw-gradient-from:rgb(var(--surface-500) / 1) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--surface-500) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-surface-900\/30{--tw-gradient-from:rgb(var(--surface-900) / .3) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--surface-900) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-surface-900\/50{--tw-gradient-from:rgb(var(--surface-900) / .5) var(--tw-gradient-from-position);--tw-gradient-to:rgb(var(--surface-900) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:transparent var(--tw-gradient-from-position);--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.via-accent-500\/5{--tw-gradient-to:rgb(var(--accent-500) / 0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), rgb(var(--accent-500) / .05) var(--tw-gradient-via-position), var(--tw-gradient-to)}.via-transparent{--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), transparent var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-accent-600{--tw-gradient-to:rgb(var(--accent-600) / 1) var(--tw-gradient-to-position)}.to-accent-600\/10{--tw-gradient-to:rgb(var(--accent-600) / .1) var(--tw-gradient-to-position)}.to-accent-600\/20{--tw-gradient-to:rgb(var(--accent-600) / .2) var(--tw-gradient-to-position)}.to-accent-700{--tw-gradient-to:rgb(var(--accent-700) / 1) var(--tw-gradient-to-position)}.to-surface-100{--tw-gradient-to:rgb(var(--surface-100) / 1) var(--tw-gradient-to-position)}.to-surface-200{--tw-gradient-to:rgb(var(--surface-200) / 1) var(--tw-gradient-to-position)}.to-surface-300{--tw-gradient-to:rgb(var(--surface-300) / 1) var(--tw-gradient-to-position)}.to-surface-600{--tw-gradient-to:rgb(var(--surface-600) / 1) var(--tw-gradient-to-position)}.to-surface-700{--tw-gradient-to:rgb(var(--surface-700) / 1) var(--tw-gradient-to-position)}.to-surface-900\/30{--tw-gradient-to:rgb(var(--surface-900) / .3) var(--tw-gradient-to-position)}.to-surface-900\/50{--tw-gradient-to:rgb(var(--surface-900) / .5) var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.stroke-surface-100{stroke:rgb(var(--surface-100) / 1)}.stroke-surface-200{stroke:rgb(var(--surface-200) / 1)}.stroke-surface-300{stroke:rgb(var(--surface-300) / 1)}.p-1{padding:.25rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-3\.5{padding:.875rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-16{padding-bottom:4rem}.pl-10{padding-left:2.5rem}.pl-9{padding-left:2.25rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-24{padding-top:6rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:JetBrains Mono,Fira Code,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.text-accent-300{--tw-text-opacity:1;color:rgb(var(--accent-300) / var(--tw-text-opacity,1))}.text-accent-400{--tw-text-opacity:1;color:rgb(var(--accent-400) / var(--tw-text-opacity,1))}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-indigo-400{--tw-text-opacity:1;color:rgb(129 140 248/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-surface-100{--tw-text-opacity:1;color:rgb(var(--surface-100) / var(--tw-text-opacity,1))}.text-surface-200{--tw-text-opacity:1;color:rgb(var(--surface-200) / var(--tw-text-opacity,1))}.text-surface-300{--tw-text-opacity:1;color:rgb(var(--surface-300) / var(--tw-text-opacity,1))}.text-surface-400{--tw-text-opacity:1;color:rgb(var(--surface-400) / var(--tw-text-opacity,1))}.text-surface-50{--tw-text-opacity:1;color:rgb(var(--surface-50) / var(--tw-text-opacity,1))}.text-surface-500{--tw-text-opacity:1;color:rgb(var(--surface-500) / var(--tw-text-opacity,1))}.text-surface-600{--tw-text-opacity:1;color:rgb(var(--surface-600) / var(--tw-text-opacity,1))}.text-transparent{color:#0000}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.placeholder-surface-500::placeholder{--tw-placeholder-opacity:1;color:rgb(var(--surface-500) / var(--tw-placeholder-opacity,1))}.accent-accent-500{accent-color:rgb(var(--accent-500) / 1)}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.shadow-accent-500\/20{--tw-shadow-color:rgb(var(--accent-500) / .2);--tw-shadow:var(--tw-shadow-colored)}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow,0 0 #0000)}.blur-2xl{--tw-blur:blur(40px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-3xl{--tw-blur:blur(64px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-xl{--tw-blur:blur(24px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur:blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-property:all;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-property:transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}.text-balance{text-wrap:balance}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-accent-400:hover{--tw-border-opacity:1;border-color:rgb(var(--accent-400) / var(--tw-border-opacity,1))}.hover\:border-emerald-500:hover{--tw-border-opacity:1;border-color:rgb(16 185 129/var(--tw-border-opacity,1))}.hover\:border-surface-600:hover{--tw-border-opacity:1;border-color:rgb(var(--surface-600) / var(--tw-border-opacity,1))}.hover\:border-surface-600\/50:hover{border-color:rgb(var(--surface-600) / .5)}.hover\:border-surface-700\/60:hover{border-color:rgb(var(--surface-700) / .6)}.hover\:bg-accent-500:hover{--tw-bg-opacity:1;background-color:rgb(var(--accent-500) / var(--tw-bg-opacity,1))}.hover\:bg-accent-500\/10:hover{background-color:rgb(var(--accent-500) / .1)}.hover\:bg-amber-500\/20:hover{background-color:#f59e0b33}.hover\:bg-emerald-500\/20:hover{background-color:#10b98133}.hover\:bg-indigo-500\/20:hover{background-color:#6366f133}.hover\:bg-surface-700:hover{--tw-bg-opacity:1;background-color:rgb(var(--surface-700) / var(--tw-bg-opacity,1))}.hover\:bg-surface-800:hover{--tw-bg-opacity:1;background-color:rgb(var(--surface-800) / var(--tw-bg-opacity,1))}.hover\:bg-surface-800\/20:hover{background-color:rgb(var(--surface-800) / .2)}.hover\:bg-surface-800\/50:hover{background-color:rgb(var(--surface-800) / .5)}.hover\:text-accent-400:hover{--tw-text-opacity:1;color:rgb(var(--accent-400) / var(--tw-text-opacity,1))}.hover\:text-surface-200:hover{--tw-text-opacity:1;color:rgb(var(--surface-200) / var(--tw-text-opacity,1))}.hover\:text-surface-300:hover{--tw-text-opacity:1;color:rgb(var(--surface-300) / var(--tw-text-opacity,1))}.hover\:text-surface-50:hover{--tw-text-opacity:1;color:rgb(var(--surface-50) / var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:shadow-accent-500\/30:hover{--tw-shadow-color:rgb(var(--accent-500) / .3);--tw-shadow:var(--tw-shadow-colored)}.focus\:border-accent-500\/50:focus{border-color:rgb(var(--accent-500) / .5)}.focus\:outline-none:focus{outline-offset:2px;outline:2px solid #0000}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow,0 0 #0000)}.focus\:ring-accent-500\/20:focus{--tw-ring-color:rgb(var(--accent-500) / .2)}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}@media (width>=640px){.sm\:inset-auto{inset:auto}.sm\:left-1\/2{left:50%}.sm\:top-1\/2{top:50%}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:w-48{width:12rem}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-lg{max-width:32rem}.sm\:flex-initial{flex:0 auto}.sm\:-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-5xl{font-size:3rem;line-height:1}.sm\:text-6xl{font-size:3.75rem;line-height:1}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (width>=768px){.md\:col-span-4{grid-column:span 4/span 4}.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}}@media (width>=1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:p-8{padding:2rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:text-7xl{font-size:4.5rem;line-height:1}}
|