@isomoes/iread 0.1.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/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/server/cli.js +84 -0
- package/dist/server/cli.js.map +1 -0
- package/dist/server/db.js +158 -0
- package/dist/server/db.js.map +1 -0
- package/dist/server/feed-service.js +523 -0
- package/dist/server/feed-service.js.map +1 -0
- package/dist/server/fetch-feed.js +83 -0
- package/dist/server/fetch-feed.js.map +1 -0
- package/dist/server/index.js +62 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/opml.js +137 -0
- package/dist/server/opml.js.map +1 -0
- package/dist/server/routes/feeds.js +68 -0
- package/dist/server/routes/feeds.js.map +1 -0
- package/dist/server/routes/helpers.js +44 -0
- package/dist/server/routes/helpers.js.map +1 -0
- package/dist/server/routes/items.js +95 -0
- package/dist/server/routes/items.js.map +1 -0
- package/dist/server/routes/opml.js +50 -0
- package/dist/server/routes/opml.js.map +1 -0
- package/dist/server/sanitize.js +75 -0
- package/dist/server/sanitize.js.map +1 -0
- package/dist/server/ssrf.js +275 -0
- package/dist/server/ssrf.js.map +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
- package/dist/web/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
- package/dist/web/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
- package/dist/web/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
- package/dist/web/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
- package/dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/web/assets/index-BI1j2sXf.css +2 -0
- package/dist/web/assets/index-HhCr0pHx.js +17 -0
- package/dist/web/assets/index-HhCr0pHx.js.map +1 -0
- package/dist/web/index.html +25 -0
- package/package.json +75 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// src/server/ssrf.ts
|
|
2
|
+
// URL guard against SSRF: scheme allowlist plus DNS-resolved IP-range blocking
|
|
3
|
+
// (loopback / link-local / private / CGNAT / unique-local / unspecified), applied
|
|
4
|
+
// before the initial fetch and re-validated on every redirect hop. (PLAN 5.1, 10)
|
|
5
|
+
import { lookup } from 'node:dns/promises';
|
|
6
|
+
import { isIP } from 'node:net';
|
|
7
|
+
import { request as httpRequest } from 'node:http';
|
|
8
|
+
import { request as httpsRequest } from 'node:https';
|
|
9
|
+
import { Readable } from 'node:stream';
|
|
10
|
+
const MAX_REDIRECTS = 5;
|
|
11
|
+
export class SsrfError extends Error {
|
|
12
|
+
name = 'SsrfError';
|
|
13
|
+
}
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// IP range checks
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function ipv4ToInt(ip) {
|
|
18
|
+
const parts = ip.split('.');
|
|
19
|
+
if (parts.length !== 4)
|
|
20
|
+
return null;
|
|
21
|
+
let acc = 0;
|
|
22
|
+
for (const part of parts) {
|
|
23
|
+
if (!/^\d+$/.test(part))
|
|
24
|
+
return null;
|
|
25
|
+
const n = Number(part);
|
|
26
|
+
if (n < 0 || n > 255)
|
|
27
|
+
return null;
|
|
28
|
+
acc = acc * 256 + n;
|
|
29
|
+
}
|
|
30
|
+
return acc >>> 0;
|
|
31
|
+
}
|
|
32
|
+
function inCidr4(ipInt, baseIp, prefix) {
|
|
33
|
+
const base = ipv4ToInt(baseIp);
|
|
34
|
+
if (base === null)
|
|
35
|
+
return false;
|
|
36
|
+
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
|
37
|
+
return (ipInt & mask) === (base & mask);
|
|
38
|
+
}
|
|
39
|
+
function isBlockedIPv4(ip) {
|
|
40
|
+
const n = ipv4ToInt(ip);
|
|
41
|
+
if (n === null)
|
|
42
|
+
return true; // unparseable -> treat as unsafe
|
|
43
|
+
return (inCidr4(n, '127.0.0.0', 8) || // loopback
|
|
44
|
+
inCidr4(n, '10.0.0.0', 8) || // private
|
|
45
|
+
inCidr4(n, '172.16.0.0', 12) || // private
|
|
46
|
+
inCidr4(n, '192.168.0.0', 16) || // private
|
|
47
|
+
inCidr4(n, '169.254.0.0', 16) || // link-local
|
|
48
|
+
inCidr4(n, '100.64.0.0', 10) || // CGNAT
|
|
49
|
+
inCidr4(n, '0.0.0.0', 8) // unspecified / "this" network
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function normalizeIPv6(ip) {
|
|
53
|
+
// Strip zone id (fe80::1%eth0) and lowercase for prefix comparison.
|
|
54
|
+
const zone = ip.indexOf('%');
|
|
55
|
+
return (zone === -1 ? ip : ip.slice(0, zone)).toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
function isBlockedIPv6(rawIp) {
|
|
58
|
+
const ip = normalizeIPv6(rawIp);
|
|
59
|
+
if (ip === '::1')
|
|
60
|
+
return true; // loopback
|
|
61
|
+
if (ip === '::' || ip === '::0')
|
|
62
|
+
return true; // unspecified
|
|
63
|
+
// IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible addresses: validate the
|
|
64
|
+
// embedded IPv4 against the v4 rules.
|
|
65
|
+
const mapped = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
66
|
+
if (mapped && mapped[1])
|
|
67
|
+
return isBlockedIPv4(mapped[1]);
|
|
68
|
+
const compat = ip.match(/^::(\d+\.\d+\.\d+\.\d+)$/);
|
|
69
|
+
if (compat && compat[1])
|
|
70
|
+
return isBlockedIPv4(compat[1]);
|
|
71
|
+
// Prefix checks on the first hextet(s).
|
|
72
|
+
// fe80::/10 link-local: first 10 bits are 1111 1110 10 -> fe80..febf.
|
|
73
|
+
const firstHextet = parseInt(ip.split(':')[0] || '0', 16);
|
|
74
|
+
if (Number.isFinite(firstHextet)) {
|
|
75
|
+
if ((firstHextet & 0xffc0) === 0xfe80)
|
|
76
|
+
return true; // fe80::/10
|
|
77
|
+
if ((firstHextet & 0xfe00) === 0xfc00)
|
|
78
|
+
return true; // fc00::/7 unique-local
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function isBlockedIp(ip) {
|
|
83
|
+
const family = isIP(ip);
|
|
84
|
+
if (family === 4)
|
|
85
|
+
return isBlockedIPv4(ip);
|
|
86
|
+
if (family === 6)
|
|
87
|
+
return isBlockedIPv6(ip);
|
|
88
|
+
return true; // not a valid IP literal -> unsafe
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Throw SsrfError unless `url` is a safe http(s) target:
|
|
92
|
+
* - scheme must be http or https,
|
|
93
|
+
* - a literal-IP host must not fall in a blocked range,
|
|
94
|
+
* - a DNS hostname must resolve only to non-blocked addresses (all A/AAAA).
|
|
95
|
+
*
|
|
96
|
+
* Returns the validated addresses so the caller can pin the connection to exactly
|
|
97
|
+
* what was checked, closing the resolve-then-fetch (DNS-rebinding / TOCTOU) gap where
|
|
98
|
+
* fetch() would otherwise re-resolve the hostname independently (PLAN 5.1, 10).
|
|
99
|
+
*/
|
|
100
|
+
export async function assertSafeFeedUrl(url) {
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = new URL(url);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new SsrfError('Invalid URL.');
|
|
107
|
+
}
|
|
108
|
+
const scheme = parsed.protocol.replace(/:$/, '').toLowerCase();
|
|
109
|
+
if (scheme !== 'http' && scheme !== 'https') {
|
|
110
|
+
throw new SsrfError(`Unsupported URL scheme: ${scheme || '(none)'}.`);
|
|
111
|
+
}
|
|
112
|
+
// Hostname may be bracketed for IPv6 literals ([::1]); URL strips the brackets
|
|
113
|
+
// from parsed.hostname already.
|
|
114
|
+
const host = parsed.hostname;
|
|
115
|
+
if (!host)
|
|
116
|
+
throw new SsrfError('URL has no host.');
|
|
117
|
+
const literalFamily = isIP(host);
|
|
118
|
+
if (literalFamily !== 0) {
|
|
119
|
+
if (isBlockedIp(host)) {
|
|
120
|
+
throw new SsrfError('URL resolves to a blocked (private/loopback) address.');
|
|
121
|
+
}
|
|
122
|
+
return [{ address: host, family: literalFamily === 6 ? 6 : 4 }];
|
|
123
|
+
}
|
|
124
|
+
// DNS hostname: resolve all addresses and reject if any is blocked.
|
|
125
|
+
let addrs;
|
|
126
|
+
try {
|
|
127
|
+
addrs = await lookup(host, { all: true });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new SsrfError(`Could not resolve host: ${host}.`);
|
|
131
|
+
}
|
|
132
|
+
if (addrs.length === 0) {
|
|
133
|
+
throw new SsrfError(`Host did not resolve to any address: ${host}.`);
|
|
134
|
+
}
|
|
135
|
+
for (const { address } of addrs) {
|
|
136
|
+
if (isBlockedIp(address)) {
|
|
137
|
+
throw new SsrfError('URL resolves to a blocked (private/loopback) address.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return addrs.map(({ address, family }) => ({
|
|
141
|
+
address,
|
|
142
|
+
family: family === 6 ? 6 : 4,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Guarded fetch with manual redirect handling
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/** A lookup callback that yields only the pre-validated addresses (DNS-rebinding pin). */
|
|
149
|
+
function pinnedLookup(validated) {
|
|
150
|
+
// Node's LookupFunction passes either a numeric family or a LookupOptions object
|
|
151
|
+
// (whose `family` may be 4 | 6 | 'IPv4' | 'IPv6'). We accept the loose shape and
|
|
152
|
+
// normalize, then return only addresses we already validated.
|
|
153
|
+
return (_hostname, options, callback) => {
|
|
154
|
+
const rawFamily = typeof options === 'object' ? options.family : options;
|
|
155
|
+
const wantFamily = rawFamily === 4 || rawFamily === 'IPv4'
|
|
156
|
+
? 4
|
|
157
|
+
: rawFamily === 6 || rawFamily === 'IPv6'
|
|
158
|
+
? 6
|
|
159
|
+
: 0;
|
|
160
|
+
const wantAll = typeof options === 'object' ? options.all === true : false;
|
|
161
|
+
const pool = wantFamily ? validated.filter((a) => a.family === wantFamily) : validated;
|
|
162
|
+
const chosen = pool.length > 0 ? pool : validated;
|
|
163
|
+
if (wantAll) {
|
|
164
|
+
callback(null, chosen.map((a) => ({ address: a.address, family: a.family })));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const first = chosen[0];
|
|
168
|
+
callback(null, first.address, first.family);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function headersToObject(init) {
|
|
172
|
+
const out = {};
|
|
173
|
+
if (!init)
|
|
174
|
+
return out;
|
|
175
|
+
new Headers(init).forEach((value, key) => {
|
|
176
|
+
out[key] = value;
|
|
177
|
+
});
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Issue one GET to `url` over node:http(s), pinning the socket to the pre-validated
|
|
182
|
+
* IP(s) via a custom `lookup`, so the kernel never re-resolves the hostname to a
|
|
183
|
+
* different (private/loopback) address between assertSafeFeedUrl and connect
|
|
184
|
+
* (DNS-rebinding / TOCTOU, PLAN 5.1/10). TLS SNI + Host header stay the original
|
|
185
|
+
* hostname (node:https uses the URL host for both); only the connect target is pinned.
|
|
186
|
+
* Returns a WHATWG Response so the streaming caller is unchanged.
|
|
187
|
+
*/
|
|
188
|
+
function pinnedFetch(url, validated, init) {
|
|
189
|
+
const parsed = new URL(url);
|
|
190
|
+
const isHttps = parsed.protocol === 'https:';
|
|
191
|
+
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
192
|
+
const signal = init.signal ?? undefined;
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const req = requestFn(parsed, {
|
|
195
|
+
method: (init.method ?? 'GET').toUpperCase(),
|
|
196
|
+
headers: headersToObject(init.headers),
|
|
197
|
+
lookup: pinnedLookup(validated),
|
|
198
|
+
// servername defaults to the URL hostname for https, preserving SNI.
|
|
199
|
+
}, (msg) => {
|
|
200
|
+
const headers = new Headers();
|
|
201
|
+
for (const [k, v] of Object.entries(msg.headers)) {
|
|
202
|
+
if (v === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
if (Array.isArray(v))
|
|
205
|
+
for (const one of v)
|
|
206
|
+
headers.append(k, one);
|
|
207
|
+
else
|
|
208
|
+
headers.set(k, v);
|
|
209
|
+
}
|
|
210
|
+
const status = msg.statusCode ?? 0;
|
|
211
|
+
// 204/304 must not carry a body in a Response.
|
|
212
|
+
const nullBody = status === 204 || status === 304;
|
|
213
|
+
const body = nullBody
|
|
214
|
+
? null
|
|
215
|
+
: Readable.toWeb(msg);
|
|
216
|
+
resolve(new Response(body, {
|
|
217
|
+
status,
|
|
218
|
+
statusText: msg.statusMessage ?? '',
|
|
219
|
+
headers,
|
|
220
|
+
}));
|
|
221
|
+
});
|
|
222
|
+
if (signal) {
|
|
223
|
+
if (signal.aborted) {
|
|
224
|
+
req.destroy(new DOMException('Aborted', 'AbortError'));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
signal.addEventListener('abort', () => req.destroy(new DOMException('Aborted', 'AbortError')), {
|
|
228
|
+
once: true,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
req.on('error', reject);
|
|
233
|
+
req.end();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* fetch() that follows redirects manually (max 5 hops), re-running the SSRF
|
|
238
|
+
* guard on every hop's destination before requesting it. A redirect-count cap
|
|
239
|
+
* alone is not protection; per-hop destination validation is what matters.
|
|
240
|
+
*
|
|
241
|
+
* The caller's `redirect` option is ignored/overridden to 'manual'.
|
|
242
|
+
*/
|
|
243
|
+
export async function fetchWithGuardedRedirects(initialUrl, init) {
|
|
244
|
+
let currentUrl = initialUrl;
|
|
245
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
246
|
+
const validated = await assertSafeFeedUrl(currentUrl);
|
|
247
|
+
// Pin the connection to exactly the addresses we validated, so the hostname cannot
|
|
248
|
+
// be re-resolved to a different (private/loopback) IP between the check and the
|
|
249
|
+
// connect (DNS-rebinding / TOCTOU). Re-validated + re-pinned per redirect hop.
|
|
250
|
+
const res = await pinnedFetch(currentUrl, validated, init);
|
|
251
|
+
// Not a redirect: return as-is (including 304 and error statuses; the caller
|
|
252
|
+
// interprets them).
|
|
253
|
+
if (res.status < 300 || res.status >= 400 || res.status === 304) {
|
|
254
|
+
return res;
|
|
255
|
+
}
|
|
256
|
+
const location = res.headers.get('location');
|
|
257
|
+
if (!location) {
|
|
258
|
+
// Redirect status with no Location: nothing to follow, hand back the response.
|
|
259
|
+
return res;
|
|
260
|
+
}
|
|
261
|
+
// Resolve relative redirects against the current URL.
|
|
262
|
+
let next;
|
|
263
|
+
try {
|
|
264
|
+
next = new URL(location, currentUrl).toString();
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
throw new SsrfError('Redirect target is not a valid URL.');
|
|
268
|
+
}
|
|
269
|
+
// Drain the redirect response body so the connection can be reused/closed.
|
|
270
|
+
await res.body?.cancel().catch(() => { });
|
|
271
|
+
currentUrl = next;
|
|
272
|
+
}
|
|
273
|
+
throw new SsrfError(`Too many redirects (> ${MAX_REDIRECTS}).`);
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=ssrf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssrf.js","sourceRoot":"","sources":["../../src/server/ssrf.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,sBAAsB;AACtB,oFAAoF;AACpF,gFAAgF"}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.0 | 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-space-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-outline-style:solid}}}@layer theme{:root,:host{--font-sans:"Geist Variable", ui-sans-serif, system-ui, sans-serif;--font-mono:"Geist Mono Variable", ui-monospace, "SF Mono", monospace;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--radius-xs:.25rem;--radius-sm:.375rem;--radius-md:.5rem;--radius-lg:.75rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--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);--color-bg:var(--bg);--color-surface:var(--surface);--color-surface-elevated:var(--surface-elevated);--color-border:var(--border);--color-text-primary:var(--text-primary);--color-text-secondary:var(--text-secondary);--color-text-muted:var(--text-muted);--color-accent:var(--accent);--color-accent-foreground:var(--accent-foreground);--color-unread:var(--unread);--color-danger:var(--danger)}}@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,ui-sans-serif, system-ui, 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{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}*{border-color:var(--color-border)}html{-webkit-text-size-adjust:100%}body{font-family:var(--font-sans);background-color:var(--color-bg);color:var(--color-text-primary);font-feature-settings:"cv11", "ss01";-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;font-size:.875rem;line-height:1.5}.num,kbd,time,.count{font-family:var(--font-mono);font-variant-numeric:tabular-nums;font-feature-settings:"tnum"}:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px;border-radius:var(--radius-sm)}::selection{background-color:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){::selection{background-color:color-mix(in oklch, var(--color-accent) 28%, transparent)}}.reader-prose{max-width:68ch;font-size:1rem;line-height:1.7}.reader-prose p{margin-block:.85em}.reader-prose a{color:var(--color-accent);text-underline-offset:2px}.reader-prose h1,.reader-prose h2,.reader-prose h3{margin-top:1.4em;line-height:1.3}.reader-prose img{border-radius:var(--radius-md);max-width:100%;height:auto}.reader-prose pre{font-family:var(--font-mono);background:var(--color-surface-elevated);border-radius:var(--radius-md);padding:.75rem 1rem;overflow-x:auto}.reader-prose blockquote{border-left:2px solid var(--color-accent);color:var(--color-text-secondary);padding-left:1rem}}@layer components;@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}.inset-0{inset:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.right-2{right:calc(var(--spacing) * 2)}.right-4{right:calc(var(--spacing) * 4)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-0{left:calc(var(--spacing) * 0)}.left-2\.5{left:calc(var(--spacing) * 2.5)}.z-10{z-index:10}.z-50{z-index:50}.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}}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-6{height:calc(var(--spacing) * 6)}.h-full{height:100%}.max-h-\[80vh\]{max-height:80vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-1\/2{width:50%}.w-2\/3{width:66.6667%}.w-2\/5{width:40%}.w-3\/4{width:75%}.w-4\/5{width:80%}.w-6{width:calc(var(--spacing) * 6)}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-5{min-width:calc(var(--spacing) * 5)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.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:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xs{border-radius:var(--radius-xs)}.border{border-style:var(--tw-border-style);border-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-border{border-color:var(--color-border)}.bg-\[color-mix\(in_oklch\,oklch\(0\.17_0\.004_264\)_45\%\,transparent\)\]{background-color:oklch(17% .004 264/.45)}.bg-accent,.bg-accent\/10{background-color:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.bg-accent\/10{background-color:color-mix(in oklab, var(--color-accent) 10%, transparent)}}.bg-accent\/15{background-color:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.bg-accent\/15{background-color:color-mix(in oklab, var(--color-accent) 15%, transparent)}}.bg-bg{background-color:var(--color-bg)}.bg-danger{background-color:var(--color-danger)}.bg-surface{background-color:var(--color-surface)}.bg-surface-elevated{background-color:var(--color-surface-elevated)}.bg-transparent{background-color:#0000}.bg-unread{background-color:var(--color-unread)}.p-1{padding:calc(var(--spacing) * 1)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-1{padding-inline:calc(var(--spacing) * 1)}.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-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.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-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-accent{color:var(--color-accent)}.text-accent-foreground{color:var(--color-accent-foreground)}.text-danger{color:var(--color-danger)}.text-text-muted{color:var(--color-text-muted)}.text-text-primary{color:var(--color-text-primary)}.text-text-secondary{color:var(--color-text-secondary)}.uppercase{text-transform:uppercase}.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,)}.opacity-0{opacity:0}.opacity-100{opacity:1}.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-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)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.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-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))}.outline-none{--tw-outline-style:none;outline-style:none}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-text-muted::placeholder{color:var(--color-text-muted)}@media (hover:hover){.hover\:bg-surface:hover{background-color:var(--color-surface)}.hover\:bg-surface-elevated:hover{background-color:var(--color-surface-elevated)}.hover\:text-danger:hover{color:var(--color-danger)}.hover\:text-text-primary:hover{color:var(--color-text-primary)}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-90:hover{opacity:.9}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:absolute:focus{position:absolute}.focus\:top-3:focus{top:calc(var(--spacing) * 3)}.focus\:left-3:focus{left:calc(var(--spacing) * 3)}.focus\:z-50:focus{z-index:50}.focus\:rounded-md:focus{border-radius:var(--radius-md)}.focus\:bg-accent:focus{background-color:var(--color-accent)}.focus\:px-3:focus{padding-inline:calc(var(--spacing) * 3)}.focus\:py-2:focus{padding-block:calc(var(--spacing) * 2)}.focus\:text-accent-foreground:focus{color:var(--color-accent-foreground)}.focus-visible\:opacity-100:focus-visible{opacity:1}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-1:focus-visible{outline-offset:1px}.focus-visible\:outline-accent:focus-visible{outline-color:var(--color-accent)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}@media (width>=48rem){.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}}}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-latin-wght-normal-BgDaEnEv.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2)format("woff2-variations");unicode-range:U+2000-2001,U+2004-2008,U+200A,U+23B8-23BD,U+2500-259F}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Mono Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--bg:oklch(98.5% .002 247);--surface:oklch(99.5% .001 247);--surface-elevated:oklch(99.8% .001 247);--border:oklch(92% .003 247);--text-primary:oklch(21% .004 264);--text-secondary:oklch(44% .006 264);--text-muted:oklch(55% .006 264);--accent:oklch(58% .13 244);--accent-foreground:oklch(99% .002 247);--unread:oklch(58% .13 244);--danger:oklch(55% .18 27);--focus-ring:oklch(58% .13 244);--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}.dark{--bg:oklch(17% .004 264);--surface:oklch(20.5% .004 264);--surface-elevated:oklch(24.5% .005 264);--border:oklch(30% .006 264);--text-primary:oklch(96% .002 247);--text-secondary:oklch(71% .004 264);--text-muted:oklch(60% .005 264);--accent:oklch(70% .13 244);--accent-foreground:oklch(17% .01 264);--unread:oklch(70% .13 244);--danger:oklch(70% .16 27);--focus-ring:oklch(70% .13 244);--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}@media (prefers-color-scheme:dark){:root:not(.theme-light):not(.dark){--bg:oklch(17% .004 264);--surface:oklch(20.5% .004 264);--surface-elevated:oklch(24.5% .005 264);--border:oklch(30% .006 264);--text-primary:oklch(96% .002 247);--text-secondary:oklch(71% .004 264);--text-muted:oklch(60% .005 264);--accent:oklch(70% .13 244);--accent-foreground:oklch(17% .01 264);--unread:oklch(70% .13 244);--danger:oklch(70% .16 27);--focus-ring:oklch(70% .13 244);--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}.app-shell{grid-template:"topbar topbar topbar""sidebar list reader"1fr/256px 360px minmax(0,1fr);height:100dvh;display:grid}@media (width<=767px){.app-shell{grid-template-columns:1fr;grid-template-areas:"topbar""active-pane"}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}@property --tw-space-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-outline-style{syntax:"*";inherits:false;initial-value:solid}@keyframes pulse{50%{opacity:.5}}
|