wacrawl-dashboard 0.1.1

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.
@@ -0,0 +1,388 @@
1
+ import { Router } from 'express';
2
+ import { getDb } from '../db.js';
3
+ import { cleanDisplayNameSql, contactDisplayNameSql, contactMatchSql } from '../lib/displayName.js';
4
+ import { bucketDateSql, normalizeBucketDate, parseGranularity, parseLimit, parsePeriod, parseYear, sinceTimestamp, unixSecondsToIso, yearBounds, } from '../lib/query.js';
5
+ import { hourInTimezone, resolveStatsTimeZone } from '../lib/timezone.js';
6
+ import { USEFUL_WORD_STOP_SET } from '../lib/wordCloudUsefulStopWords.js';
7
+ export function getOverviewStats(db = getDb()) {
8
+ const messageRow = db.prepare(`
9
+ SELECT
10
+ COUNT(*) AS totalMessages,
11
+ SUM(CASE WHEN media_type IS NOT NULL AND media_type <> '' THEN 1 ELSE 0 END) AS totalMediaFiles,
12
+ MIN(ts) AS oldestTs,
13
+ MAX(ts) AS newestTs
14
+ FROM messages
15
+ `).get();
16
+ const chatRow = db.prepare('SELECT COUNT(*) AS count FROM chats').get();
17
+ const contactRow = db.prepare('SELECT COUNT(*) AS count FROM contacts').get();
18
+ return {
19
+ totalMessages: messageRow.totalMessages,
20
+ totalChats: chatRow.count,
21
+ totalContacts: contactRow.count,
22
+ totalMediaFiles: messageRow.totalMediaFiles ?? 0,
23
+ oldestMessage: unixSecondsToIso(messageRow.oldestTs),
24
+ newestMessage: unixSecondsToIso(messageRow.newestTs),
25
+ };
26
+ }
27
+ export function getTopContacts(params, db = getDb()) {
28
+ const period = parsePeriod(params.period);
29
+ const limit = parseLimit(params.limit, 10, 50);
30
+ const since = sinceTimestamp(period);
31
+ const rows = db.prepare(`
32
+ WITH normalized_messages AS (
33
+ SELECT
34
+ COALESCE(
35
+ contacts.jid,
36
+ CASE
37
+ WHEN from_me = 1 THEN chat_jid
38
+ ELSE COALESCE(sender_jid, chat_jid)
39
+ END
40
+ ) AS jid,
41
+ ${contactDisplayNameSql('contacts')} AS contact_name,
42
+ contacts.phone AS contact_phone,
43
+ ${cleanDisplayNameSql(`
44
+ CASE
45
+ WHEN from_me = 1 THEN messages.chat_name
46
+ ELSE messages.sender_name
47
+ END
48
+ `)} AS message_name,
49
+ ${cleanDisplayNameSql(`
50
+ COALESCE(
51
+ chats.name,
52
+ CASE
53
+ WHEN messages.chat_jid = CASE
54
+ WHEN messages.from_me = 1 THEN messages.chat_jid
55
+ ELSE COALESCE(messages.sender_jid, messages.chat_jid)
56
+ END THEN messages.chat_name
57
+ ELSE NULL
58
+ END
59
+ )
60
+ `)} AS direct_chat_name,
61
+ from_me
62
+ FROM messages
63
+ LEFT JOIN chats ON chats.jid = CASE
64
+ WHEN messages.from_me = 1 THEN messages.chat_jid
65
+ ELSE COALESCE(messages.sender_jid, messages.chat_jid)
66
+ END
67
+ LEFT JOIN contacts ON ${contactMatchSql('contacts', `CASE
68
+ WHEN messages.from_me = 1 THEN messages.chat_jid
69
+ ELSE COALESCE(messages.sender_jid, messages.chat_jid)
70
+ END`)}
71
+ WHERE ts >= @since
72
+ )
73
+ SELECT
74
+ jid,
75
+ COALESCE(
76
+ MAX(message_name),
77
+ MAX(direct_chat_name),
78
+ MAX(contact_name),
79
+ ${cleanDisplayNameSql('jid')},
80
+ 'Unknown'
81
+ ) AS name,
82
+ MAX(contact_phone) AS phone,
83
+ COUNT(*) AS messageCount,
84
+ SUM(CASE WHEN from_me = 1 THEN 1 ELSE 0 END) AS sentByMe,
85
+ SUM(CASE WHEN from_me = 0 THEN 1 ELSE 0 END) AS sentByThem
86
+ FROM normalized_messages
87
+ WHERE jid IS NOT NULL AND jid <> ''
88
+ GROUP BY jid
89
+ ORDER BY messageCount DESC
90
+ LIMIT @limit
91
+ `).all({ since, limit });
92
+ return rows.map((row) => ({
93
+ jid: row.jid ?? 'unknown',
94
+ name: row.name ?? row.jid ?? 'Unknown',
95
+ phone: row.phone?.trim() ? row.phone.trim() : null,
96
+ messageCount: row.messageCount,
97
+ sentByMe: row.sentByMe ?? 0,
98
+ sentByThem: row.sentByThem ?? 0,
99
+ }));
100
+ }
101
+ export function getMessageVolume(params, db = getDb()) {
102
+ const period = parsePeriod(params.period);
103
+ const fallbackGranularity = period === 'year' || period === 'all' ? 'month' : 'day';
104
+ const granularity = parseGranularity(params.granularity, fallbackGranularity);
105
+ const since = sinceTimestamp(period);
106
+ const bucketSql = bucketDateSql(granularity);
107
+ const rows = db.prepare(`
108
+ SELECT
109
+ ${bucketSql} AS date,
110
+ SUM(CASE WHEN from_me = 1 THEN 1 ELSE 0 END) AS sent,
111
+ SUM(CASE WHEN from_me = 0 THEN 1 ELSE 0 END) AS received
112
+ FROM messages
113
+ WHERE ts >= @since
114
+ GROUP BY date
115
+ ORDER BY date ASC
116
+ `).all({ since });
117
+ return rows.map((row) => ({
118
+ date: normalizeBucketDate(row.date, granularity),
119
+ sent: row.sent ?? 0,
120
+ received: row.received ?? 0,
121
+ }));
122
+ }
123
+ export function getActivityHeatmap(params, db = getDb()) {
124
+ const year = parseYear(params.year);
125
+ const bounds = yearBounds(year);
126
+ return db.prepare(`
127
+ SELECT date(ts, 'unixepoch') AS date, COUNT(*) AS count
128
+ FROM messages
129
+ WHERE ts >= @start AND ts < @end
130
+ GROUP BY date
131
+ ORDER BY date ASC
132
+ `).all(bounds);
133
+ }
134
+ export function getHourOfDayStats(params, db = getDb()) {
135
+ const since = sinceTimestamp(parsePeriod(params.period));
136
+ const timeZone = resolveStatsTimeZone(params.timeZone);
137
+ const rows = db.prepare(`
138
+ SELECT ts
139
+ FROM messages
140
+ WHERE ts >= @since
141
+ `).all({ since });
142
+ const counts = new Array(24).fill(0);
143
+ for (const row of rows) {
144
+ counts[hourInTimezone(row.ts, timeZone)] += 1;
145
+ }
146
+ const out = [];
147
+ for (let hour = 0; hour < 24; hour += 1) {
148
+ const count = counts[hour] ?? 0;
149
+ if (count > 0)
150
+ out.push({ hour, count });
151
+ }
152
+ return out;
153
+ }
154
+ export function getDayOfWeekStats(params, db = getDb()) {
155
+ const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
156
+ const since = sinceTimestamp(parsePeriod(params.period));
157
+ const rows = db.prepare(`
158
+ SELECT strftime('%w', ts, 'unixepoch') AS day, COUNT(*) AS count
159
+ FROM messages
160
+ WHERE ts >= @since
161
+ GROUP BY day
162
+ ORDER BY day ASC
163
+ `).all({ since });
164
+ return rows.map((row) => {
165
+ const day = Number(row.day);
166
+ return { day, label: labels[day] ?? String(day), count: row.count };
167
+ });
168
+ }
169
+ export function getMediaBreakdown(params, db = getDb()) {
170
+ const since = sinceTimestamp(parsePeriod(params.period));
171
+ const rows = db.prepare(`
172
+ SELECT media_type AS mediaType, COUNT(*) AS count, COALESCE(SUM(media_size), 0) AS totalBytes
173
+ FROM messages
174
+ WHERE ts >= @since AND media_type IS NOT NULL AND media_type <> ''
175
+ GROUP BY media_type
176
+ ORDER BY count DESC, mediaType ASC
177
+ `).all({ since });
178
+ return rows.map((row) => ({
179
+ mediaType: row.mediaType,
180
+ count: row.count,
181
+ totalBytes: row.totalBytes ?? 0,
182
+ }));
183
+ }
184
+ export function getMediaSenders(params, db = getDb()) {
185
+ const since = sinceTimestamp(parsePeriod(params.period));
186
+ const limit = parseLimit(params.limit, 10, 50);
187
+ const rows = db.prepare(`
188
+ WITH normalized AS (
189
+ SELECT
190
+ CASE WHEN from_me = 1 THEN chat_jid ELSE COALESCE(sender_jid, chat_jid) END AS jid,
191
+ ${cleanDisplayNameSql('CASE WHEN from_me = 1 THEN COALESCE(chats.name, chat_name) ELSE sender_name END')} AS message_name,
192
+ ${contactDisplayNameSql('contacts')} AS contact_name,
193
+ media_size
194
+ FROM messages
195
+ LEFT JOIN chats ON chats.jid = CASE WHEN messages.from_me = 1 THEN messages.chat_jid ELSE COALESCE(messages.sender_jid, messages.chat_jid) END
196
+ LEFT JOIN contacts ON ${contactMatchSql('contacts', 'CASE WHEN messages.from_me = 1 THEN messages.chat_jid ELSE COALESCE(messages.sender_jid, messages.chat_jid) END')}
197
+ WHERE ts >= @since AND media_type IS NOT NULL AND media_type <> ''
198
+ )
199
+ SELECT
200
+ jid,
201
+ COALESCE(MAX(message_name), MAX(contact_name), ${cleanDisplayNameSql('jid')}, 'Unknown') AS name,
202
+ COUNT(*) AS mediaCount,
203
+ COALESCE(SUM(media_size), 0) AS totalBytes
204
+ FROM normalized
205
+ WHERE jid IS NOT NULL AND jid <> ''
206
+ GROUP BY jid
207
+ ORDER BY mediaCount DESC, name COLLATE NOCASE ASC
208
+ LIMIT @limit
209
+ `).all({ since, limit });
210
+ return rows.map((row) => ({
211
+ jid: row.jid ?? 'unknown',
212
+ name: row.name ?? row.jid ?? 'Unknown',
213
+ mediaCount: row.mediaCount,
214
+ totalBytes: row.totalBytes ?? 0,
215
+ }));
216
+ }
217
+ export function getSentReceivedRatio(params, db = getDb()) {
218
+ const since = sinceTimestamp(parsePeriod(params.period));
219
+ const rows = db.prepare(`
220
+ SELECT
221
+ strftime('%Y-%m-01', ts, 'unixepoch') AS month,
222
+ SUM(CASE WHEN from_me = 1 THEN 1 ELSE 0 END) AS sent,
223
+ SUM(CASE WHEN from_me = 0 THEN 1 ELSE 0 END) AS received
224
+ FROM messages
225
+ WHERE ts >= @since
226
+ GROUP BY month
227
+ ORDER BY month ASC
228
+ `).all({ since });
229
+ return rows.map((row) => ({
230
+ month: row.month,
231
+ sent: row.sent ?? 0,
232
+ received: row.received ?? 0,
233
+ ratio: row.received ? Number((row.sent / row.received).toFixed(2)) : null,
234
+ }));
235
+ }
236
+ export function getResponseTimes(params, db = getDb()) {
237
+ const since = sinceTimestamp(parsePeriod(params.period));
238
+ const limit = parseLimit(params.limit, 10, 50);
239
+ const rows = db.prepare(`
240
+ WITH ordered AS (
241
+ SELECT
242
+ chat_jid,
243
+ chat_name,
244
+ ts,
245
+ from_me,
246
+ LAG(ts) OVER (PARTITION BY chat_jid ORDER BY ts, rowid) AS previous_ts,
247
+ LAG(from_me) OVER (PARTITION BY chat_jid ORDER BY ts, rowid) AS previous_from_me
248
+ FROM messages
249
+ WHERE ts >= @since
250
+ )
251
+ SELECT
252
+ chat_jid AS jid,
253
+ COALESCE(${cleanDisplayNameSql('MAX(chats.name)')}, ${cleanDisplayNameSql('MAX(chat_name)')}, ${contactDisplayNameSql('chat_contacts')}, ${cleanDisplayNameSql('chat_jid')}, 'Unknown') AS name,
254
+ COUNT(*) AS responseCount,
255
+ ROUND(AVG(ts - previous_ts)) AS averageSeconds
256
+ FROM ordered
257
+ LEFT JOIN chats ON chats.jid = ordered.chat_jid
258
+ LEFT JOIN contacts AS chat_contacts ON ${contactMatchSql('chat_contacts', 'ordered.chat_jid')}
259
+ WHERE previous_ts IS NOT NULL
260
+ AND previous_from_me IS NOT NULL
261
+ AND previous_from_me <> from_me
262
+ AND ts > previous_ts
263
+ GROUP BY chat_jid
264
+ ORDER BY averageSeconds ASC
265
+ LIMIT @limit
266
+ `).all({ since, limit });
267
+ return rows.map((row) => ({
268
+ jid: row.jid,
269
+ name: row.name ?? row.jid,
270
+ responseCount: row.responseCount,
271
+ averageSeconds: row.averageSeconds ?? 0,
272
+ }));
273
+ }
274
+ export function getGroupActivity(params, db = getDb()) {
275
+ const since = sinceTimestamp(parsePeriod(params.period));
276
+ const limit = parseLimit(params.limit, 10, 50);
277
+ const rows = db.prepare(`
278
+ SELECT
279
+ chats.jid,
280
+ COALESCE(${cleanDisplayNameSql('chats.name')}, ${cleanDisplayNameSql('MAX(messages.chat_name)')}, ${cleanDisplayNameSql('chats.jid')}, 'Unknown') AS name,
281
+ COUNT(messages.rowid) AS messageCount,
282
+ COUNT(DISTINCT CASE WHEN messages.from_me = 0 THEN messages.sender_jid END) AS participantCount
283
+ FROM chats
284
+ JOIN messages ON messages.chat_jid = chats.jid
285
+ WHERE chats.kind = 'group' AND messages.ts >= @since
286
+ GROUP BY chats.jid
287
+ ORDER BY messageCount DESC, name COLLATE NOCASE ASC
288
+ LIMIT @limit
289
+ `).all({ since, limit });
290
+ return rows.map((row) => ({
291
+ jid: row.jid,
292
+ name: row.name ?? row.jid,
293
+ messageCount: row.messageCount,
294
+ participantCount: row.participantCount,
295
+ }));
296
+ }
297
+ export function getMessageStreaks(params, db = getDb()) {
298
+ const since = sinceTimestamp(parsePeriod(params.period));
299
+ const rows = db.prepare(`
300
+ SELECT DISTINCT date(ts, 'unixepoch') AS date
301
+ FROM messages
302
+ WHERE ts >= @since
303
+ ORDER BY date ASC
304
+ `).all({ since });
305
+ let longestStreak = 0;
306
+ let activeRun = 0;
307
+ let previousTime = null;
308
+ for (const row of rows) {
309
+ const time = Date.parse(`${row.date}T00:00:00.000Z`);
310
+ activeRun = previousTime != null && time - previousTime === 86_400_000 ? activeRun + 1 : 1;
311
+ longestStreak = Math.max(longestStreak, activeRun);
312
+ previousTime = time;
313
+ }
314
+ const today = new Date();
315
+ const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
316
+ const lastTime = rows.length ? Date.parse(`${rows[rows.length - 1].date}T00:00:00.000Z`) : null;
317
+ const currentStreak = lastTime != null && todayUtc - lastTime <= 86_400_000 ? activeRun : 0;
318
+ return { currentStreak, longestStreak };
319
+ }
320
+ function parseWordCloudFilter(value) {
321
+ return value === 'useful' ? 'useful' : 'all';
322
+ }
323
+ export function getWordCloud(params, db = getDb()) {
324
+ const since = sinceTimestamp(parsePeriod(params.period));
325
+ const limit = parseLimit(params.limit, 50, 200);
326
+ const filterMode = parseWordCloudFilter(params.filter);
327
+ const rows = db.prepare(`
328
+ SELECT text
329
+ FROM messages
330
+ WHERE ts >= @since AND text IS NOT NULL AND TRIM(text) <> ''
331
+ `).all({ since });
332
+ const counts = new Map();
333
+ for (const row of rows) {
334
+ for (const term of (row.text ?? '').toLowerCase().match(/[a-z0-9]{3,}/g) ?? []) {
335
+ if (filterMode === 'useful') {
336
+ if (/^\d+$/.test(term))
337
+ continue;
338
+ if (USEFUL_WORD_STOP_SET.has(term))
339
+ continue;
340
+ }
341
+ counts.set(term, (counts.get(term) ?? 0) + 1);
342
+ }
343
+ }
344
+ return [...counts.entries()]
345
+ .map(([text, value]) => ({ text, value }))
346
+ .sort((a, b) => b.value - a.value || a.text.localeCompare(b.text))
347
+ .slice(0, limit);
348
+ }
349
+ export const statsRouter = Router();
350
+ statsRouter.get('/overview', (_req, res) => {
351
+ res.json(getOverviewStats());
352
+ });
353
+ statsRouter.get('/top-contacts', (req, res) => {
354
+ res.json(getTopContacts(req.query));
355
+ });
356
+ statsRouter.get('/message-volume', (req, res) => {
357
+ res.json(getMessageVolume(req.query));
358
+ });
359
+ statsRouter.get('/activity-heatmap', (req, res) => {
360
+ res.json(getActivityHeatmap(req.query));
361
+ });
362
+ statsRouter.get('/hour-of-day', (req, res) => {
363
+ res.json(getHourOfDayStats(req.query));
364
+ });
365
+ statsRouter.get('/day-of-week', (req, res) => {
366
+ res.json(getDayOfWeekStats(req.query));
367
+ });
368
+ statsRouter.get('/media-breakdown', (req, res) => {
369
+ res.json(getMediaBreakdown(req.query));
370
+ });
371
+ statsRouter.get('/media-senders', (req, res) => {
372
+ res.json(getMediaSenders(req.query));
373
+ });
374
+ statsRouter.get('/sent-received-ratio', (req, res) => {
375
+ res.json(getSentReceivedRatio(req.query));
376
+ });
377
+ statsRouter.get('/response-times', (req, res) => {
378
+ res.json(getResponseTimes(req.query));
379
+ });
380
+ statsRouter.get('/group-activity', (req, res) => {
381
+ res.json(getGroupActivity(req.query));
382
+ });
383
+ statsRouter.get('/streaks', (req, res) => {
384
+ res.json(getMessageStreaks(req.query));
385
+ });
386
+ statsRouter.get('/word-cloud', (req, res) => {
387
+ res.json(getWordCloud(req.query));
388
+ });
@@ -0,0 +1,10 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { resolvePathsForRequest } from './pathsResolve.js';
3
+ export const requestPathsStore = new AsyncLocalStorage();
4
+ export function getResolvedPaths() {
5
+ return requestPathsStore.getStore() ?? resolvePathsForRequest({});
6
+ }
7
+ export function pathsMiddleware(req, _res, next) {
8
+ const merged = resolvePathsForRequest(req.headers);
9
+ requestPathsStore.run(merged, next);
10
+ }
@@ -0,0 +1,14 @@
1
+ import Database from 'better-sqlite3';
2
+ import { getDbPath } from '../db.js';
3
+ import { createSearchIndexes } from '../lib/searchIndex.js';
4
+ const dbPath = getDbPath();
5
+ const db = new Database(dbPath, { fileMustExist: true });
6
+ try {
7
+ const startedAt = Date.now();
8
+ createSearchIndexes(db);
9
+ const elapsedMs = Date.now() - startedAt;
10
+ console.log(`Optimized search indexes for ${dbPath} in ${elapsedMs}ms.`);
11
+ }
12
+ finally {
13
+ db.close();
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ *,:before,:after{--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: rgb(59 130 246 / .5);--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: }::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: rgb(59 130 246 / .5);--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-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings: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:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-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{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,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{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.left-0{left:0}.left-4{left:1rem}.right-5{right:1.25rem}.right-6{right:1.5rem}.top-0{top:0}.top-1\/2{top:50%}.top-5{top:1.25rem}.top-6{top:1.5rem}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.col-span-2{grid-column:span 2 / span 2}.mb-1{margin-bottom:.25rem}.mb-4{margin-bottom:1rem}.ml-\[220px\]{margin-left:220px}.ml-auto{margin-left:auto}.mr-auto{margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-16{height:4rem}.h-24{height:6rem}.h-3{height:.75rem}.h-32{height:8rem}.h-4{height:1rem}.h-40{height:10rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-56{height:14rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-72{height:18rem}.h-8{height:2rem}.h-80{height:20rem}.h-9{height:2.25rem}.h-full{height:100%}.max-h-56{max-height:14rem}.max-h-\[75vh\]{max-height:75vh}.max-h-\[calc\(100vh-180px\)\]{max-height:calc(100vh - 180px)}.max-h-full{max-height:100%}.min-h-72{min-height:18rem}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-16{width:4rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-\[220px\]{width:220px}.w-\[640px\]{width:640px}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-5xl{max-width:64rem}.max-w-\[75\%\]{max-width:75%}.max-w-\[90\%\]{max-width:90%}.max-w-\[calc\(100vw-4rem\)\]{max-width:calc(100vw - 4rem)}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-full{--tw-translate-y: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(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:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.grid-flow-col{grid-auto-flow:column}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.grid-cols-\[360px_1fr\]{grid-template-columns:360px 1fr}.grid-rows-7{grid-template-rows:repeat(7,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}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-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-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * 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))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-slate-100>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(241 245 249 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[2rem\]{border-radius:2rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-emerald-200{--tw-border-opacity: 1;border-color:rgb(167 243 208 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-slate-100{--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity, 1))}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.border-white\/10{border-color:#ffffff1a}.border-white\/15{border-color:#ffffff26}.border-white\/40{border-color:#fff6}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-brand-50{--tw-bg-opacity: 1;background-color:rgb(236 253 245 / var(--tw-bg-opacity, 1))}.bg-brand-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-brand-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-emerald-200{--tw-bg-opacity: 1;background-color:rgb(167 243 208 / var(--tw-bg-opacity, 1))}.bg-emerald-300{--tw-bg-opacity: 1;background-color:rgb(110 231 183 / var(--tw-bg-opacity, 1))}.bg-emerald-50{--tw-bg-opacity: 1;background-color:rgb(236 253 245 / var(--tw-bg-opacity, 1))}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity, 1))}.bg-emerald-700{--tw-bg-opacity: 1;background-color:rgb(4 120 87 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-slate-100\/90{background-color:#f1f5f9e6}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.bg-slate-50\/90{background-color:#f8fafce6}.bg-slate-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.bg-slate-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.bg-slate-950{--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity, 1))}.bg-slate-950\/70{background-color:#020617b3}.bg-slate-950\/75{background-color:#020617bf}.bg-slate-950\/80{background-color:#020617cc}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/10{background-color:#ffffff1a}.bg-white\/60{background-color:#fff9}.bg-white\/90{background-color:#ffffffe6}.bg-\[radial-gradient\(circle_at_top_left\,_rgba\(34\,197\,94\,0\.45\)\,_transparent_34\%\)\,radial-gradient\(circle_at_bottom_right\,_rgba\(14\,165\,233\,0\.35\)\,_transparent_30\%\)\]{background-image:radial-gradient(circle at top left,rgba(34,197,94,.45),transparent 34%),radial-gradient(circle at bottom right,rgba(14,165,233,.35),transparent 30%)}.fill-current{fill:currentColor}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.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-6{padding-left:1.5rem;padding-right:1.5rem}.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-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-4{padding-bottom:1rem}.pl-12{padding-left:3rem}.pr-4{padding-right:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.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-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-6{line-height:1.5rem}.leading-8{line-height:2rem}.leading-tight{line-height:1.25}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-brand-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-brand-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-red-900{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity, 1))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.text-slate-950{--tw-text-opacity: 1;color:rgb(2 6 23 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-white\/50{color:#ffffff80}.text-white\/55{color:#ffffff8c}.text-white\/60{color:#fff9}.text-white\/70{color:#ffffffb3}.text-white\/75{color:#ffffffbf}.text-white\/80{color:#fffc}.underline{text-decoration-line:underline}.decoration-brand-600\/40{text-decoration-color:#16a34a66}.underline-offset-2{text-underline-offset:2px}.opacity-70{opacity:.7}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px 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-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--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-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px 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-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--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)}.ring-brand-500{--tw-ring-opacity: 1;--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1))}.ring-slate-200{--tw-ring-opacity: 1;--tw-ring-color: rgb(226 232 240 / var(--tw-ring-opacity, 1))}.ring-slate-200\/80{--tw-ring-color: rgb(226 232 240 / .8)}.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{--tw-backdrop-blur: blur(8px);-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-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)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.\[content-visibility\:auto\]{content-visibility:auto}:root{color-scheme:light;font-family:Inter,ui-sans-serif,system-ui,sans-serif;background:#f8fafc}body{margin:0;min-width:1024px}*{box-sizing:border-box}.hover\:-translate-y-0\.5:hover{--tw-translate-y: -.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-brand-500:hover{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.hover\:border-slate-300:hover{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.hover\:bg-brand-500:hover{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-200:hover{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-200\/80:hover{background-color:#e2e8f0cc}.hover\:bg-slate-50:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.hover\:bg-white:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.hover\:bg-white\/10:hover{background-color:#ffffff1a}.hover\:bg-white\/20:hover{background-color:#fff3}.hover\:text-slate-700:hover{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.hover\:text-slate-950:hover{--tw-text-opacity: 1;color:rgb(2 6 23 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-brand-500:focus{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.focus\:bg-white:focus{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2: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(2px + 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-4: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(4px + 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-brand-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1))}.focus\:ring-brand-500\/10:focus{--tw-ring-color: rgb(34 197 94 / .1)}.focus\:ring-brand-500\/15:focus{--tw-ring-color: rgb(34 197 94 / .15)}.focus\:ring-brand-500\/40:focus{--tw-ring-color: rgb(34 197 94 / .4)}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-4:focus-visible{--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(4px + 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)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-60:disabled{opacity:.6}.dark\:divide-slate-800:is(.dark *)>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(30 41 59 / var(--tw-divide-opacity, 1))}.dark\:border-amber-900:is(.dark *){--tw-border-opacity: 1;border-color:rgb(120 53 15 / var(--tw-border-opacity, 1))}.dark\:border-emerald-900:is(.dark *){--tw-border-opacity: 1;border-color:rgb(6 78 59 / var(--tw-border-opacity, 1))}.dark\:border-slate-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.dark\:border-slate-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}.dark\:bg-amber-900\/60:is(.dark *){background-color:#78350f99}.dark\:bg-amber-950\/40:is(.dark *){background-color:#451a0366}.dark\:bg-brand-500:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.dark\:bg-brand-600\/20:is(.dark *){background-color:#16a34a33}.dark\:bg-emerald-950\/40:is(.dark *){background-color:#022c2266}.dark\:bg-slate-700:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-800\/80:is(.dark *){background-color:#1e293bcc}.dark\:bg-slate-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-950\/30:is(.dark *){background-color:#0206174d}.dark\:bg-slate-950\/40:is(.dark *){background-color:#02061766}.dark\:bg-slate-950\/90:is(.dark *){background-color:#020617e6}.dark\:text-amber-200:is(.dark *){--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.dark\:text-amber-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.dark\:text-amber-400:is(.dark *){--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.dark\:text-brand-50:is(.dark *){--tw-text-opacity: 1;color:rgb(236 253 245 / var(--tw-text-opacity, 1))}.dark\:text-slate-100:is(.dark *){--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity, 1))}.dark\:text-slate-200:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.dark\:text-slate-300:is(.dark *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.dark\:text-slate-400:is(.dark *){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.dark\:text-slate-50:is(.dark *){--tw-text-opacity: 1;color:rgb(248 250 252 / var(--tw-text-opacity, 1))}.dark\:text-slate-500:is(.dark *){--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.dark\:ring-slate-700:is(.dark *){--tw-ring-opacity: 1;--tw-ring-color: rgb(51 65 85 / var(--tw-ring-opacity, 1))}.dark\:hover\:border-brand-600:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(22 163 74 / var(--tw-border-opacity, 1))}.dark\:hover\:border-slate-600:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity, 1))}.dark\:hover\:bg-brand-600\/30:hover:is(.dark *){background-color:#16a34a4d}.dark\:hover\:bg-slate-700:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-slate-700\/80:hover:is(.dark *){background-color:#334155cc}.dark\:hover\:bg-slate-800:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.dark\:hover\:text-slate-200:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.dark\:hover\:text-white:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:focus\:bg-slate-900:focus:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}@media(min-width:640px){.sm\:max-h-72{max-height:18rem}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-\[140px_1fr\]{grid-template-columns:140px 1fr}.sm\:p-10{padding:2.5rem}.sm\:text-5xl{font-size:3rem;line-height:1}}@media(min-width:1024px){.lg\:grid-cols-\[1\.05fr_0\.95fr\]{grid-template-columns:1.05fr .95fr}}