@isomoes/iread 0.2.3 → 0.2.5
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/README.md +4 -2
- package/dist/server/index.js +2 -1
- package/dist/server/proxy.js +100 -0
- package/dist/server/ssrf.js +65 -39
- package/dist/web/assets/index-CMtWFJ_5.css +2 -0
- package/dist/web/assets/index-CwHd_Y2c.js +16 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CW5g_eWm.css +0 -2
- package/dist/web/assets/index-DFve9iOj.js +0 -16
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ docker compose up -d
|
|
|
48
48
|
|
|
49
49
|
Build the image from the checkout instead of pulling it with `docker compose up -d --build`, or `docker build -t iread .`.
|
|
50
50
|
|
|
51
|
-
`PORT` (default 8787), `DB_PATH` (default `/data/iread.db`), and `OPML_PATH` (default `/data/feeds.opml`) are configurable via `-e`/`environment:`. To store data in a host directory instead of a named volume, bind-mount it and make it writable by the image's unprivileged `node` user (uid 1000), e.g. `mkdir iread-data && sudo chown 1000:1000 iread-data` then `-v "$PWD/iread-data:/data"`.
|
|
51
|
+
`PORT` (default 8787), `HOST` (default all interfaces), `DB_PATH` (default `/data/iread.db`), and `OPML_PATH` (default `/data/feeds.opml`) are configurable via `-e`/`environment:`. With host networking, set `HOST=127.0.0.1` to restrict access to localhost; with bridge networking, leave `HOST` unset and restrict the published port instead. To store data in a host directory instead of a named volume, bind-mount it and make it writable by the image's unprivileged `node` user (uid 1000), e.g. `mkdir iread-data && sudo chown 1000:1000 iread-data` then `-v "$PWD/iread-data:/data"`.
|
|
52
52
|
|
|
53
53
|
## Features
|
|
54
54
|
|
|
@@ -148,4 +148,6 @@ pnpm start
|
|
|
148
148
|
|
|
149
149
|
Open http://localhost:8787
|
|
150
150
|
|
|
151
|
-
`PORT` (default 8787), `DB_PATH` (default `~/.config/iread/iread.db`), and `OPML_PATH` (default `feeds.opml` next to the database; set empty to disable the auto-saved mirror) are read from the environment. See `config/.env.example`.
|
|
151
|
+
`PORT` (default 8787), `HOST` (default all interfaces; set `127.0.0.1` for localhost only), `DB_PATH` (default `~/.config/iread/iread.db`), and `OPML_PATH` (default `feeds.opml` next to the database; set empty to disable the auto-saved mirror) are read from the environment. See `config/.env.example`.
|
|
152
|
+
|
|
153
|
+
Feeds are fetched directly by default. To go through an outbound HTTP proxy, set `HTTPS_PROXY` / `HTTP_PROXY` (an `http://host:port` URL, optionally with `user:pass@`) and optionally `NO_PROXY` (comma-separated hosts/domains to bypass). HTTPS feeds are tunnelled with `CONNECT`. Note that the SSRF guard still validates the feed host's DNS answer locally, but with a proxy the proxy does the final connect, so DNS-rebinding pinning cannot apply.
|
package/dist/server/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { feeds } from './routes/feeds.js';
|
|
|
13
13
|
import { items } from './routes/items.js';
|
|
14
14
|
import { opml } from './routes/opml.js';
|
|
15
15
|
const PORT = Number(process.env.PORT ?? 8787);
|
|
16
|
+
const HOST = process.env.HOST || undefined;
|
|
16
17
|
const IS_PROD = process.env.NODE_ENV === 'production';
|
|
17
18
|
// Importing ./db has the side effect of opening the DB and running migrations.
|
|
18
19
|
// Do it explicitly so startup failures surface immediately.
|
|
@@ -56,7 +57,7 @@ if (IS_PROD) {
|
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
// --- Start ------------------------------------------------------------------
|
|
59
|
-
serve({ fetch: app.fetch, port: PORT }, (info) => {
|
|
60
|
+
serve({ fetch: app.fetch, port: PORT, hostname: HOST }, (info) => {
|
|
60
61
|
const url = `http://localhost:${info.port}`;
|
|
61
62
|
console.log(`[iread] listening on ${url}`);
|
|
62
63
|
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/server/proxy.ts
|
|
2
|
+
// Optional outbound HTTP(S) proxy support for feed fetching, driven by the usual
|
|
3
|
+
// HTTPS_PROXY / HTTP_PROXY / NO_PROXY environment variables (either case).
|
|
4
|
+
// Only `http://` proxy URLs are supported (HTTPS targets are tunnelled via CONNECT).
|
|
5
|
+
import { request as httpRequest } from 'node:http';
|
|
6
|
+
import { connect as tlsConnect } from 'node:tls';
|
|
7
|
+
function env(name) {
|
|
8
|
+
return process.env[name] ?? process.env[name.toLowerCase()] ?? undefined;
|
|
9
|
+
}
|
|
10
|
+
function bypassed(host, port, noProxy) {
|
|
11
|
+
const h = host.toLowerCase();
|
|
12
|
+
for (const raw of noProxy.split(',')) {
|
|
13
|
+
let entry = raw.trim().toLowerCase();
|
|
14
|
+
if (!entry)
|
|
15
|
+
continue;
|
|
16
|
+
if (entry === '*')
|
|
17
|
+
return true;
|
|
18
|
+
const colon = entry.lastIndexOf(':');
|
|
19
|
+
if (colon > 0 && !entry.endsWith(']') && /^\d+$/.test(entry.slice(colon + 1))) {
|
|
20
|
+
if (entry.slice(colon + 1) !== port)
|
|
21
|
+
continue;
|
|
22
|
+
entry = entry.slice(0, colon);
|
|
23
|
+
}
|
|
24
|
+
entry = entry.replace(/^\*?\./, '');
|
|
25
|
+
if (h === entry || h.endsWith(`.${entry}`))
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
/** Proxy to use for `target`, or null for a direct connection. */
|
|
31
|
+
export function proxyFor(target) {
|
|
32
|
+
const isHttps = target.protocol === 'https:';
|
|
33
|
+
const value = isHttps ? (env('HTTPS_PROXY') ?? env('HTTP_PROXY')) : env('HTTP_PROXY');
|
|
34
|
+
if (!value)
|
|
35
|
+
return null;
|
|
36
|
+
const port = target.port || (isHttps ? '443' : '80');
|
|
37
|
+
const host = target.hostname.replace(/^\[|\]$/g, '');
|
|
38
|
+
if (bypassed(host, port, env('NO_PROXY') ?? ''))
|
|
39
|
+
return null;
|
|
40
|
+
let p;
|
|
41
|
+
try {
|
|
42
|
+
p = new URL(value.includes('://') ? value : `http://${value}`);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (p.protocol !== 'http:')
|
|
48
|
+
return null;
|
|
49
|
+
const auth = p.username
|
|
50
|
+
? `${decodeURIComponent(p.username)}:${decodeURIComponent(p.password)}`
|
|
51
|
+
: null;
|
|
52
|
+
return { host: p.hostname.replace(/^\[|\]$/g, ''), port: Number(p.port || 80), auth };
|
|
53
|
+
}
|
|
54
|
+
function proxyAuthHeader(proxy) {
|
|
55
|
+
return proxy.auth
|
|
56
|
+
? { 'Proxy-Authorization': `Basic ${Buffer.from(proxy.auth).toString('base64')}` }
|
|
57
|
+
: {};
|
|
58
|
+
}
|
|
59
|
+
/** Headers to add when sending an absolute-URI request to the proxy (http targets). */
|
|
60
|
+
export function plainProxyHeaders(proxy) {
|
|
61
|
+
return proxyAuthHeader(proxy);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Open a CONNECT tunnel through `proxy` to target host:port and wrap it in TLS
|
|
65
|
+
* (SNI = target hostname). Honors `signal` for cancellation.
|
|
66
|
+
*/
|
|
67
|
+
export function tunnelTls(proxy, target, signal) {
|
|
68
|
+
const port = target.port || '443';
|
|
69
|
+
const hostname = target.hostname.replace(/^\[|\]$/g, '');
|
|
70
|
+
const authority = `${target.hostname}:${port}`;
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const req = httpRequest({
|
|
73
|
+
host: proxy.host,
|
|
74
|
+
port: proxy.port,
|
|
75
|
+
method: 'CONNECT',
|
|
76
|
+
path: authority,
|
|
77
|
+
headers: { Host: authority, ...proxyAuthHeader(proxy) },
|
|
78
|
+
});
|
|
79
|
+
const abort = () => req.destroy(new DOMException('Aborted', 'AbortError'));
|
|
80
|
+
if (signal) {
|
|
81
|
+
if (signal.aborted)
|
|
82
|
+
abort();
|
|
83
|
+
else
|
|
84
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
85
|
+
}
|
|
86
|
+
req.once('connect', (res, socket) => {
|
|
87
|
+
signal?.removeEventListener('abort', abort);
|
|
88
|
+
if (res.statusCode !== 200) {
|
|
89
|
+
socket.destroy();
|
|
90
|
+
reject(new Error(`proxy CONNECT failed: HTTP ${res.statusCode ?? 0}`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const tls = tlsConnect({ socket, servername: hostname });
|
|
94
|
+
tls.once('secureConnect', () => resolve(tls));
|
|
95
|
+
tls.once('error', reject);
|
|
96
|
+
});
|
|
97
|
+
req.once('error', reject);
|
|
98
|
+
req.end();
|
|
99
|
+
});
|
|
100
|
+
}
|
package/dist/server/ssrf.js
CHANGED
|
@@ -7,6 +7,7 @@ import { isIP } from 'node:net';
|
|
|
7
7
|
import { request as httpRequest } from 'node:http';
|
|
8
8
|
import { request as httpsRequest } from 'node:https';
|
|
9
9
|
import { Readable } from 'node:stream';
|
|
10
|
+
import { plainProxyHeaders, proxyFor, tunnelTls } from './proxy.js';
|
|
10
11
|
const MAX_REDIRECTS = 5;
|
|
11
12
|
export class SsrfError extends Error {
|
|
12
13
|
name = 'SsrfError';
|
|
@@ -190,48 +191,73 @@ function pinnedFetch(url, validated, init) {
|
|
|
190
191
|
const isHttps = parsed.protocol === 'https:';
|
|
191
192
|
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
192
193
|
const signal = init.signal ?? undefined;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
const proxy = proxyFor(parsed);
|
|
195
|
+
return (async () => {
|
|
196
|
+
// Via a proxy the destination was still validated (assertSafeFeedUrl) but the
|
|
197
|
+
// proxy performs the final resolve+connect, so IP pinning cannot apply.
|
|
198
|
+
let tunnel;
|
|
199
|
+
if (proxy && isHttps)
|
|
200
|
+
tunnel = await tunnelTls(proxy, parsed, signal);
|
|
201
|
+
return send(tunnel);
|
|
202
|
+
})();
|
|
203
|
+
function send(tunnel) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const headers = headersToObject(init.headers);
|
|
206
|
+
let target = parsed;
|
|
207
|
+
let extra = { lookup: pinnedLookup(validated) };
|
|
208
|
+
let fn = requestFn;
|
|
209
|
+
if (tunnel) {
|
|
210
|
+
extra = { createConnection: () => tunnel };
|
|
209
211
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
:
|
|
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'));
|
|
212
|
+
else if (proxy) {
|
|
213
|
+
// Plain-http target: absolute-URI request to the proxy.
|
|
214
|
+
fn = httpRequest;
|
|
215
|
+
target = new URL(`http://${proxy.host.includes(':') ? `[${proxy.host}]` : proxy.host}:${proxy.port}`);
|
|
216
|
+
extra = { path: parsed.href, setHost: false };
|
|
217
|
+
Object.assign(headers, { host: parsed.host }, plainProxyHeaders(proxy));
|
|
225
218
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
219
|
+
const req = fn(target, {
|
|
220
|
+
method: (init.method ?? 'GET').toUpperCase(),
|
|
221
|
+
headers,
|
|
222
|
+
...extra,
|
|
223
|
+
// servername defaults to the URL hostname for https, preserving SNI.
|
|
224
|
+
}, (msg) => {
|
|
225
|
+
const headers = new Headers();
|
|
226
|
+
for (const [k, v] of Object.entries(msg.headers)) {
|
|
227
|
+
if (v === undefined)
|
|
228
|
+
continue;
|
|
229
|
+
if (Array.isArray(v))
|
|
230
|
+
for (const one of v)
|
|
231
|
+
headers.append(k, one);
|
|
232
|
+
else
|
|
233
|
+
headers.set(k, v);
|
|
234
|
+
}
|
|
235
|
+
const status = msg.statusCode ?? 0;
|
|
236
|
+
// 204/304 must not carry a body in a Response.
|
|
237
|
+
const nullBody = status === 204 || status === 304;
|
|
238
|
+
const body = nullBody
|
|
239
|
+
? null
|
|
240
|
+
: Readable.toWeb(msg);
|
|
241
|
+
resolve(new Response(body, {
|
|
242
|
+
status,
|
|
243
|
+
statusText: msg.statusMessage ?? '',
|
|
244
|
+
headers,
|
|
245
|
+
}));
|
|
246
|
+
});
|
|
247
|
+
if (signal) {
|
|
248
|
+
if (signal.aborted) {
|
|
249
|
+
req.destroy(new DOMException('Aborted', 'AbortError'));
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
signal.addEventListener('abort', () => req.destroy(new DOMException('Aborted', 'AbortError')), {
|
|
253
|
+
once: true,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
230
256
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
257
|
+
req.on('error', reject);
|
|
258
|
+
req.end();
|
|
259
|
+
});
|
|
260
|
+
}
|
|
235
261
|
}
|
|
236
262
|
/**
|
|
237
263
|
* fetch() that follows redirects manually (max 5 hops), re-running the SSRF
|
|
@@ -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-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--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>:first-child{margin-top:0}.reader-prose>:last-child{margin-bottom:0}.reader-prose p{margin-block:.85em}.reader-prose a{color:var(--color-accent);text-underline-offset:2px;text-decoration:underline;text-decoration-thickness:from-font}.reader-prose .linknum{font-family:var(--font-mono);vertical-align:super;letter-spacing:-.03em;color:var(--color-text-muted);-webkit-user-select:none;user-select:none;margin-inline-start:.1em;font-size:.68em;line-height:0}.reader-prose.link-jump-active .linknum{color:var(--color-accent);font-weight:600}.reader-prose a[data-linknum].iread-link-target{outline:2px solid var(--color-accent);outline-offset:2px;border-radius:var(--radius-xs);background:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.reader-prose a[data-linknum].iread-link-target{background:color-mix(in oklch, var(--color-accent) 14%, transparent)}}.reader-prose h1,.reader-prose h2,.reader-prose h3,.reader-prose h4,.reader-prose h5,.reader-prose h6{text-wrap:balance;margin-top:1.6em;margin-bottom:.6em;font-weight:600;line-height:1.3}.reader-prose h1{font-size:1.5em}.reader-prose h2{font-size:1.3em}.reader-prose h3{font-size:1.15em}.reader-prose h4{font-size:1em}.reader-prose h5{color:var(--color-text-secondary);font-size:.9em}.reader-prose h6{color:var(--color-text-secondary);font-size:.85em}.reader-prose ul,.reader-prose ol{margin-block:.85em;padding-inline-start:1.6em}.reader-prose ul{list-style:outside}.reader-prose ol{list-style:decimal}.reader-prose ul ul{list-style:circle}.reader-prose ul ul ul{list-style:square}.reader-prose li{margin-block:.3em}.reader-prose li::marker{color:var(--color-text-muted)}.reader-prose :is(ul,ol) :is(ul,ol){margin-block:.3em}.reader-prose dl{margin-block:.85em}.reader-prose dt{margin-top:.6em;font-weight:600}.reader-prose dd{margin-inline-start:1.6em}.reader-prose img,.reader-prose video{border-radius:var(--radius-md);max-width:100%;height:auto}.reader-prose audio{width:100%;max-width:100%}.reader-prose figure{margin-block:1.2em}.reader-prose figcaption{color:var(--color-text-muted);text-align:center;margin-top:.5em;font-size:.85em}.reader-prose code{font-family:var(--font-mono);background:var(--color-surface-elevated);border-radius:var(--radius-xs);padding:.1em .35em;font-size:.875em}.reader-prose pre{font-family:var(--font-mono);background:var(--color-surface-elevated);border:1px solid var(--color-border);border-radius:var(--radius-md);margin-block:1em;padding:.85rem 1rem;line-height:1.5;overflow-x:auto}.reader-prose pre code{background:0 0;padding:0;font-size:.85em}.reader-prose blockquote{border-left:3px solid var(--color-accent);color:var(--color-text-secondary);margin-block:1em;padding-left:1rem;font-style:italic}.reader-prose hr{border:0;border-top:1px solid var(--color-border);margin-block:1.6em}.reader-prose mark{background:var(--color-accent)}@supports (color:color-mix(in lab, red, red)){.reader-prose mark{background:color-mix(in oklch, var(--color-accent) 25%, transparent)}}.reader-prose mark{color:inherit;border-radius:var(--radius-xs);padding:0 .15em}.reader-prose table{border-collapse:collapse;width:max-content;max-width:100%;margin-block:1.2em;font-size:.9em;display:block;overflow-x:auto}.reader-prose th,.reader-prose td{border:1px solid var(--color-border);text-align:left;vertical-align:top;padding:.4em .7em}.reader-prose th{background:var(--color-surface-elevated);font-weight:600}.reader-prose thead th{border-bottom-width:2px}.reader-prose tbody tr:nth-child(2n){background:var(--color-surface-elevated)}@supports (color:color-mix(in lab, red, red)){.reader-prose tbody tr:nth-child(2n){background:color-mix(in oklch, var(--color-surface-elevated) 55%, transparent)}}.reader-prose caption{caption-side:bottom;color:var(--color-text-muted);text-align:left;margin-top:.5em;font-size:.85em}}@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}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.top-0{top: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-1\/2{left:50%}.left-2\.5{left:calc(var(--spacing) * 2.5)}.z-10{z-index:10}.z-20{z-index:20}.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-3{margin-left:calc(var(--spacing) * 3)}.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-0{height:calc(var(--spacing) * 0)}.h-0\.5{height:calc(var(--spacing) * .5)}.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}.origin-left{transform-origin:0}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.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-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-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}}
|