feedkeeper 0.23.1 → 0.25.0
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/package.json +1 -1
- package/server/dist/api/imageResponse.js +15 -0
- package/server/dist/api/items.js +3 -3
- package/server/dist/api/v1/resources.js +2 -2
- package/server/dist/db/migrations/0033_full_text_jobs.sql +5 -0
- package/server/dist/feeds/archive.js +1 -1
- package/server/dist/feeds/articleWorkPool.js +147 -0
- package/server/dist/feeds/extractor.js +33 -9
- package/server/dist/feeds/fullText.js +57 -50
- package/server/dist/feeds/fullTextQueue.js +235 -32
- package/server/dist/feeds/imageCache.js +2 -1
- package/server/dist/feeds/inbox.js +8 -3
- package/server/dist/feeds/poller.js +2 -0
- package/server/dist/feeds/repository.js +5 -2
- package/server/dist/securityHeaders.js +20 -0
- package/server/dist/server.js +2 -9
- package/server/package.json +1 -1
- package/web/dist/assets/index-BprbGvrl.css +2 -0
- package/web/dist/assets/index-CqwqZaF0.js +32 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +30 -2
- package/web/dist/assets/index-B4e2oqbo.css +0 -2
- package/web/dist/assets/index-xx6TyOwB.js +0 -32
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { isCachedConsentSnippet } from "./extractor.js";
|
|
2
2
|
import { db } from "../db/index.js";
|
|
3
3
|
import { hasUserCapability } from "../auth/capabilities.js";
|
|
4
|
-
import { loadFullText } from "./fullText.js";
|
|
4
|
+
import { loadFullText, executeFullText } from "./fullText.js";
|
|
5
|
+
import { articleWorkPool, restartArticleWorkPool } from "./articleWorkPool.js";
|
|
6
|
+
import { sourceHost } from "./requestGate.js";
|
|
5
7
|
import { canAccessItem, findSubscriptionFullTextMode, findItemById } from "./repository.js";
|
|
6
8
|
/** Refillable durable queue, bounded independently of library size. */
|
|
7
9
|
export function scheduleFullTexts() {
|
|
@@ -18,6 +20,7 @@ export function scheduleFullTexts() {
|
|
|
18
20
|
FROM editions e, json_each(e.item_ids) ids
|
|
19
21
|
JOIN items i ON i.id = ids.value
|
|
20
22
|
WHERE e.user_id IN (SELECT value FROM json_each(?))
|
|
23
|
+
AND NOT EXISTS (SELECT 1 FROM subscriptions disabled WHERE disabled.user_id=e.user_id AND disabled.feed_id=i.feed_id AND disabled.full_text_mode='never')
|
|
21
24
|
AND e.status = 'active' AND e.expires_at > strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
22
25
|
AND i.link IS NOT NULL AND i.full_content_html IS NULL
|
|
23
26
|
AND i.extraction_status != 'feed_only'
|
|
@@ -30,6 +33,7 @@ export function scheduleFullTexts() {
|
|
|
30
33
|
FROM item_bookmarks b
|
|
31
34
|
JOIN items i ON i.id = b.item_id
|
|
32
35
|
WHERE b.user_id IN (SELECT value FROM json_each(?))
|
|
36
|
+
AND NOT EXISTS (SELECT 1 FROM subscriptions disabled WHERE disabled.user_id=b.user_id AND disabled.feed_id=i.feed_id AND disabled.full_text_mode='never')
|
|
33
37
|
AND i.link IS NOT NULL AND i.full_content_html IS NULL
|
|
34
38
|
AND i.extraction_status != 'feed_only'
|
|
35
39
|
AND (i.extraction_retry_at IS NULL OR i.extraction_retry_at <= strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
@@ -41,6 +45,7 @@ export function scheduleFullTexts() {
|
|
|
41
45
|
FROM item_notes n
|
|
42
46
|
JOIN items i ON i.id = n.item_id
|
|
43
47
|
WHERE n.user_id IN (SELECT value FROM json_each(?))
|
|
48
|
+
AND NOT EXISTS (SELECT 1 FROM subscriptions disabled WHERE disabled.user_id=n.user_id AND disabled.feed_id=i.feed_id AND disabled.full_text_mode='never')
|
|
44
49
|
AND i.link IS NOT NULL AND i.full_content_html IS NULL
|
|
45
50
|
AND i.extraction_status != 'feed_only'
|
|
46
51
|
AND (i.extraction_retry_at IS NULL OR i.extraction_retry_at <= strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
@@ -61,8 +66,8 @@ export function scheduleFullTexts() {
|
|
|
61
66
|
SELECT item_id AS id, user_id, MAX(priority) AS priority
|
|
62
67
|
FROM candidate_articles
|
|
63
68
|
GROUP BY item_id
|
|
64
|
-
ORDER BY priority DESC, created_at
|
|
65
|
-
const add = db.prepare("INSERT OR IGNORE INTO full_text_jobs(item_id, user_id, priority) VALUES (?, ?,
|
|
69
|
+
ORDER BY (priority + CAST((julianday('now')-julianday(created_at))*1440 / 60 AS INTEGER)) DESC, created_at ASC LIMIT ?`).all(JSON.stringify(eligible), JSON.stringify(eligible), JSON.stringify(eligible), JSON.stringify(eligible), room);
|
|
70
|
+
const add = db.prepare("INSERT OR IGNORE INTO full_text_jobs(item_id, user_id, priority, created_at) VALUES (?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))");
|
|
66
71
|
let count = 0;
|
|
67
72
|
db.transaction(() => {
|
|
68
73
|
for (const row of candidates)
|
|
@@ -81,10 +86,7 @@ export function enqueueArticleContents(userId, ids) {
|
|
|
81
86
|
if (unique.some(id => !canAccessItem(userId, id)))
|
|
82
87
|
throw new Error("item_not_found");
|
|
83
88
|
return db.transaction(() => {
|
|
84
|
-
const
|
|
85
|
-
if (count.n + unique.length > 1000)
|
|
86
|
-
throw new Error("queue_full");
|
|
87
|
-
const add = db.prepare("INSERT INTO full_text_jobs(item_id,user_id,priority) VALUES (?,?,4) ON CONFLICT(item_id) DO UPDATE SET priority = MAX(priority,4), user_id = excluded.user_id");
|
|
89
|
+
const add = db.prepare("INSERT INTO full_text_jobs(item_id,user_id,priority,created_at) VALUES (?,?,4,strftime('%Y-%m-%dT%H:%M:%fZ','now')) ON CONFLICT(item_id) DO UPDATE SET priority = MAX(priority,4), user_id = excluded.user_id");
|
|
88
90
|
const queued = [];
|
|
89
91
|
for (const id of unique) {
|
|
90
92
|
const item = findItemById(id);
|
|
@@ -92,13 +94,15 @@ export function enqueueArticleContents(userId, ids) {
|
|
|
92
94
|
continue;
|
|
93
95
|
if (item.extraction_retry_at && Date.parse(item.extraction_retry_at) > Date.now())
|
|
94
96
|
continue;
|
|
97
|
+
ensureRoom(id);
|
|
95
98
|
add.run(id, userId);
|
|
96
99
|
queued.push(id);
|
|
97
100
|
}
|
|
101
|
+
wake();
|
|
98
102
|
return queued;
|
|
99
103
|
})();
|
|
100
104
|
}
|
|
101
|
-
/** Synchronous consumers
|
|
105
|
+
/** Synchronous consumers join durable article jobs and wait for their results. */
|
|
102
106
|
export async function prepareArticleContents(userId, ids) {
|
|
103
107
|
if (ids.length > 50 || ids.some(id => !Number.isSafeInteger(id) || id <= 0))
|
|
104
108
|
throw new Error("invalid_source_selection");
|
|
@@ -119,41 +123,240 @@ export async function prepareArticleContents(userId, ids) {
|
|
|
119
123
|
}
|
|
120
124
|
return results;
|
|
121
125
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
const active = new Map();
|
|
127
|
+
const listeners = new Map();
|
|
128
|
+
let listenerCount = 0;
|
|
129
|
+
let timer;
|
|
130
|
+
let seedTimer;
|
|
131
|
+
let stopping = false;
|
|
132
|
+
let scheduled = false;
|
|
133
|
+
const startedAt = Date.now();
|
|
134
|
+
let completed = 0;
|
|
135
|
+
let failed = 0;
|
|
136
|
+
let consumerTimeouts = 0;
|
|
137
|
+
let attemptErrors = 0;
|
|
138
|
+
const errorCounts = {};
|
|
139
|
+
let retries = 0;
|
|
140
|
+
let throttled = 0;
|
|
141
|
+
let rejected = 0;
|
|
142
|
+
function ensureRoom(id) {
|
|
143
|
+
if (!db.prepare("SELECT 1 FROM full_text_jobs WHERE item_id = ?").get(id) &&
|
|
144
|
+
db.prepare("SELECT COUNT(*) n FROM full_text_jobs").get().n >= 1000)
|
|
145
|
+
throw new Error("queue_full");
|
|
146
|
+
}
|
|
147
|
+
/** Waiting HTTP consumers are bounded separately from the durable queue. Their deadline never deletes work. */
|
|
148
|
+
export function requestFullText(user, id, force) {
|
|
149
|
+
if (stopping || listenerCount >= 64) {
|
|
150
|
+
rejected++;
|
|
151
|
+
return Promise.resolve({ ok: false, error: "queue_full" });
|
|
152
|
+
}
|
|
127
153
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
ensureRoom(id);
|
|
155
|
+
db.prepare(`INSERT INTO full_text_jobs(item_id,user_id,priority,force,created_at) VALUES (?,?,5,?,?)
|
|
156
|
+
ON CONFLICT(item_id) DO UPDATE SET user_id=excluded.user_id, priority=MAX(priority,5), force=MAX(force,excluded.force)`)
|
|
157
|
+
.run(id, user, Number(force), new Date().toISOString());
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
rejected++;
|
|
161
|
+
if (error instanceof Error && error.message === "queue_full")
|
|
162
|
+
return Promise.resolve({ ok: false, error: "queue_full" });
|
|
163
|
+
console.error(JSON.stringify({ event: "fulltext_queue_error" }));
|
|
164
|
+
return Promise.resolve({ ok: false, error: "extraction_failed" });
|
|
165
|
+
}
|
|
166
|
+
return new Promise(resolve => {
|
|
167
|
+
const entry = { user, resolve, timer: setTimeout(() => {
|
|
168
|
+
listeners.get(id)?.delete(entry);
|
|
169
|
+
listenerCount--;
|
|
170
|
+
consumerTimeouts++;
|
|
171
|
+
if (!listeners.get(id)?.size)
|
|
172
|
+
listeners.delete(id);
|
|
173
|
+
resolve({ ok: false, error: "timeout" });
|
|
174
|
+
}, 30_000) };
|
|
175
|
+
const pending = listeners.get(id) ?? new Set();
|
|
176
|
+
pending.add(entry);
|
|
177
|
+
listeners.set(id, pending);
|
|
178
|
+
listenerCount++;
|
|
179
|
+
wake();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
export function authorizedFullTextRequester(id, preferred) {
|
|
183
|
+
const owner = db.prepare("SELECT user_id FROM full_text_jobs WHERE item_id=?").get(id)?.user_id;
|
|
184
|
+
const candidates = new Set([owner, preferred, ...[...(listeners.get(id) ?? [])].map(entry => entry.user)]);
|
|
185
|
+
return [...candidates].find(user => user !== undefined && canAccessItem(user, id) && hasUserCapability(user, "fulltext") && findSubscriptionFullTextMode(user, findItemById(id).feed_id) !== "never");
|
|
186
|
+
}
|
|
187
|
+
function finishListeners(id, result) {
|
|
188
|
+
const pending = listeners.get(id);
|
|
189
|
+
listeners.delete(id);
|
|
190
|
+
for (const entry of pending ?? []) {
|
|
191
|
+
clearTimeout(entry.timer);
|
|
192
|
+
listenerCount--;
|
|
193
|
+
const allowed = canAccessItem(entry.user, id) && hasUserCapability(entry.user, "fulltext");
|
|
194
|
+
entry.resolve(allowed ? result : { ok: false, error: "capability_not_available" });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function wake() {
|
|
198
|
+
if (stopping || scheduled)
|
|
199
|
+
return;
|
|
200
|
+
scheduled = true;
|
|
201
|
+
queueMicrotask(() => {
|
|
202
|
+
scheduled = false;
|
|
203
|
+
try {
|
|
204
|
+
pump();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
console.error(JSON.stringify({ event: "fulltext_queue_error" }));
|
|
208
|
+
if (!stopping) {
|
|
209
|
+
timer = setTimeout(wake, 1000);
|
|
210
|
+
timer.unref();
|
|
136
211
|
}
|
|
137
|
-
|
|
138
|
-
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
export function configureFullTextPool() {
|
|
216
|
+
const pool = articleWorkPool();
|
|
217
|
+
pool.cooldown = host => Date.parse(db.prepare("SELECT retry_at FROM full_text_hosts WHERE host=?").get(host)?.retry_at ?? "") || 0;
|
|
218
|
+
return pool;
|
|
219
|
+
}
|
|
220
|
+
export function deferFullTextHost(host, retryAt) {
|
|
221
|
+
const previous = db.prepare("SELECT retry_at FROM full_text_hosts WHERE host=?").get(host);
|
|
222
|
+
const until = Math.max(Date.now() + 1000, retryAt ?? 0, Date.parse(previous?.retry_at ?? "") || 0);
|
|
223
|
+
db.prepare("INSERT INTO full_text_hosts(host,retry_at) VALUES (?,?) ON CONFLICT(host) DO UPDATE SET retry_at=excluded.retry_at")
|
|
224
|
+
.run(host, new Date(until).toISOString());
|
|
225
|
+
}
|
|
226
|
+
function pump() {
|
|
227
|
+
if (stopping)
|
|
228
|
+
return;
|
|
229
|
+
if (timer)
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
const pool = configureFullTextPool();
|
|
232
|
+
if (seedTimer && db.prepare("SELECT COUNT(*) n FROM full_text_jobs").get().n < pool.limits.concurrency * 2)
|
|
233
|
+
scheduleFullTexts();
|
|
234
|
+
const jobs = db.prepare(`SELECT j.*, i.link FROM full_text_jobs j JOIN items i ON i.id=j.item_id
|
|
235
|
+
WHERE julianday(j.available_at) <= julianday(?) ORDER BY (j.priority + CAST((julianday('now')-julianday(j.created_at))*86400000 / ? AS INTEGER)) DESC,
|
|
236
|
+
j.created_at, j.item_id LIMIT 1000`).all(new Date().toISOString(), pool.limits.agingMs);
|
|
237
|
+
for (const job of jobs) {
|
|
238
|
+
if (active.size >= pool.limits.concurrency)
|
|
239
|
+
break;
|
|
240
|
+
if (active.has(job.item_id))
|
|
241
|
+
continue;
|
|
242
|
+
let host;
|
|
243
|
+
try {
|
|
244
|
+
host = sourceHost(job.link ?? "");
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
db.prepare("DELETE FROM full_text_jobs WHERE item_id=?").run(job.item_id);
|
|
248
|
+
finishListeners(job.item_id, { ok: false, error: "item_has_no_link" });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const cooldown = db.prepare("SELECT retry_at FROM full_text_hosts WHERE host=?").get(host);
|
|
252
|
+
if (cooldown && Date.parse(cooldown.retry_at) > Date.now()) {
|
|
253
|
+
db.prepare("UPDATE full_text_jobs SET available_at=? WHERE item_id=?").run(cooldown.retry_at, job.item_id);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!pool.canStart(host))
|
|
257
|
+
continue;
|
|
258
|
+
const work = execute(job, host).finally(() => { active.delete(job.item_id); wake(); });
|
|
259
|
+
active.set(job.item_id, work);
|
|
260
|
+
}
|
|
261
|
+
if (db.prepare("SELECT COUNT(*) n FROM full_text_jobs").get().n) {
|
|
262
|
+
timer = setTimeout(wake, 250);
|
|
263
|
+
timer.unref();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function execute(job, host) {
|
|
267
|
+
try {
|
|
268
|
+
const result = await executeFullText(job.user_id, job.item_id, !!job.force, job.priority);
|
|
269
|
+
if (stopping)
|
|
270
|
+
return; // The SQLite row is the restart record, including its original age.
|
|
271
|
+
if (!result.ok) {
|
|
272
|
+
attemptErrors++;
|
|
273
|
+
errorCounts[result.error] = (errorCounts[result.error] ?? 0) + 1;
|
|
274
|
+
}
|
|
275
|
+
if (!result.ok && result.error === "source_busy") {
|
|
276
|
+
throttled++;
|
|
277
|
+
deferFullTextHost(result.host ?? host, result.retryAt);
|
|
278
|
+
}
|
|
279
|
+
if (!result.ok && result.retryable && (result.deferred || job.attempts < 2)) {
|
|
280
|
+
const delay = Math.min(60_000, 1000 * 2 ** job.attempts) * (0.5 + Math.random() * 0.5);
|
|
281
|
+
const retryAt = Math.max(Date.now() + delay, result.retryAt ?? 0);
|
|
282
|
+
db.transaction(() => {
|
|
283
|
+
db.prepare("UPDATE full_text_jobs SET attempts=attempts+?,available_at=? WHERE item_id=?").run(result.deferred ? 0 : 1, new Date(retryAt).toISOString(), job.item_id);
|
|
284
|
+
db.prepare("UPDATE items SET extraction_status='pending',extraction_retry_at=NULL WHERE id=?").run(job.item_id);
|
|
285
|
+
})();
|
|
286
|
+
retries++;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (!result.ok && result.retryable) {
|
|
290
|
+
const retryAt = Math.max(Date.now() + 3600_000, result.retryAt ?? 0);
|
|
291
|
+
db.prepare("UPDATE items SET extraction_status=?,extraction_retry_at=? WHERE id=?")
|
|
292
|
+
.run(result.error === "source_busy" ? "bot_blocked" : "failed", new Date(retryAt).toISOString(), job.item_id);
|
|
293
|
+
}
|
|
294
|
+
db.prepare("DELETE FROM full_text_jobs WHERE item_id=?").run(job.item_id);
|
|
295
|
+
completed++;
|
|
296
|
+
if (!result.ok)
|
|
297
|
+
failed++;
|
|
298
|
+
finishListeners(job.item_id, result);
|
|
139
299
|
}
|
|
140
|
-
|
|
141
|
-
|
|
300
|
+
catch {
|
|
301
|
+
// A database failure retains the restart record and fails the current waiters explicitly.
|
|
302
|
+
finishListeners(job.item_id, { ok: false, error: "extraction_failed" });
|
|
303
|
+
console.error(JSON.stringify({ event: "fulltext_queue_error" }));
|
|
304
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
142
305
|
}
|
|
143
306
|
}
|
|
307
|
+
export function fullTextMetrics() {
|
|
308
|
+
const queued = db.prepare("SELECT COUNT(*) n FROM full_text_jobs").get().n;
|
|
309
|
+
const oldest = db.prepare("SELECT MIN(created_at) oldest FROM full_text_jobs").get().oldest;
|
|
310
|
+
const work = articleWorkPool().metrics();
|
|
311
|
+
return { queued, waiters: listenerCount, ...work, completed, failed, attemptErrors, errorCounts: { ...errorCounts }, consumerTimeouts, retries, throttled, rejected,
|
|
312
|
+
meanDurationMs: work.completed ? work.durationMs / work.completed : 0,
|
|
313
|
+
throughputPerSecond: completed / Math.max(1, (Date.now() - startedAt) / 1000),
|
|
314
|
+
oldestWaitMs: oldest ? Math.max(0, Date.now() - Date.parse(oldest)) : 0 };
|
|
315
|
+
}
|
|
316
|
+
export async function runFullTextQueue() {
|
|
317
|
+
if (stopping)
|
|
318
|
+
return;
|
|
319
|
+
if (db.prepare("SELECT COUNT(*) n FROM full_text_jobs").get().n < 200)
|
|
320
|
+
scheduleFullTexts();
|
|
321
|
+
pump();
|
|
322
|
+
await Promise.all(active.values());
|
|
323
|
+
}
|
|
144
324
|
export function startFullTextScheduler() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
325
|
+
if (seedTimer)
|
|
326
|
+
return seedTimer;
|
|
327
|
+
stopping = false;
|
|
328
|
+
restartArticleWorkPool();
|
|
329
|
+
// Older versions can insert rows without the added age column after a rollback.
|
|
330
|
+
db.prepare("UPDATE full_text_jobs SET created_at=available_at WHERE created_at=''").run();
|
|
331
|
+
const tick = () => {
|
|
332
|
+
db.prepare("DELETE FROM full_text_hosts WHERE julianday(retry_at) <= julianday(?)").run(new Date().toISOString());
|
|
333
|
+
scheduleFullTexts();
|
|
334
|
+
wake();
|
|
335
|
+
console.log(JSON.stringify({ event: "fulltext_queue", ...fullTextMetrics() }));
|
|
336
|
+
};
|
|
337
|
+
// Repair old consent-only caches once; the normal bounded planner performs network work.
|
|
338
|
+
const cached = db.prepare("SELECT id,full_content_html FROM items WHERE full_content_html IS NOT NULL AND (lower(full_content_html) LIKE '%cookie%' OR lower(full_content_html) LIKE '%consent%') LIMIT 100").all();
|
|
148
339
|
db.transaction(() => {
|
|
149
340
|
for (const item of cached)
|
|
150
341
|
if (isCachedConsentSnippet(item.full_content_html)) {
|
|
151
|
-
db.prepare("UPDATE items SET full_content_html
|
|
342
|
+
db.prepare("UPDATE items SET full_content_html=NULL,extraction_status='pending',extraction_retry_at=NULL WHERE id=?").run(item.id);
|
|
152
343
|
}
|
|
153
344
|
})();
|
|
154
345
|
tick();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return
|
|
346
|
+
seedTimer = setInterval(tick, 15_000);
|
|
347
|
+
seedTimer.unref();
|
|
348
|
+
return seedTimer;
|
|
349
|
+
}
|
|
350
|
+
export async function stopFullTextScheduler() {
|
|
351
|
+
stopping = true;
|
|
352
|
+
if (timer)
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
if (seedTimer)
|
|
355
|
+
clearInterval(seedTimer);
|
|
356
|
+
seedTimer = undefined;
|
|
357
|
+
for (const id of listeners.keys())
|
|
358
|
+
finishListeners(id, { ok: false, error: "queue_full" });
|
|
359
|
+
await articleWorkPool().close();
|
|
360
|
+
await Promise.all(active.values());
|
|
158
361
|
}
|
|
159
362
|
//# sourceMappingURL=fullTextQueue.js.map
|
|
@@ -163,7 +163,7 @@ export class ImageCache {
|
|
|
163
163
|
if (buffer.length === entry.size && type) {
|
|
164
164
|
entry.used = this.now();
|
|
165
165
|
this.counters.hits++;
|
|
166
|
-
return { buffer, mime: type.mime, path: this.file(key) };
|
|
166
|
+
return { buffer, mime: type.mime, path: this.file(key), cacheTtlMs: Math.max(0, entry.expires - this.now()) };
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
catch { /* Re-fetch evicted or damaged files through the guarded downloader. */ }
|
|
@@ -182,6 +182,7 @@ export class ImageCache {
|
|
|
182
182
|
}
|
|
183
183
|
const result = { buffer: image.buffer, mime: type.mime, path: image.path };
|
|
184
184
|
const ttl = Math.min(this.settings.ttlMs, image.cacheTtlMs ?? this.settings.ttlMs);
|
|
185
|
+
result.cacheTtlMs = ttl;
|
|
185
186
|
if (this.diskAvailable && ttl > 0 && image.buffer.length <= this.settings.maxBytes) {
|
|
186
187
|
try {
|
|
187
188
|
await this.serialize(async () => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { db } from "../db/index.js";
|
|
3
|
+
import { hasUserCapability } from "../auth/capabilities.js";
|
|
4
|
+
import { configureFullTextPool, deferFullTextHost } from "./fullTextQueue.js";
|
|
3
5
|
import { extractArticle } from "./extractor.js";
|
|
4
6
|
import { assertPublicHttpUrl, normalizeUrlCandidate } from "./ssrfGuard.js";
|
|
5
7
|
import { articleImageUrls, scheduleArchive } from "./archive.js";
|
|
@@ -46,9 +48,12 @@ export async function saveToInbox(userId, input) {
|
|
|
46
48
|
const normalized = normalizeUrlCandidate(input.url.trim());
|
|
47
49
|
await assertPublicHttpUrl(normalized);
|
|
48
50
|
targetUrl = normalized;
|
|
49
|
-
if (!contentHtml) {
|
|
51
|
+
if (!contentHtml && hasUserCapability(userId, "fulltext")) {
|
|
50
52
|
try {
|
|
51
|
-
|
|
53
|
+
configureFullTextPool();
|
|
54
|
+
const extracted = await extractArticle(targetUrl, undefined, 5, () => hasUserCapability(userId, "fulltext"));
|
|
55
|
+
if (extracted.status === "throttled")
|
|
56
|
+
deferFullTextHost(extracted.host, extracted.retryAt);
|
|
52
57
|
if (extracted.status === "ok" || extracted.status === "partial") {
|
|
53
58
|
extractionStatus = extracted.status === "ok" ? "ready" : "partial";
|
|
54
59
|
extractedPartial = extracted.status === "partial";
|
|
@@ -62,7 +67,7 @@ export async function saveToInbox(userId, input) {
|
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
catch (err) {
|
|
65
|
-
console.warn(
|
|
70
|
+
console.warn(JSON.stringify({ event: "inbox_extraction_failed", error: err instanceof Error ? err.name : "unknown" }));
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
}
|
|
@@ -197,6 +197,8 @@ export async function stopPollingScheduler() {
|
|
|
197
197
|
clearInterval(scheduler);
|
|
198
198
|
scheduler = undefined;
|
|
199
199
|
const { stopImageCache } = await import("./archive.js");
|
|
200
|
+
const { stopFullTextScheduler } = await import("./fullTextQueue.js");
|
|
201
|
+
await stopFullTextScheduler();
|
|
200
202
|
await stopImageCache();
|
|
201
203
|
await Promise.all([queue?.stop(), stopPushOutbox()]);
|
|
202
204
|
feedRequests.stop();
|
|
@@ -650,7 +650,10 @@ export function searchItemsForUser(userId, opts) {
|
|
|
650
650
|
if (opts.sort !== "date" && fts) {
|
|
651
651
|
orderBy = "COALESCE(fts.rank, 0) ASC, COALESCE(julianday(i.published_at), julianday(i.created_at)) DESC";
|
|
652
652
|
}
|
|
653
|
+
const ranked = opts.sort !== "date" && fts;
|
|
654
|
+
// Materialize once: a virtual-table MATCH in the LEFT JOIN repeats the full ranking scan for each article.
|
|
653
655
|
const querySql = `
|
|
656
|
+
${ranked ? "WITH ranked_matches AS MATERIALIZED (SELECT rowid, rank FROM items_fts WHERE items_fts MATCH ?)" : ""}
|
|
654
657
|
SELECT DISTINCT i.*,
|
|
655
658
|
COALESCE(NULLIF(TRIM(s.label), ''), NULLIF(TRIM(f.title), ''), f.url) AS feed_title,
|
|
656
659
|
f.site_url AS feed_site_url,
|
|
@@ -666,17 +669,17 @@ export function searchItemsForUser(userId, opts) {
|
|
|
666
669
|
LEFT JOIN item_reads r ON r.item_id = i.id AND r.user_id = ?
|
|
667
670
|
LEFT JOIN item_bookmarks b ON b.item_id = i.id AND b.user_id = ?
|
|
668
671
|
LEFT JOIN item_notes n ON n.item_id = i.id AND n.user_id = ?
|
|
669
|
-
${
|
|
672
|
+
${ranked ? "LEFT JOIN ranked_matches fts ON fts.rowid = i.id" : ""}
|
|
670
673
|
WHERE ${conditions.join(" AND ")}
|
|
671
674
|
ORDER BY ${orderBy}
|
|
672
675
|
LIMIT ? OFFSET ?
|
|
673
676
|
`;
|
|
674
677
|
const queryParams = [
|
|
678
|
+
...(ranked ? [fts] : []),
|
|
675
679
|
userId,
|
|
676
680
|
userId,
|
|
677
681
|
userId,
|
|
678
682
|
userId,
|
|
679
|
-
...(opts.sort !== "date" && fts ? [fts] : []),
|
|
680
683
|
...params,
|
|
681
684
|
limit,
|
|
682
685
|
offset,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import helmet from "helmet";
|
|
2
|
+
/**
|
|
3
|
+
* Upgrading every request to https is only right when the instance is served over https. On a plain http address
|
|
4
|
+
* (localhost, a home network) Safari would upgrade scripts and images to an https origin that does not exist.
|
|
5
|
+
*/
|
|
6
|
+
export function securityHeaderOptions(publicUrl) {
|
|
7
|
+
const servedOverHttps = new URL(publicUrl).protocol === "https:";
|
|
8
|
+
return {
|
|
9
|
+
contentSecurityPolicy: {
|
|
10
|
+
directives: {
|
|
11
|
+
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
|
12
|
+
"script-src": ["'self'"],
|
|
13
|
+
"img-src": ["'self'", "data:", "https:", "http:"],
|
|
14
|
+
"upgrade-insecure-requests": servedOverHttps ? [] : null,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
strictTransportSecurity: servedOverHttps,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=securityHeaders.js.map
|
package/server/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import express from "express";
|
|
5
5
|
import helmet from "helmet";
|
|
6
|
+
import { securityHeaderOptions } from "./securityHeaders.js";
|
|
6
7
|
import cors from "cors";
|
|
7
8
|
import cookieSession from "cookie-session";
|
|
8
9
|
import rateLimit from "express-rate-limit";
|
|
@@ -42,15 +43,7 @@ if (config.clientIpHeader && !Array.isArray(config.trustProxy)) {
|
|
|
42
43
|
}
|
|
43
44
|
// Before every rate limiter, so limits count the CDN's client rather than its edge.
|
|
44
45
|
app.use(clientAddressFromHeader(config.clientIpHeader, config.trustProxy));
|
|
45
|
-
app.use(helmet(
|
|
46
|
-
contentSecurityPolicy: {
|
|
47
|
-
directives: {
|
|
48
|
-
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
|
49
|
-
"script-src": ["'self'"],
|
|
50
|
-
"img-src": ["'self'", "data:", "https:", "http:"],
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
}));
|
|
46
|
+
app.use(helmet(securityHeaderOptions(config.publicUrl)));
|
|
54
47
|
// Send X-Robots-Tag to ensure all HTTP endpoints disallow indexing even outside HTML
|
|
55
48
|
app.use((_req, res, next) => {
|
|
56
49
|
res.setHeader("X-Robots-Tag", "noindex, nofollow");
|
package/server/package.json
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-outline-style:solid;--tw-content:""}}}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/inter-latin-400-normal-C38fXH4l.woff2)format("woff2"),url(/assets/inter-latin-400-normal-CyCys3Eg.woff)format("woff")}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/inter-latin-500-normal-Cerq10X2.woff2)format("woff2"),url(/assets/inter-latin-500-normal-BL9OpVg8.woff)format("woff")}@font-face{font-family:Inter;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/inter-latin-600-normal-LgqL8muc.woff2)format("woff2"),url(/assets/inter-latin-600-normal-CiBQ2DWP.woff)format("woff")}@font-face{font-family:Manrope;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/manrope-latin-600-normal-4f0koTD-.woff2)format("woff2"),url(/assets/manrope-latin-600-normal-BqgrALkZ.woff)format("woff")}@font-face{font-family:Manrope;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/manrope-latin-700-normal-BZp_XxE4.woff2)format("woff2"),url(/assets/manrope-latin-700-normal-DGRFkw-m.woff)format("woff")}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-amber-500:oklch(76.9% .188 70.08);--color-emerald-500:oklch(69.6% .17 162.48);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--radius-xs:.125rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-xs:4px;--blur-md:12px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);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}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components{.brand-lockup{--m:28px;align-items:flex-start;gap:calc(var(--m) * .375);color:inherit;display:inline-flex}.brand-lockup img{width:var(--m);height:var(--m);flex:none}.brand-lockup .wordmark{width:auto;height:calc(var(--m) * .803);margin-top:calc(var(--m) * .151);fill:currentColor;flex:none;display:block}.card{background-color:var(--c-surface);border:1px solid var(--c-border);border-radius:.75rem}.btn-primary{background-color:var(--c-blue1);color:#fdfdfd;cursor:pointer;border-radius:.5rem;padding:.5rem 1rem;font-weight:500;transition:opacity .15s}.btn-primary:hover{opacity:.9}.btn-primary:disabled{opacity:.5;cursor:not-allowed}.btn-secondary{color:var(--c-text);border:1px solid var(--c-border);cursor:pointer;background-color:#0000;border-radius:.5rem;padding:.5rem 1rem;font-weight:500;transition:background-color .15s,opacity .15s}.btn-secondary:hover{background-color:var(--c-border)}.btn-secondary:disabled{opacity:.5;cursor:not-allowed}.input{background-color:var(--c-surface);border:1px solid var(--c-border);min-height:38px;color:var(--c-text);border-radius:.5rem;width:100%;padding:.375rem .75rem;font-size:1rem;line-height:1.5rem;transition:border-color .15s;box-shadow:0 1px 2px #0000000d}@media (width>=640px){.input{font-size:.875rem;line-height:1.25rem}}.input:hover{border-color:var(--c-blue3)}.input:focus{border-color:var(--c-blue3);outline:none}.input::placeholder{color:var(--c-text-muted);opacity:1}.btn-danger-circle{color:#fff;background-color:#7c2d12;transition:background-color .15s,opacity .15s}.btn-danger-circle:hover{background-color:#9a3412}.text-danger{color:var(--c-danger)}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-x-0{inset-inline:0}.inset-y-0{inset-block:0}.inset-y-0\.5{inset-block:calc(var(--spacing) * .5)}.inset-y-1{inset-block:var(--spacing)}.-top-px{top:-1px}.top-0{top:0}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-1\/2{top:50%}.top-24{top:calc(var(--spacing) * 24)}.top-\[1\.4rem\]{top:1.4rem}.top-full{top:100%}.right-0{right:0}.right-3{right:calc(var(--spacing) * 3)}.bottom-0{bottom:0}.bottom-1\.5{bottom:calc(var(--spacing) * 1.5)}.bottom-\[calc\(6\.5rem\+env\(safe-area-inset-bottom\,0px\)\)\]{bottom:calc(6.5rem + env(safe-area-inset-bottom,0px))}.bottom-full{bottom:100%}.left-0{left:0}.left-0\.5{left:calc(var(--spacing) * .5)}.left-1{left:var(--spacing)}.left-1\.5{left:calc(var(--spacing) * 1.5)}.left-1\/2{left:50%}.left-3{left:calc(var(--spacing) * 3)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[60\]{z-index:60}.order-first{order:-9999}.float-right{float:right}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.-mx-4{margin-inline:calc(var(--spacing) * -4)}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.-mt-4{margin-top:calc(var(--spacing) * -4)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-auto{margin-top:auto}.-mr-1{margin-right:calc(var(--spacing) * -1)}.-mr-2{margin-right:calc(var(--spacing) * -2)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.-ml-1{margin-left:calc(var(--spacing) * -1)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-9\.5{margin-left:calc(var(--spacing) * 9.5)}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-4{-webkit-line-clamp:4;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.aspect-\[3\/2\]{aspect-ratio:3/2}.aspect-\[16\/9\]{aspect-ratio:16/9}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-48{height:calc(var(--spacing) * 48)}.h-\[38px\]{height:38px}.h-\[54px\]{height:54px}.h-\[100dvh\]{height:100dvh}.h-auto{height:auto}.h-full{height:100%}.max-h-60{max-height:calc(var(--spacing) * 60)}.max-h-\[380px\]{max-height:380px}.min-h-0{min-height:0}.min-h-\[30px\]{min-height:30px}.min-h-\[38px\]{min-height:38px}.min-h-\[45vh\]{min-height:45vh}.min-h-screen{min-height:100vh}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-28{width:calc(var(--spacing) * 28)}.w-44{width:calc(var(--spacing) * 44)}.w-64{width:calc(var(--spacing) * 64)}.w-72{width:calc(var(--spacing) * 72)}.w-\[38px\]{width:38px}.w-\[220px\]{width:220px}.w-\[calc\(\(100\%-0\.5rem\)\/2\)\]{width:calc(50% - .25rem)}.w-\[calc\(100\%\+2rem\)\]{width:calc(100% + 2rem)}.w-fit{width:fit-content}.w-full{width:100%}.w-screen{width:100vw}.max-w-5xl{max-width:var(--container-5xl)}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-md{max-width:var(--container-md)}.max-w-prose{max-width:65ch}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-\[180px\]{min-width:180px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-105{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform-gpu{transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-grab{cursor:grab}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.scroll-mt-28{scroll-margin-top:calc(var(--spacing) * 28)}.list-decimal{list-style-type:decimal}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-\[13rem_minmax\(0\,1fr\)\]{grid-template-columns:13rem minmax(0,1fr)}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-10{gap:calc(var(--spacing) * 10)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-y-1{row-gap:var(--spacing)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-\[var\(--c-border\)\]>:not(:last-child)){border-color:var(--c-border)}.self-center{align-self:center}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-xs{border-radius:var(--radius-xs)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-3{border-style:var(--tw-border-style);border-width:3px}.border-\[2\.5px\]{border-style:var(--tw-border-style);border-width:2.5px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-\[var\(--c-blue1\)\],.border-\[var\(--c-blue1\)\]\/40{border-color:var(--c-blue1)}@supports (color:color-mix(in lab, red, red)){.border-\[var\(--c-blue1\)\]\/40{border-color:color-mix(in oklab, var(--c-blue1) 40%, transparent)}}.border-\[var\(--c-blue3\)\]{border-color:var(--c-blue3)}.border-\[var\(--c-border\)\],.border-\[var\(--c-border\)\]\/60{border-color:var(--c-border)}@supports (color:color-mix(in lab, red, red)){.border-\[var\(--c-border\)\]\/60{border-color:color-mix(in oklab, var(--c-border) 60%, transparent)}}.border-\[var\(--c-border\)\]\/80{border-color:var(--c-border)}@supports (color:color-mix(in lab, red, red)){.border-\[var\(--c-border\)\]\/80{border-color:color-mix(in oklab, var(--c-border) 80%, transparent)}}.border-\[var\(--c-danger-border\)\]{border-color:var(--c-danger-border)}.border-\[var\(--c-green3\)\]{border-color:var(--c-green3)}.border-\[var\(--c-mobile-nav-active-border\)\]{border-color:var(--c-mobile-nav-active-border)}.border-\[var\(--c-success-border\)\]{border-color:var(--c-success-border)}.border-amber-500\/40{border-color:#f99c0066}@supports (color:color-mix(in lab, red, red)){.border-amber-500\/40{border-color:color-mix(in oklab, var(--color-amber-500) 40%, transparent)}}.border-red-500\/30{border-color:#fb2c364d}@supports (color:color-mix(in lab, red, red)){.border-red-500\/30{border-color:color-mix(in oklab, var(--color-red-500) 30%, transparent)}}.border-transparent{border-color:#0000}.border-t-\[var\(--c-blue1\)\]{border-top-color:var(--c-blue1)}.bg-\[var\(--c-bg\)\],.bg-\[var\(--c-bg\)\]\/60{background-color:var(--c-bg)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--c-bg\)\]\/60{background-color:color-mix(in oklab, var(--c-bg) 60%, transparent)}}.bg-\[var\(--c-blue1\)\],.bg-\[var\(--c-blue1\)\]\/10{background-color:var(--c-blue1)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--c-blue1\)\]\/10{background-color:color-mix(in oklab, var(--c-blue1) 10%, transparent)}}.bg-\[var\(--c-blue1\)\]\/15{background-color:var(--c-blue1)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--c-blue1\)\]\/15{background-color:color-mix(in oklab, var(--c-blue1) 15%, transparent)}}.bg-\[var\(--c-border\)\],.bg-\[var\(--c-border\)\]\/40{background-color:var(--c-border)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--c-border\)\]\/40{background-color:color-mix(in oklab, var(--c-border) 40%, transparent)}}.bg-\[var\(--c-danger\)\]{background-color:var(--c-danger)}.bg-\[var\(--c-danger-bg\)\]{background-color:var(--c-danger-bg)}.bg-\[var\(--c-green3\)\]{background-color:var(--c-green3)}.bg-\[var\(--c-mobile-nav-active\)\]{background-color:var(--c-mobile-nav-active)}.bg-\[var\(--c-success-bg\)\]{background-color:var(--c-success-bg)}.bg-\[var\(--c-surface\)\]{background-color:var(--c-surface)}.bg-\[var\(--c-surface-hover\)\],.bg-\[var\(--c-surface-hover\)\]\/25{background-color:var(--c-surface-hover)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--c-surface-hover\)\]\/25{background-color:color-mix(in oklab, var(--c-surface-hover) 25%, transparent)}}.bg-\[var\(--c-text-muted\)\]{background-color:var(--c-text-muted)}.bg-amber-500\/15{background-color:#f99c0026}@supports (color:color-mix(in lab, red, red)){.bg-amber-500\/15{background-color:color-mix(in oklab, var(--color-amber-500) 15%, transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/10{background-color:color-mix(in oklab, var(--color-red-500) 10%, transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-0{padding:0}.p-0\!{padding:0!important}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-3\.5{padding-block:calc(var(--spacing) * 3.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-1{padding-right:var(--spacing)}.pr-1\.5{padding-right:calc(var(--spacing) * 1.5)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-5{padding-right:calc(var(--spacing) * 5)}.pb-1{padding-bottom:var(--spacing)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-24{padding-bottom:calc(var(--spacing) * 24)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-9{padding-left:calc(var(--spacing) * 9)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-\[-2px\]{vertical-align:-2px}.font-\[\'Manrope\'\]{font-family:Manrope}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.625rem\]{font-size:.625rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[var\(--c-blue1\)\]{color:var(--c-blue1)}.text-\[var\(--c-blue3\)\]{color:var(--c-blue3)}.text-\[var\(--c-danger\)\]{color:var(--c-danger)}.text-\[var\(--c-danger\)\]\!{color:var(--c-danger)!important}.text-\[var\(--c-green3\)\]{color:var(--c-green3)}.text-\[var\(--c-text\)\]{color:var(--c-text)}.text-\[var\(--c-text-muted\)\]{color:var(--c-text-muted)}.text-amber-500{color:var(--color-amber-500)}.text-emerald-500{color:var(--color-emerald-500)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.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,)}.underline{text-decoration-line:underline}.accent-\[var\(--c-blue1\)\]{accent-color:var(--c-blue1)}.opacity-0{opacity:0}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[inset_0_2px_0_var\(--c-blue3\)\]{--tw-shadow:inset 0 2px 0 var(--tw-shadow-color,var(--c-blue3));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-\[var\(--c-blue3\)\]{--tw-ring-color:var(--c-blue3)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-\[var\(--c-surface\)\]{--tw-ring-offset-color:var(--c-surface)}.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-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-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-md{--tw-backdrop-blur:blur(var(--blur-md));-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-xs{--tw-backdrop-blur:blur(var(--blur-xs));-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-saturate-180{--tw-backdrop-saturate:saturate(180%);-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,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[opacity\,background-color\]{transition-property:opacity,background-color;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[transform\,width\]{transition-property:transform,width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.delay-75{transition-delay:75ms}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-\[cubic-bezier\(0\.25\,1\,0\.5\,1\)\]{--tw-ease:cubic-bezier(.25,1,.5,1);transition-timing-function:cubic-bezier(.25,1,.5,1)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:scale-\[1\.025\]:is(:where(.group):hover *){scale:1.025}.group-hover\/feed\:opacity-60:is(:where(.group\/feed):hover *){opacity:.6}}.peer-checked\:bg-\[var\(--c-blue1\)\]:is(:where(.peer):checked~*){background-color:var(--c-blue1)}.peer-focus-visible\:outline-2:is(:where(.peer):focus-visible~*){outline-style:var(--tw-outline-style);outline-width:2px}.peer-focus-visible\:outline-offset-2:is(:where(.peer):focus-visible~*){outline-offset:2px}.peer-focus-visible\:outline-\[var\(--c-blue3\)\]:is(:where(.peer):focus-visible~*){outline-color:var(--c-blue3)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:top-\[2px\]:after{content:var(--tw-content);top:2px}.after\:left-\[2px\]:after{content:var(--tw-content);left:2px}.after\:h-5:after{content:var(--tw-content);height:calc(var(--spacing) * 5)}.after\:w-5:after{content:var(--tw-content);width:calc(var(--spacing) * 5)}.after\:rounded-full:after{content:var(--tw-content);border-radius:2147483647px}.after\:bg-white:after{content:var(--tw-content);background-color:var(--color-white)}.after\:shadow-sm:after{content:var(--tw-content);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.after\:transition-transform:after{content:var(--tw-content);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.peer-checked\:after\:translate-x-5:is(:where(.peer):checked~*):after{content:var(--tw-content);--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.first\:rounded-t-xl:first-child{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.first\:border-t-0:first-child{border-top-style:var(--tw-border-style);border-top-width:0}.last\:rounded-b-xl:last-child{border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}@media (hover:hover){.hover\:border-\[var\(--c-blue3\)\]:hover,.hover\:border-\[var\(--c-blue3\)\]\/60:hover{border-color:var(--c-blue3)}@supports (color:color-mix(in lab, red, red)){.hover\:border-\[var\(--c-blue3\)\]\/60:hover{border-color:color-mix(in oklab, var(--c-blue3) 60%, transparent)}}.hover\:border-\[var\(--c-border\)\]:hover{border-color:var(--c-border)}.hover\:border-amber-500\/30:hover{border-color:#f99c004d}@supports (color:color-mix(in lab, red, red)){.hover\:border-amber-500\/30:hover{border-color:color-mix(in oklab, var(--color-amber-500) 30%, transparent)}}.hover\:bg-\[var\(--c-border\)\]:hover,.hover\:bg-\[var\(--c-border\)\]\/40:hover{background-color:var(--c-border)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-\[var\(--c-border\)\]\/40:hover{background-color:color-mix(in oklab, var(--c-border) 40%, transparent)}}.hover\:bg-\[var\(--c-border\)\]\/60:hover{background-color:var(--c-border)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-\[var\(--c-border\)\]\/60:hover{background-color:color-mix(in oklab, var(--c-border) 60%, transparent)}}.hover\:bg-\[var\(--c-danger-bg\)\]:hover{background-color:var(--c-danger-bg)}.hover\:bg-\[var\(--c-danger-bg\)\]\!:hover{background-color:var(--c-danger-bg)!important}.hover\:bg-\[var\(--c-surface-hover\)\]:hover,.hover\:bg-\[var\(--c-surface-hover\)\]\/50:hover{background-color:var(--c-surface-hover)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-\[var\(--c-surface-hover\)\]\/50:hover{background-color:color-mix(in oklab, var(--c-surface-hover) 50%, transparent)}}.hover\:text-\[var\(--c-danger\)\]:hover{color:var(--c-danger)}.hover\:text-\[var\(--c-text\)\]:hover{color:var(--c-text)}.hover\:text-amber-500:hover{color:var(--color-amber-500)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-85:hover{opacity:.85}.hover\:opacity-90:hover{opacity:.9}.hover\:opacity-100:hover{opacity:1}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:ring-\[var\(--c-border\)\]:hover{--tw-ring-color:var(--c-border)}.hover\:ring-offset-2:hover{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.hover\:ring-offset-\[var\(--c-surface\)\]:hover{--tw-ring-offset-color:var(--c-surface)}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:border-\[var\(--c-blue3\)\]:focus{border-color:var(--c-blue3)}.focus\:border-\[var\(--c-border\)\]:focus{border-color:var(--c-border)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-offset-\[-3px\]:focus-visible{outline-offset:-3px}.focus-visible\:outline-\[var\(--c-blue3\)\]:focus-visible{outline-color:var(--c-blue3)}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:cursor-default:disabled{cursor:default}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media (prefers-reduced-motion:reduce){.motion-reduce\:animate-none{animation:none}.motion-reduce\:transition-none{transition-property:none}}@media not all and (width>=48rem){.max-md\:hidden{display:none}}@media (width>=40rem){.sm\:m-0{margin:0}.sm\:mx-0{margin-inline:0}.sm\:ml-6{margin-left:calc(var(--spacing) * 6)}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:h-auto{height:auto}.sm\:w-48{width:calc(var(--spacing) * 48)}.sm\:w-52{width:calc(var(--spacing) * 52)}.sm\:w-64{width:calc(var(--spacing) * 64)}.sm\:max-w-2xl{max-width:var(--container-2xl)}.sm\:max-w-none{max-width:none}.sm\:max-w-sm{max-width:var(--container-sm)}.sm\:min-w-52{min-width:calc(var(--spacing) * 52)}.sm\:flex-1{flex:1}.sm\:flex-none{flex:none}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.sm\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:items-center{align-items:center}.sm\:items-start{align-items:flex-start}.sm\:items-stretch{align-items:stretch}.sm\:justify-between{justify-content:space-between}.sm\:gap-0{gap:0}.sm\:gap-2{gap:calc(var(--spacing) * 2)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-5{gap:calc(var(--spacing) * 5)}.sm\:gap-6{gap:calc(var(--spacing) * 6)}.sm\:self-center{align-self:center}.sm\:self-start{align-self:flex-start}.sm\:self-stretch{align-self:stretch}.sm\:overflow-visible{overflow:visible}.sm\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.sm\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.sm\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.sm\:p-0{padding:0}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:p-7{padding:calc(var(--spacing) * 7)}.sm\:px-0{padding-inline:0}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:px-10{padding-inline:calc(var(--spacing) * 10)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}.sm\:pr-3{padding-right:calc(var(--spacing) * 3)}.sm\:text-left{text-align:left}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (width>=48rem){.md\:bottom-6{bottom:calc(var(--spacing) * 6)}.md\:flex{display:flex}.md\:hidden{display:none}.md\:max-w-3xl{max-width:var(--container-3xl)}.md\:py-6{padding-block:calc(var(--spacing) * 6)}}@media (width>=64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:line-clamp-12{-webkit-line-clamp:12;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (prefers-color-scheme:dark){.dark\:border-t-white\/80{border-top-color:#fffc}@supports (color:color-mix(in lab, red, red)){.dark\:border-t-white\/80{border-top-color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.dark\:text-red-400{color:var(--color-red-400)}}}:root,[data-theme=light]{--c-bg:#f8fafc;--c-surface:#fff;--c-surface-hover:#f1f5f9;--c-border:#33415524;--c-text:#0f172a;--c-text-muted:#64748b;--c-blue1:#0f172a;--c-blue3:#334155;--c-blue4:#475569;--c-green3:#16a34a;--c-success-bg:#16a34a14;--c-success-border:#16a34a40;--c-copper:#7c2d12;--c-copper-hover:#9a3412;--c-danger:#7c2d12;--c-danger-hover:#9a3412;--c-danger-bg:#7c2d1214;--c-danger-border:#7c2d1240;--c-mobile-nav-active:#e2e8f0;--c-mobile-nav-active-border:#33415538;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}[data-theme=dark]{--c-bg:#0b0f17;--c-surface:#141b27;--c-surface-hover:#1b2434;--c-border:#cbd5e11f;--c-text:#f1f5f9;--c-text-muted:#94a3b8;--c-blue1:#334155;--c-blue3:#64748b;--c-blue4:#94a3b8;--c-green3:#22c55e;--c-success-bg:#22c55e1f;--c-success-border:#22c55e4d;--c-copper:#ea580c;--c-copper-hover:#f97316;--c-danger:#ea580c;--c-danger-hover:#f97316;--c-danger-bg:#ea580c1f;--c-danger-border:#ea580c4d;--c-mobile-nav-active:#ffffff1f;--c-mobile-nav-active-border:#ffffff26;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--c-bg:#0b0f17;--c-surface:#141b27;--c-surface-hover:#1b2434;--c-border:#cbd5e11f;--c-text:#f1f5f9;--c-text-muted:#94a3b8;--c-blue1:#334155;--c-blue3:#64748b;--c-blue4:#94a3b8;--c-green3:#22c55e;--c-success-bg:#22c55e1f;--c-success-border:#22c55e4d;--c-copper:#ea580c;--c-copper-hover:#f97316;--c-danger:#ea580c;--c-danger-hover:#f97316;--c-danger-bg:#ea580c1f;--c-danger-border:#ea580c4d;--c-mobile-nav-active:#ffffff1f;--c-mobile-nav-active-border:#ffffff26;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}html,body{background-color:var(--c-bg);color:var(--c-text);margin:0;font-family:Inter,sans-serif}h1,h2,h3{font-family:Manrope,sans-serif}button,a,select,summary,[role=button],input[type=checkbox],input[type=radio],input[type=submit],input[type=button]{cursor:pointer}button:disabled,input:disabled{cursor:not-allowed}input[type=checkbox]{appearance:none;border:1px solid var(--c-border);background-color:var(--c-surface);cursor:pointer;vertical-align:middle;border-radius:.375rem;flex-shrink:0;place-content:center;width:1.125rem;height:1.125rem;margin:0;transition:background-color .15s,border-color .15s,box-shadow .15s;display:inline-grid;box-shadow:0 1px 2px #0000000d}input[type=checkbox]:hover{border-color:var(--c-blue3)}input[type=checkbox]:focus-visible{border-color:var(--c-blue3);outline:none}input[type=checkbox]:checked{background-color:var(--c-blue1);border-color:var(--c-blue1);background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='%23ffffff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%}input[type=checkbox]:disabled{opacity:.5;cursor:not-allowed}@media (pointer:coarse){input,select,textarea{font-size:16px}}@keyframes drawerSlideIn{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes backdropFadeIn{0%{opacity:0}to{opacity:1}}.animate-drawer-in{animation:.26s cubic-bezier(.16,1,.3,1) forwards drawerSlideIn}.animate-fade-in{animation:.2s ease-out backdropFadeIn}.reader-content{color:var(--c-text);word-break:break-word;font-size:1.0625rem;line-height:1.75}.reader-content p{margin-top:1.25rem;margin-bottom:1.25rem}.reader-content h1,.reader-content h2,.reader-content h3,.reader-content h4{color:var(--c-text);margin-top:2rem;margin-bottom:.75rem;font-family:Manrope,sans-serif;font-weight:700;line-height:1.35}.reader-content h1{font-size:1.75rem}.reader-content h2{font-size:1.4rem}.reader-content h3{font-size:1.2rem}.reader-content h4{font-size:1.05rem}.reader-content a{color:var(--c-blue1);text-underline-offset:3px;text-decoration:underline;transition:opacity .15s}.reader-content a:hover:not(:has(img,picture,video)){opacity:.8}.reader-content a:has(img,picture,video){text-decoration:none;transition:none}.reader-content img,.reader-content video{clear:both;border-radius:.5rem;max-width:100%;height:auto;margin:1.5rem auto;display:block}.reader-content figure{margin:1.5rem 0}.reader-content figcaption{color:var(--c-text-muted);text-align:center;margin-top:.5rem;font-size:.8125rem}.reader-content blockquote{border-left:3px solid var(--c-blue1);color:var(--c-text-muted);margin:1.5rem 0;padding-left:1rem;font-style:italic}.reader-content ul,.reader-content ol{margin:1.25rem 0;padding-left:1.5rem}.reader-content ul{list-style-type:disc}.reader-content ol{list-style-type:decimal}.reader-content li{margin-top:.375rem;margin-bottom:.375rem}.reader-content pre{background-color:var(--c-bg);border:1px solid var(--c-border);border-radius:.5rem;margin:1.5rem 0;padding:1rem;font-size:.875rem;overflow-x:auto}.reader-content code:not(pre code){background-color:var(--c-bg);border:1px solid var(--c-border);border-radius:.25rem;padding:.125rem .375rem;font-size:.875rem}.reader-content hr{border:0;border-top:1px solid var(--c-border);margin:2rem 0}@keyframes pageFadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.animate-page-fade{will-change:opacity, transform;animation:.28s cubic-bezier(.16,1,.3,1) both pageFadeIn}.mobile-bottom-nav{bottom:max(.75rem, env(safe-area-inset-bottom,0px))}@media (width<=639px){.items-toolbar{transition:gap .3s cubic-bezier(.25,1,.5,1)}.items-toolbar-scope,.items-toolbar-search,.items-toolbar-filters{transition:flex-grow .3s cubic-bezier(.25,1,.5,1),flex-basis .3s cubic-bezier(.25,1,.5,1),opacity .2s}.items-toolbar-search{flex:0 0 38px;width:38px}.items-toolbar-filters{flex:0 0 82px;width:82px;overflow:hidden}.items-toolbar-open{gap:0}.items-toolbar-open .items-toolbar-scope,.items-toolbar-open .items-toolbar-filters{opacity:0;pointer-events:none;flex:0 0 0;width:0;overflow:hidden}.items-toolbar-open .items-toolbar-search{flex:1 1 0;width:auto}}@media (prefers-reduced-motion:reduce){.animate-page-fade{animation:none}.items-toolbar,.items-toolbar-scope,.items-toolbar-search,.items-toolbar-filters{transition:none}}.no-drag{touch-action:manipulation;-webkit-touch-callout:none!important;-webkit-user-select:none!important;user-select:none!important;-webkit-user-drag:none!important;user-drag:none!important}@media screen and (width<=767px){input:not([type=checkbox]):not([type=radio]),select,textarea{font-size:16px!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}
|