nixamp 0.26.7 → 0.27.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/README.md +78 -6
- package/dist/live-interpreter.d.ts +70 -0
- package/dist/live-interpreter.js +243 -0
- package/dist/live-voice.d.ts +5 -2
- package/dist/live-voice.js +35 -6
- package/dist/pcm-wav.d.ts +2 -0
- package/dist/pcm-wav.js +27 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +136 -7
- package/dist/shared-translation.d.ts +55 -0
- package/dist/shared-translation.js +335 -0
- package/dist/translation-passes.d.ts +97 -0
- package/dist/translation-passes.js +227 -0
- package/dist/web-sites.d.ts +6 -0
- package/dist/web-sites.js +26 -0
- package/package.json +1 -1
- package/src/live-interpreter.ts +194 -0
- package/src/live-voice.ts +33 -6
- package/src/pcm-wav.ts +26 -0
- package/src/server.ts +96 -6
- package/src/shared-translation.ts +218 -0
- package/src/translation-passes.ts +232 -0
- package/src/web-sites.ts +29 -0
- package/web/dist/assets/{hls-3VKVEQE3-BVogqLei.js → hls-3VKVEQE3-P3sj2d3a.js} +1 -1
- package/web/dist/assets/{index-biLbPIu_.css → index-BmR7T0zX.css} +1 -1
- package/web/dist/assets/index-Do9VYiU8.js +3 -0
- package/web/dist/assets/{mpegts-mpVD5UG-.js → mpegts-CZ1YoF1U.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-CsNrXUYy.js → mpegts-LO6RVLD6-DuNM0Zxm.js} +1 -1
- package/web/dist/index.html +20 -2
- package/web/dist/sw.js +6 -6
- package/web/dist/assets/index-DMn-qhqH.js +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/** Account-bound, prepaid translation credit. Money is integer micro-USD;
|
|
2
|
+
* each listening account reserves 5x base provider cost for paid access. */
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import type { Queryable } from "./follows.ts";
|
|
5
|
+
import { SpeechError } from "./speech.ts";
|
|
6
|
+
|
|
7
|
+
export const TRANSLATION_MULTIPLIER = 5;
|
|
8
|
+
export const TRANSLATION_PLANS = [
|
|
9
|
+
{ id: "day", name: "Day", days: 1, priceCents: 500 },
|
|
10
|
+
{ id: "week", name: "Week", days: 7, priceCents: 2500 },
|
|
11
|
+
{ id: "month", name: "Month", days: 30, priceCents: 10000 },
|
|
12
|
+
] as const;
|
|
13
|
+
export type TranslationPlan = typeof TRANSLATION_PLANS[number];
|
|
14
|
+
export type TranslationUsage = "transcription" | "voice";
|
|
15
|
+
export interface TranslationMeter {
|
|
16
|
+
require(by: string): Promise<void>;
|
|
17
|
+
eligible?(accounts: string[]): Promise<string[]>;
|
|
18
|
+
reserve(by: string, kind: TranslationUsage, units: number): Promise<string>;
|
|
19
|
+
reserveMany?(accounts: string[], kind: TranslationUsage, units: number): Promise<{ by: string; id: string }[]>;
|
|
20
|
+
commitMany?(ids: string[]): Promise<void>;
|
|
21
|
+
refundMany?(ids: string[]): Promise<void>;
|
|
22
|
+
commit(id: string): Promise<void>;
|
|
23
|
+
refund(id: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface TranslationAccess {
|
|
26
|
+
required: boolean; available: boolean; balanceMicros: number; expires: string | null;
|
|
27
|
+
plans: readonly TranslationPlan[]; coins: string[]; orders: { id: string; url: string; plan: string }[];
|
|
28
|
+
rates: { transcriptionHour: number; voiceThousand: number; multiplier: number };
|
|
29
|
+
}
|
|
30
|
+
const COINS = ["USDC_POL", "USDC_SOL", "SOL", "POL", "BTC", "ETH", "USDC_ETH", "BCH", "DOGE"];
|
|
31
|
+
const ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
32
|
+
const DAY = 86_400_000;
|
|
33
|
+
const PAID = new Set(["confirmed", "forwarding", "forwarded", "forwarding_failed", "completed"]);
|
|
34
|
+
|
|
35
|
+
/** At the published rates: Scribe $0.22/submitted hour, Flash $0.05/1k chars.
|
|
36
|
+
* Transcription units are 16 kHz samples; overlap is charged exactly as sent. */
|
|
37
|
+
export function translationCost(kind: TranslationUsage, units: number): { cost: number; charge: number } {
|
|
38
|
+
if (!["voice", "transcription"].includes(kind) || !Number.isSafeInteger(units) || units < 1 || (kind === "voice" ? units > 600 : units > 241600)) throw new SpeechError("invalid translation usage", 400);
|
|
39
|
+
const cost = Math.ceil(kind === "voice" ? units * 50 : units * 220_000 / 57_600_000);
|
|
40
|
+
return { cost, charge: cost * TRANSLATION_MULTIPLIER };
|
|
41
|
+
}
|
|
42
|
+
function cents(value: unknown): number {
|
|
43
|
+
if (typeof value !== "number" && typeof value !== "string") return -1;
|
|
44
|
+
const text = String(value);
|
|
45
|
+
if (!/^\d+(?:\.\d{1,2}0*)?$/.test(text)) return -1;
|
|
46
|
+
const result = Math.round(Number(text) * 100);
|
|
47
|
+
return Number.isSafeInteger(result) ? result : -1;
|
|
48
|
+
}
|
|
49
|
+
export function verifyTranslationPayment(payment: Record<string, unknown>, id: string, priceCents: number): boolean {
|
|
50
|
+
return payment["id"] === id && PAID.has(String(payment["status"])) &&
|
|
51
|
+
String(payment["currency"]).toUpperCase() === "USD" && cents(payment["amount"]) === priceCents;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class TranslationPasses implements TranslationMeter {
|
|
55
|
+
private schema: Promise<void> | null = null;
|
|
56
|
+
private merchant: Promise<{ business: string; coins: string[] }> | null = null;
|
|
57
|
+
private merchantUntil = 0;
|
|
58
|
+
private readonly fetcher: typeof fetch;
|
|
59
|
+
private readonly now: () => number;
|
|
60
|
+
constructor(private readonly options: { db: Queryable; key: string; site: string; fetcher?: typeof fetch; now?: () => number }) {
|
|
61
|
+
this.fetcher = options.fetcher ?? fetch; this.now = options.now ?? Date.now;
|
|
62
|
+
}
|
|
63
|
+
private async ensure(): Promise<void> {
|
|
64
|
+
this.schema ??= (async () => {
|
|
65
|
+
await this.options.db.query(`CREATE TABLE IF NOT EXISTS translation_wallets (
|
|
66
|
+
by_account TEXT PRIMARY KEY, balance_micros BIGINT NOT NULL CHECK (balance_micros >= 0), expires_at TIMESTAMPTZ NOT NULL)`);
|
|
67
|
+
await this.options.db.query(`CREATE TABLE IF NOT EXISTS translation_orders (
|
|
68
|
+
id TEXT PRIMARY KEY, by_account TEXT NOT NULL, request_key TEXT NOT NULL, plan TEXT NOT NULL,
|
|
69
|
+
price_cents INTEGER NOT NULL CHECK (price_cents > 0), days INTEGER NOT NULL,
|
|
70
|
+
business_id TEXT NOT NULL, coin TEXT NOT NULL, payment_id TEXT UNIQUE,
|
|
71
|
+
status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ NOT NULL,
|
|
72
|
+
credited_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, UNIQUE(by_account, request_key))`);
|
|
73
|
+
await this.options.db.query(`CREATE TABLE IF NOT EXISTS translation_usage (
|
|
74
|
+
id TEXT PRIMARY KEY, by_account TEXT NOT NULL, kind TEXT NOT NULL, units BIGINT NOT NULL,
|
|
75
|
+
cost_micros BIGINT NOT NULL, charge_micros BIGINT NOT NULL, status TEXT NOT NULL DEFAULT 'reserved',
|
|
76
|
+
created_at TIMESTAMPTZ NOT NULL)`);
|
|
77
|
+
await this.options.db.query(`CREATE TABLE IF NOT EXISTS translation_checkout_limits (bucket TEXT PRIMARY KEY, attempts INTEGER NOT NULL)`);
|
|
78
|
+
await this.options.db.query("CREATE INDEX IF NOT EXISTS translation_orders_account ON translation_orders (by_account, created_at)");
|
|
79
|
+
})().catch(error => { this.schema = null; throw error; });
|
|
80
|
+
await this.schema;
|
|
81
|
+
}
|
|
82
|
+
private async provider(path: string, init: RequestInit = {}): Promise<any> {
|
|
83
|
+
if (!this.options.key) throw new SpeechError("Translation checkout is unavailable.", 503);
|
|
84
|
+
const response = await this.fetcher(`https://coinpayportal.com${path}`, {
|
|
85
|
+
...init, signal: AbortSignal.timeout(15_000), headers: { authorization: `Bearer ${this.options.key}`, "content-type": "application/json", ...init.headers },
|
|
86
|
+
});
|
|
87
|
+
const body = await response.json();
|
|
88
|
+
if (!response.ok || body.success !== true) throw new SpeechError("The payment service is unavailable. Your purchase has not been activated; retry to check its status.", 502);
|
|
89
|
+
return body;
|
|
90
|
+
}
|
|
91
|
+
private async settings(): Promise<{ business: string; coins: string[] }> {
|
|
92
|
+
if (!this.merchant || this.now() >= this.merchantUntil) {
|
|
93
|
+
this.merchantUntil = this.now() + 600_000;
|
|
94
|
+
this.merchant = this.provider("/api/supported-coins").then(body => {
|
|
95
|
+
if (!ID.test(body.business_id)) throw new SpeechError("Translation checkout is unavailable.", 503);
|
|
96
|
+
const coins = COINS.filter(symbol => body.coins?.some((coin: any) => coin.symbol === symbol && coin.is_active && coin.has_wallet));
|
|
97
|
+
return { business: body.business_id as string, coins };
|
|
98
|
+
}).catch(error => { this.merchant = null; throw error; });
|
|
99
|
+
}
|
|
100
|
+
return this.merchant;
|
|
101
|
+
}
|
|
102
|
+
async access(by?: string): Promise<TranslationAccess> {
|
|
103
|
+
await this.ensure();
|
|
104
|
+
const config = await this.settings().catch(() => ({ business: "", coins: [] as string[] }));
|
|
105
|
+
const row = by ? (await this.options.db.query("SELECT balance_micros, expires_at FROM translation_wallets WHERE by_account = $1 AND expires_at > $2", [by, new Date(this.now())])).rows[0] : null;
|
|
106
|
+
const orders = by ? (await this.options.db.query("SELECT id, payment_id, plan FROM translation_orders WHERE by_account = $1 AND status = 'pending' AND payment_id IS NOT NULL ORDER BY created_at DESC LIMIT 5", [by])).rows.map(order => ({ id: String(order["id"]), plan: String(order["plan"]), url: `https://coinpayportal.com/pay/${order["payment_id"]}` })) : [];
|
|
107
|
+
return { required: true, available: config.coins.length > 0, balanceMicros: Number(row?.["balance_micros"] ?? 0),
|
|
108
|
+
expires: row ? new Date(String(row["expires_at"])).toISOString() : null, plans: TRANSLATION_PLANS, coins: config.coins, orders,
|
|
109
|
+
rates: { transcriptionHour: 0.22, voiceThousand: 0.05, multiplier: TRANSLATION_MULTIPLIER } };
|
|
110
|
+
}
|
|
111
|
+
async require(by: string): Promise<void> {
|
|
112
|
+
await this.ensure();
|
|
113
|
+
const found = await this.options.db.query("SELECT by_account FROM translation_wallets WHERE by_account = $1 AND expires_at > $2 AND balance_micros > 0", [by, new Date(this.now())]);
|
|
114
|
+
if (!found.rows.length) throw new SpeechError("Buy a translation pass to enable translated audio.", 402);
|
|
115
|
+
}
|
|
116
|
+
async eligible(accounts: string[]): Promise<string[]> {
|
|
117
|
+
await this.ensure();
|
|
118
|
+
const result = await this.options.db.query("SELECT by_account FROM translation_wallets WHERE by_account = ANY($1::text[]) AND expires_at > $2 AND balance_micros > 0", [accounts, new Date(this.now())]);
|
|
119
|
+
return result.rows.map(row => String(row["by_account"]));
|
|
120
|
+
}
|
|
121
|
+
async reserve(by: string, kind: TranslationUsage, units: number): Promise<string> {
|
|
122
|
+
await this.ensure();
|
|
123
|
+
const { cost, charge } = translationCost(kind, units), id = randomUUID();
|
|
124
|
+
const result = await this.options.db.query(`WITH debit AS (
|
|
125
|
+
UPDATE translation_wallets SET balance_micros = balance_micros - $2
|
|
126
|
+
WHERE by_account = $1 AND expires_at > $3 AND balance_micros >= $2 RETURNING by_account)
|
|
127
|
+
INSERT INTO translation_usage (id, by_account, kind, units, cost_micros, charge_micros, created_at)
|
|
128
|
+
SELECT $4, by_account, $5, $6, $7, $2, $3 FROM debit RETURNING id`,
|
|
129
|
+
[by, charge, new Date(this.now()), id, kind, units, cost]);
|
|
130
|
+
if (!result.rows.length) throw new SpeechError("Your translation balance is used up or expired. Buy another pass to continue.", 402);
|
|
131
|
+
return id;
|
|
132
|
+
}
|
|
133
|
+
/** Batch a shared stream's access charges in one database round trip. */
|
|
134
|
+
async reserveMany(accounts: string[], kind: TranslationUsage, units: number): Promise<{ by: string; id: string }[]> {
|
|
135
|
+
await this.ensure();
|
|
136
|
+
const { cost, charge } = translationCost(kind, units);
|
|
137
|
+
const result = await this.options.db.query(`WITH debit AS (
|
|
138
|
+
UPDATE translation_wallets SET balance_micros = balance_micros - $2
|
|
139
|
+
WHERE by_account = ANY($1::text[]) AND expires_at > $3 AND balance_micros >= $2 RETURNING by_account)
|
|
140
|
+
INSERT INTO translation_usage (id, by_account, kind, units, cost_micros, charge_micros, created_at)
|
|
141
|
+
SELECT gen_random_uuid()::text, by_account, $4, $5, $6, $2, $3 FROM debit RETURNING id, by_account`,
|
|
142
|
+
[[...new Set(accounts)], charge, new Date(this.now()), kind, units, cost]);
|
|
143
|
+
return result.rows.map(row => ({ by: String(row["by_account"]), id: String(row["id"]) }));
|
|
144
|
+
}
|
|
145
|
+
async commit(id: string): Promise<void> {
|
|
146
|
+
await this.options.db.query("UPDATE translation_usage SET status = 'charged' WHERE id = $1 AND status = 'reserved'", [id]);
|
|
147
|
+
}
|
|
148
|
+
async refund(id: string): Promise<void> {
|
|
149
|
+
await this.options.db.query(`WITH undone AS (
|
|
150
|
+
UPDATE translation_usage SET status = 'refunded' WHERE id = $1 AND status = 'reserved'
|
|
151
|
+
RETURNING by_account, charge_micros)
|
|
152
|
+
UPDATE translation_wallets AS wallet SET balance_micros = wallet.balance_micros + undone.charge_micros
|
|
153
|
+
FROM undone WHERE wallet.by_account = undone.by_account`, [id]);
|
|
154
|
+
}
|
|
155
|
+
async commitMany(ids: string[]): Promise<void> {
|
|
156
|
+
await this.options.db.query("UPDATE translation_usage SET status = 'charged' WHERE id = ANY($1::text[]) AND status = 'reserved'", [ids]);
|
|
157
|
+
}
|
|
158
|
+
async refundMany(ids: string[]): Promise<void> {
|
|
159
|
+
await this.options.db.query(`WITH undone AS (
|
|
160
|
+
UPDATE translation_usage SET status = 'refunded' WHERE id = ANY($1::text[]) AND status = 'reserved'
|
|
161
|
+
RETURNING by_account, charge_micros), totals AS (SELECT by_account, SUM(charge_micros) AS charge FROM undone GROUP BY by_account)
|
|
162
|
+
UPDATE translation_wallets AS wallet SET balance_micros = wallet.balance_micros + totals.charge
|
|
163
|
+
FROM totals WHERE wallet.by_account = totals.by_account`, [ids]);
|
|
164
|
+
}
|
|
165
|
+
async checkout(by: string, planId: string, coin: string, requestKey: string): Promise<{ id: string; url: string }> {
|
|
166
|
+
await this.ensure();
|
|
167
|
+
const plan = TRANSLATION_PLANS.find(plan => plan.id === planId);
|
|
168
|
+
if (!plan || !ID.test(requestKey)) throw new SpeechError("Choose a translation pass.", 400);
|
|
169
|
+
const config = await this.settings();
|
|
170
|
+
if (!config.coins.includes(coin)) throw new SpeechError("Choose an available payment currency.", 400);
|
|
171
|
+
// Durable caps across restarts/replicas protect the merchant's invoice quota.
|
|
172
|
+
// Retrying an existing order does not consume another invoice allowance.
|
|
173
|
+
const existing = await this.options.db.query("SELECT id FROM translation_orders WHERE by_account = $1 AND request_key = $2", [by, requestKey]);
|
|
174
|
+
if (!existing.rows.length) {
|
|
175
|
+
for (const [scope, limit] of [[`account:${by}`, 5], ["server", 50]] as const) {
|
|
176
|
+
const limited = await this.options.db.query(`INSERT INTO translation_checkout_limits (bucket, attempts) VALUES ($1, 1)
|
|
177
|
+
ON CONFLICT (bucket) DO UPDATE SET attempts = translation_checkout_limits.attempts + 1
|
|
178
|
+
WHERE translation_checkout_limits.attempts < $2 RETURNING attempts`, [`${scope}:${Math.floor(this.now() / DAY)}`, limit]);
|
|
179
|
+
if (!limited.rows.length) throw new SpeechError("Too many new checkouts today. Resume an existing purchase or try tomorrow.", 429);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const id = randomUUID();
|
|
183
|
+
await this.options.db.query(`INSERT INTO translation_orders (id, by_account, request_key, plan, price_cents, days, business_id, coin, created_at)
|
|
184
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT (by_account, request_key) DO NOTHING`,
|
|
185
|
+
[id, by, requestKey, plan.id, plan.priceCents, plan.days, config.business, coin, new Date(this.now())]);
|
|
186
|
+
const order = (await this.options.db.query("SELECT * FROM translation_orders WHERE by_account = $1 AND request_key = $2", [by, requestKey])).rows[0]!;
|
|
187
|
+
if (order["plan"] !== plan.id || order["coin"] !== coin) throw new SpeechError("This checkout already belongs to a different pass. Start a new purchase.", 409);
|
|
188
|
+
let paymentId = String(order["payment_id"] ?? "");
|
|
189
|
+
if (!paymentId) {
|
|
190
|
+
const created = await this.provider("/api/payments/create", {
|
|
191
|
+
method: "POST", headers: { "idempotency-key": `nixamp-translation-${order["id"]}` },
|
|
192
|
+
body: JSON.stringify({ business_id: order["business_id"], amount_usd: Number(order["price_cents"]) / 100,
|
|
193
|
+
currency: coin.toLowerCase(), payment_method: "crypto", description: `Nixamp translated audio: ${plan.name} pass`,
|
|
194
|
+
metadata: { app: "nixamp", product: "translation", order_id: order["id"] },
|
|
195
|
+
redirect_url: `${this.options.site}/?translation_order=${order["id"]}` }),
|
|
196
|
+
});
|
|
197
|
+
paymentId = created.payment?.id;
|
|
198
|
+
if (!ID.test(paymentId ?? "")) throw new SpeechError("Checkout did not return a valid payment. Retry this purchase.", 502);
|
|
199
|
+
await this.options.db.query("UPDATE translation_orders SET payment_id = $2 WHERE id = $1 AND payment_id IS NULL", [order["id"], paymentId]);
|
|
200
|
+
}
|
|
201
|
+
return { id: String(order["id"]), url: `https://coinpayportal.com/pay/${paymentId}` };
|
|
202
|
+
}
|
|
203
|
+
async check(by: string, id: string): Promise<{ status: string; access: TranslationAccess }> {
|
|
204
|
+
if (!ID.test(id)) throw new SpeechError("Purchase not found.", 404);
|
|
205
|
+
await this.ensure();
|
|
206
|
+
const order = (await this.options.db.query("SELECT * FROM translation_orders WHERE id = $1 AND by_account = $2", [id, by])).rows[0];
|
|
207
|
+
if (!order) throw new SpeechError("Purchase not found.", 404);
|
|
208
|
+
let status = String(order["status"]);
|
|
209
|
+
if (["pending", "expired"].includes(status) && order["payment_id"]) {
|
|
210
|
+
const result = await this.provider(`/api/payments/${encodeURIComponent(String(order["payment_id"]))}`);
|
|
211
|
+
const payment = result.payment;
|
|
212
|
+
if (payment && verifyTranslationPayment(payment, String(order["payment_id"]), Number(order["price_cents"]))) {
|
|
213
|
+
const now = new Date(this.now()), expires = new Date(this.now() + Number(order["days"]) * DAY);
|
|
214
|
+
// One statement atomically marks the order and adds credit. A retry,
|
|
215
|
+
// second tab or second replica cannot activate the same money twice.
|
|
216
|
+
await this.options.db.query(`WITH paid AS (
|
|
217
|
+
UPDATE translation_orders SET status = 'paid', credited_at = $3, expires_at = $4
|
|
218
|
+
WHERE id = $1 AND by_account = $2 AND status IN ('pending', 'expired') RETURNING by_account, price_cents)
|
|
219
|
+
INSERT INTO translation_wallets (by_account, balance_micros, expires_at)
|
|
220
|
+
SELECT by_account, price_cents::bigint * 10000, $4 FROM paid
|
|
221
|
+
ON CONFLICT (by_account) DO UPDATE SET
|
|
222
|
+
balance_micros = CASE WHEN translation_wallets.expires_at > $3 THEN translation_wallets.balance_micros ELSE 0 END + EXCLUDED.balance_micros,
|
|
223
|
+
expires_at = GREATEST(translation_wallets.expires_at, EXCLUDED.expires_at)`, [id, by, now, expires]);
|
|
224
|
+
status = "paid";
|
|
225
|
+
} else if (payment && ["expired", "failed", "refunded"].includes(payment.status)) {
|
|
226
|
+
status = payment.status;
|
|
227
|
+
await this.options.db.query("UPDATE translation_orders SET status = $3 WHERE id = $1 AND by_account = $2 AND status = 'pending'", [id, by, status]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { status, access: await this.access(by) };
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/web-sites.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface WebSite {
|
|
5
|
+
web: string;
|
|
6
|
+
site: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Explicit origins only: a caller's Host header must never become an invite URL. */
|
|
10
|
+
export function webSitesFromEnv(value = process.env["NIXAMP_WEB_SITES"]): Map<string, WebSite> {
|
|
11
|
+
const sites = new Map<string, WebSite>();
|
|
12
|
+
if (!value) return sites;
|
|
13
|
+
const entries: unknown = JSON.parse(value);
|
|
14
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
15
|
+
throw new Error("NIXAMP_WEB_SITES must map public origins to web directories");
|
|
16
|
+
}
|
|
17
|
+
for (const [origin, directory] of Object.entries(entries)) {
|
|
18
|
+
const url = new URL(origin);
|
|
19
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password
|
|
20
|
+
|| url.pathname !== "/" || url.search || url.hash || typeof directory !== "string") {
|
|
21
|
+
throw new Error(`Invalid web site: ${origin}`);
|
|
22
|
+
}
|
|
23
|
+
const web = resolve(directory);
|
|
24
|
+
if (!existsSync(join(web, "index.html"))) throw new Error(`Missing web build: ${web}`);
|
|
25
|
+
if (sites.has(url.host)) throw new Error(`Duplicate web host: ${url.host}`);
|
|
26
|
+
sites.set(url.host, { web, site: url.origin });
|
|
27
|
+
}
|
|
28
|
+
return sites;
|
|
29
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./index-
|
|
1
|
+
import{t as e}from"./index-Do9VYiU8.js";var t=3;async function n(n){let{media:r,src:i,isTv:a}=n,{default:o}=await e(async()=>{let{default:e}=await import(`./hls-n74Cnh8A.js`);return{default:e}},[]);if(!o.isSupported())return n.onError(`This browser cannot play HLS streams.`),{destroy:()=>void 0,levels:()=>[]};let s=new o({...a?{maxBufferLength:60,maxMaxBufferLength:120,backBufferLength:30,liveSyncDurationCount:4}:{backBufferLength:90},enableWorker:!0}),c=0,l=!1;s.on(o.Events.ERROR,(e,r)=>{if(!l&&r.fatal){if(c>=t){n.onError(`This stream kept failing and has been stopped.`),s.destroy();return}switch(c+=1,r.type){case o.ErrorTypes.NETWORK_ERROR:n.onNotice(`Reconnecting…`),s.startLoad();break;case o.ErrorTypes.MEDIA_ERROR:n.onNotice(`Recovering…`),s.recoverMediaError();break;default:n.onError(`This stream could not be played.`),s.destroy()}}}),s.on(o.Events.MANIFEST_PARSED,()=>{l||(n.onNotice(null),n.onReady?.({live:s.levels.length>0&&!Number.isFinite(r.duration),levels:u()}))}),s.on(o.Events.LEVEL_LOADED,(e,t)=>{l||n.onReady?.({live:t.details.live,levels:u()})}),s.on(o.Events.FRAG_BUFFERED,()=>{l||n.onNotice(null)});function u(){return s.levels.map((e,t)=>({index:t,height:e.height||null,bitrate:e.bitrate||null,label:e.height?`${String(e.height)}p`:`${String(Math.round((e.bitrate||0)/1e3))}k`}))}return s.loadSource(i),s.attachMedia(r),{destroy(){l=!0,s.destroy()},levels:u,setLevel(e){s.currentLevel=e},currentLevel:()=>s.autoLevelEnabled?-1:s.currentLevel}}export{n as createHlsEngine};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--bg:#080c09;--panel:#0c120e;--edge:#1d2c22;--green:#4af689;--green-dim:#55a97b;--fg:#cfe8d8;--muted:#819d8c;--warn:#e8c35a;--accent:#7ef0c4;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}input:not([type=range]):not([type=file]),select,textarea{min-height:28px}input[type=checkbox],input[type=radio]{vertical-align:middle;min-width:24px;min-height:24px}[hidden]{display:none!important}html,body{background:var(--bg);min-height:100%;color:var(--fg);margin:0;font:14px/1.45 ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace}body{background-image:repeating-linear-gradient(#4af68906 0 1px,#0000 1px 3px)}#app{max-width:1100px;padding:calc(12px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right)) calc(16px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left));flex-direction:column;gap:10px;margin:0 auto;display:flex}.welcome-title{color:var(--green);margin:4px 0 8px;font-size:22px;font-weight:600;line-height:1.25}.welcome-lead{max-width:70ch;color:var(--fg);margin:0 0 14px}.welcome-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin-bottom:14px;display:grid}.welcome-grid h2{letter-spacing:.06em;text-transform:uppercase;color:var(--green-dim);margin:0 0 4px;font-size:13px}.welcome-grid p{color:var(--muted);margin:0;font-size:13px}.welcome-grid code,.welcome-install{color:var(--accent)}.welcome-actions{gap:10px}.welcome-install{border:1px dashed var(--edge);overflow-wrap:anywhere;border-radius:4px;padding:6px 10px;font-size:12px}.welcome .button.primary{color:var(--green);border-color:var(--green-dim)}@media (max-width:720px){.welcome-grid{grid-template-columns:1fr}.welcome-title{font-size:19px}}.bar{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.brand{color:var(--green);letter-spacing:.14em;align-items:center;gap:6px;font-weight:700;display:inline-flex}.brand-mark{flex:none;width:1.15em;height:1.15em}.status{color:var(--muted)}.status[data-playing=true]{color:var(--green)}.status[data-playing=loading]{color:var(--accent)}.status[data-playing=loading]:before,.row.loading>.name:after{content:"";vertical-align:-.1em;border:2px solid;border-right-color:#0000;border-radius:50%;width:.8em;height:.8em;animation:.8s linear infinite turn;display:inline-block}.status[data-playing=loading]:before{margin-right:6px}.row.loading>.name:after{color:var(--accent);margin-left:8px}@keyframes turn{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.status[data-playing=loading]:before,.row.loading>.name:after{animation-duration:2.4s}}.chip{color:var(--muted);border:1px solid var(--edge);text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:46vw;margin-left:auto;padding:1px 10px;font-size:12px;overflow:hidden}.panel{background:linear-gradient(var(--panel), var(--panel)) 0 9px / 100% calc(100% - 9px) no-repeat;min-width:0;padding:22px 12px 12px;position:relative}.panel:after{content:"";border:1px solid var(--edge);pointer-events:none;border-radius:6px;position:absolute;inset:9px 0 0}.panel:before{content:none}.panel>.panel-heading{white-space:nowrap;text-overflow:ellipsis;background:var(--panel);max-width:calc(100% - 108px);color:var(--green-dim);letter-spacing:.06em;z-index:1;margin:0;padding:0 6px;font-size:12px;font-weight:400;line-height:18px;position:absolute;top:0;left:10px;overflow:hidden}.panel-tools{background:var(--panel);z-index:1;align-items:center;gap:2px;height:18px;padding:0 4px;display:flex;position:absolute;top:0;right:8px}.panel-tools button{font:inherit;border:1px solid var(--edge);height:16px;color:var(--muted);cursor:pointer;background:#121a15;border-radius:3px;padding:0 5px;font-size:11px;line-height:1}.panel-tools button:hover{color:var(--fg);border-color:var(--green-dim)}.panel-tools .grip{cursor:grab}.panel[data-collapsed]{padding:18px 12px 0}.panel[data-collapsed]>:not(.panel-tools):not(.panel-heading),.panel[data-closed]{display:none!important}.panel.dragging{opacity:.45}.panel.drop-before{box-shadow:0 -4px 0 0 var(--accent)}.panel.drop-after{box-shadow:0 4px 0 0 var(--accent)}.link-note{color:var(--warn);margin:6px 0 0}.log-list{max-height:220px;margin:0;padding:0;font-size:12px;list-style:none;overflow-y:auto}.log-list li{border-bottom:1px dotted var(--edge);align-items:baseline;gap:8px;padding:2px 0;display:flex}.log-list time{color:var(--muted);flex:none}.log-list span{overflow-wrap:anywhere;min-width:0}.trollbox-list{max-height:320px;margin:0 0 8px;padding:0;font-size:13px;list-style:none;overflow-y:auto}.trollbox-list li{border-bottom:1px dotted var(--edge);align-items:baseline;gap:6px;padding:2px 0;display:flex}.trollbox-list .when{color:var(--muted);flex:none;font-size:11px}.trollbox-list .who{color:var(--accent);flex:none}.trollbox-list .who:after{content:":"}.trollbox-list .line{overflow-wrap:anywhere;flex:auto;min-width:0}.button.mic{min-width:44px}.button.mic[aria-pressed=true]{color:#ff6b6b;border-color:#ff6b6b;animation:1.2s ease-in-out infinite listening}.button.mic[disabled]{opacity:.6}@keyframes listening{50%{opacity:.55}}.transcript-list{max-height:320px;margin:0;padding:0;font-size:13px;list-style:none;overflow-y:auto}.transcript-list li{border-bottom:1px dotted var(--edge);align-items:baseline;gap:6px;padding:2px 0;display:flex}.transcript-list .when{color:var(--muted);flex:none;font-size:11px}.transcript-list .line{overflow-wrap:anywhere;flex:auto;min-width:0}.transcript-list li.now .line{color:var(--accent)}.transcript-switch{color:var(--muted);align-items:center;gap:6px;margin:0 0 6px;font-size:12px;display:inline-flex}.transcript-switch+.transcript-switch{margin-left:12px}.transcript-language{font:inherit;color:inherit;border:1px solid var(--edge);background:0 0;border-radius:3px;max-width:12em;padding:1px 4px;font-size:12px}.transcript-list .lang{color:var(--muted);flex:none;font-size:11px}.transcript-actions{flex-wrap:wrap;gap:6px;margin:4px 0 10px;display:flex}.transcript-audio{border-top:1px solid var(--edge);padding-top:8px}.transcript-audio select,#transcript-speakers select{max-width:min(18em,100%);font:inherit;color:var(--fg);background:var(--panel);border:1px solid var(--edge);border-radius:3px;padding:3px}.transcript-audio .transcript-switch,#transcript-speakers .transcript-switch{flex-wrap:wrap;max-width:100%;margin-left:0;margin-right:10px}#transcript-speakers{max-height:150px;overflow:auto}.subtitle{z-index:2;pointer-events:none;text-align:center;color:#fff;text-shadow:0 1px 2px #000;background:#000000a8;border-radius:4px;padding:4px 10px;font-size:clamp(14px,2.4vw,22px);line-height:1.3;position:absolute;left:12px;right:12px}body.tv .subtitle{font-size:28px}.panels-list li{align-items:center;gap:8px;padding:3px 0;display:flex}.panels-list li label{cursor:pointer;flex:auto;align-items:center;gap:8px;display:flex}.panels-list li .name{flex:auto}.panels-list li.off .name{color:var(--muted)}.panels-list li .detail{color:var(--muted);white-space:nowrap;font-size:12px}.split{columns:2;column-gap:10px}.split>*{break-inside:avoid;width:100%;margin-bottom:14px;display:block}@supports (display:grid-lanes){.split{display:grid-lanes;columns:auto;grid-template-columns:1.3fr 1fr;gap:14px 10px}.split>*{margin-bottom:0}.split>.col-a{grid-column:1}.split>.col-b{grid-column:2}}.directory-list li.onair{border-bottom:1px solid var(--edge);flex-direction:column;align-items:stretch;gap:6px;padding:6px 0}.directory-list li.onair:last-child{border-bottom:0}.directory-list li.onair>.recent-label{flex:none}.directory-list li.onair .detail{white-space:normal;overflow-wrap:anywhere}.onair-actions{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.icon{border:1px solid var(--edge);min-width:2.2em;height:2.2em;color:var(--muted);cursor:pointer;background:0 0;border-radius:3px;flex:none;padding:0 6px;font-size:1em;line-height:1}.icon:hover{color:var(--accent);border-color:var(--accent)}.directory-list li>.button:disabled,.directory-list li>.way-in:disabled{opacity:.4;cursor:not-allowed}.directory-list li>.way-in:disabled:hover{color:var(--muted);border-color:var(--edge)}.directory-list li>.way-in{color:var(--fg);flex:none;align-self:center;padding:0 8px}.directory-list li>.way-in svg{width:18px;height:18px}.heart{color:var(--muted);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:3px;padding:2px 6px;font-size:1.1em;line-height:1}.heart:hover{color:var(--accent);border-color:var(--accent)}.heart[data-on=yes]{color:var(--accent)}.directory-list li>span.server-label{flex-direction:column;flex:1 1 0;gap:2px;min-width:0;padding:4px 2px;display:flex}.directory-list li>.server-label .name{font-weight:600}.directory-list li>.server-label .detail{color:var(--muted);white-space:nowrap}.directory-list li>.server-label .live{color:var(--green)}@media (max-width:720px){.split{columns:1}@supports (display:grid-lanes){.split{grid-template-columns:1fr}.split>.col-a,.split>.col-b{grid-column:auto}}}.track-title{color:var(--accent);text-overflow:ellipsis;white-space:nowrap;font-weight:700;overflow:hidden}.track-sub{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.track-title.with-context,.track-sub.with-context{white-space:normal;overflow-wrap:anywhere}.track-live{color:var(--muted);overflow-wrap:anywhere;margin-top:6px;line-height:1.5}.track-live b{color:var(--green);font-weight:700}.track-meta{flex-wrap:wrap;align-items:center;gap:6px;margin-top:6px;margin-bottom:4px;display:flex}.meta-chip{color:var(--muted);border:1px solid var(--edge);letter-spacing:.04em;text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:100%;padding:0 8px;font-size:11px;line-height:1.7;overflow:hidden}.meta-chip:first-of-type{color:var(--green);border-color:var(--green-dim)}.meta-logo{object-fit:contain;background:#000;border-radius:3px;max-width:64px;height:22px}.meta-logo.meta-poster{max-width:52px;height:72px}.live-art{object-fit:cover;vertical-align:middle;background:#000;border-radius:3px;flex:0 0 28px;width:28px;height:28px;margin-right:6px}.meta-score{flex-wrap:wrap;flex-basis:100%;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:1.4;display:flex}.meta-team{color:var(--fg);text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:38vw;overflow:hidden}.meta-points{font-variant-numeric:tabular-nums;font-size:16px;font-weight:700}.meta-dash{color:var(--muted);padding:0 2px}.meta-team-logo{object-fit:contain;flex:none;width:20px;height:20px}.meta-chip.chip-live{color:var(--fg);border-color:#b3261e}.meta-chip.chip-live:before{content:"";vertical-align:1px;background:#e53935;border-radius:50%;width:7px;height:7px;margin-right:5px;animation:1.4s ease-in-out infinite pulse;display:inline-block}@keyframes pulse{50%{opacity:.35}}@media (prefers-reduced-motion:reduce){.meta-chip.chip-live:before{animation:none}}.track-blurb{color:var(--muted);-webkit-line-clamp:2;-webkit-box-orient:vertical;margin:4px 0 0;font-size:12px;line-height:1.5;display:-webkit-box;overflow:hidden}#video{border:1px solid var(--edge);background:#000;border-radius:4px;width:100%;max-height:46vh;margin-bottom:8px}.scrub{align-items:center;gap:10px;margin-top:6px;display:flex}.time{color:var(--fg);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}input[type=range]{appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:14px}input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(var(--edge), var(--edge));border:1px solid var(--edge);border-radius:3px;height:6px}input[type=range]::-moz-range-track{background:var(--edge);border-radius:3px;height:6px}input[type=range]::-webkit-slider-thumb{appearance:none;background:var(--green);border-radius:2px;width:10px;height:16px;margin-top:-6px}input[type=range]::-moz-range-thumb{background:var(--green);border:0;border-radius:2px;width:10px;height:16px}input[type=range]:disabled{opacity:.45;cursor:default}#spectrum{border:1px solid var(--edge);background:#060a07;border-radius:4px;width:100%;height:190px;display:block}.meters{align-items:baseline;gap:10px;margin-top:6px;display:flex;overflow:hidden}.glyphs{color:var(--green);letter-spacing:1px;white-space:pre;flex:auto;min-width:0;min-height:1.4em;font-family:ui-monospace,SF Mono,Menlo,Consolas,monospace;overflow:hidden}.levelmeter{color:var(--accent);white-space:pre;font-variant-ligatures:none;text-align:right;flex:none;min-width:15ch;margin-left:auto;font-family:ui-monospace,SF Mono,Menlo,Consolas,monospace}.filter{border:1px solid var(--edge);width:100%;color:var(--fg);font:inherit;background:#0a100c;border-radius:4px;margin-bottom:6px;padding:4px 8px}.filter:focus{border-color:var(--accent);outline:none}.crumbs{color:var(--muted);flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:6px;font-size:12px;display:flex}.crumbs button{font:inherit;color:var(--accent);cursor:pointer;background:0 0;border:0;padding:0 2px}.crumbs button:hover{text-decoration:underline}.crumbs .here{color:var(--fg)}.catalog-tag{color:var(--muted);letter-spacing:.06em;flex:none;font-size:11px}.catalog-live{color:var(--green)}#catalogs-entries{max-height:320px;overflow-y:auto}#catalogs-list{max-height:240px}#catalogs-list li{align-items:center}#catalogs-list .name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}#catalogs-list .detail{opacity:1;color:var(--muted);font-size:11px}#catalogs-list button{min-height:24px;padding:2px 8px;font-size:12px}.folder{cursor:pointer;white-space:nowrap;color:var(--accent);border-radius:3px;gap:8px;padding:2px 6px;display:flex}.folder:hover{background:#142019}.folder .name{text-overflow:ellipsis;flex:1;overflow:hidden}.folder .count{color:var(--muted);flex:none}.playlist{scrollbar-color:var(--green-dim) transparent;max-height:214px;margin:0;padding:0;list-style:none;overflow-y:auto}.row{cursor:pointer;white-space:nowrap;border-radius:3px;gap:8px;padding:2px 6px;display:flex}.row:hover{background:#142019}.row.selected{background:#16241c}.row.selected .name{color:var(--accent)}.row.playing .name{color:var(--green)}.group{color:var(--muted);letter-spacing:.08em;text-transform:uppercase;border-top:1px solid var(--edge);align-items:center;gap:8px;margin-top:4px;padding:6px 6px 2px;font-size:11px;display:flex}.group:first-child{border-top:0;margin-top:0}.group-name{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.group-remove{border:1px solid var(--edge);color:var(--muted);cursor:pointer;background:0 0;border-radius:3px;flex:none;padding:0 6px;line-height:1.4}.group-remove:hover{color:var(--accent);border-color:var(--accent)}.row .n{color:var(--muted);text-align:right;flex:none;width:2.4em}.row .name{text-overflow:ellipsis;flex:1;overflow:hidden}.row .time{color:var(--muted);flex:none}.row-copy{color:var(--muted);cursor:pointer;opacity:.55;background:0 0;border:1px solid #0000;border-radius:3px;flex:none;padding:0 5px;line-height:1.4}.row:hover .row-copy,.row.selected .row-copy{opacity:1}.row-copy svg,.icon svg{vertical-align:-2px;width:14px;height:14px;display:inline-block}.icon{justify-content:center;align-items:center;display:inline-flex}.row-copy:hover{color:var(--accent);border-color:var(--accent)}.transport{border:1px solid var(--edge);background:var(--panel);border-radius:6px;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 10px;display:flex}button{font:inherit;color:var(--fg);border:1px solid var(--edge);cursor:pointer;background:#121a15;border-radius:4px;padding:6px 12px}button:hover{border-color:var(--green-dim);color:var(--green)}button:active{transform:translateY(1px)}button:focus-visible,input:focus-visible,a:focus-visible{outline:2px solid var(--green);outline-offset:2px}button.primary{color:var(--green);border-color:var(--green-dim);min-width:64px}button.ghost{color:var(--muted);background:0 0}button.share{color:var(--accent);border-color:var(--edge);letter-spacing:.04em}button.share:hover{background:#16241c}button.golive{color:#ff5c5c;letter-spacing:.06em;border-color:#7a2b2b}button.golive:hover{color:#fff;background:#7a2b2b;border-color:#ff5c5c}button.golive:disabled{opacity:.5;cursor:progress}.row-live:hover{color:#ff5c5c;border-color:#ff5c5c}.directory-list li:has(>.server-lives){flex-wrap:wrap}.directory-list li>.server-lives{border-left:2px solid var(--green-dim);flex-direction:column;flex:100%;gap:4px;margin:2px 0 0;padding:0 0 0 12px;list-style:none;display:flex}.server-lives li{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.server-lives li>.detail{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.server-lives li>.button{flex:none;padding:2px 10px;font-size:12px}.volume{flex:1;align-items:center;gap:8px;min-width:120px;max-width:220px;margin-left:auto;display:flex}.vol{color:var(--muted);letter-spacing:.08em;font-size:12px}.picker{flex-wrap:wrap;align-items:center;gap:8px;display:flex}#party-form label{color:var(--muted);flex-basis:100%}#party-code{flex:12ch;min-width:0}.link-form{margin-top:8px}.link-form input{flex:240px;min-width:0}.link-form select{min-width:0;font:inherit;color:inherit;background:#0b120e;border:1px solid #2a3a30;border-radius:4px;flex:0 180px;padding:6px 8px}.button{border:1px solid var(--edge);cursor:pointer;color:var(--fg);background:#121a15;border-radius:4px;padding:6px 12px;display:inline-block}.button:hover{border-color:var(--green-dim);color:var(--green)}.button:has(input[type=file]){position:relative}.button input[type=file]{opacity:0;cursor:pointer;width:100%;height:100%;position:absolute;inset:0}.button:focus-within{outline:2px solid var(--green);outline-offset:2px}#remote-url{font:inherit;min-width:12ch;color:var(--fg);border:1px solid var(--edge);background:#060a07;border-radius:4px;flex:1;padding:6px 10px}.check{color:var(--muted);align-items:center;gap:8px;margin-top:10px;display:flex}.check input{accent-color:var(--green)}.hint{color:var(--muted);margin:0 0 8px;font-size:13px}.hint code{color:var(--accent)}#remote-state[data-status=live]{color:var(--green)}#remote-state[data-status=connecting],#remote-state[data-status=error]{color:var(--warn)}.note{color:var(--warn);border-left:2px solid var(--warn);margin:0;padding-left:8px}.statusbar{color:var(--muted);border-top:1px solid var(--edge);flex-wrap:wrap;gap:14px;padding-top:8px;font-size:12px;display:flex}.statusbar b{color:var(--green);font-weight:700}.statusbar .tv-toggle{color:var(--accent);font:inherit;cursor:pointer;background:0 0;border:0;padding:0;text-decoration:underline}body.tv .statusbar .tv-toggle{color:var(--green)}.statusbar .spacer{flex:1}.statusbar a{color:var(--muted)}.directory{border-top:1px solid var(--line);margin-top:.6rem;padding-top:.6rem}.directory-list{max-height:12rem;margin:0;padding:0;list-style:none;overflow-y:auto}.directory-list li+li{margin-top:.3rem}.directory-list button{border:1px solid var(--line);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:4px;padding:.4rem .5rem}.directory-list button:hover,.directory-list button:focus-visible{border-color:var(--accent);background:#ffffff0a}.directory-list .name{color:var(--accent);display:block}.directory-list .detail{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:.85em;display:block;overflow:hidden}.publish-list{margin:.3rem 0 0;padding:0;list-style:none}.publish-list li{align-items:center;gap:8px;margin-top:.3rem;display:flex}.publish-list li.in-use .slot{color:var(--green)}.publish-list .slot{color:var(--muted);flex:none;min-width:4.5em;font-size:12px}.publish-list input{flex:auto;min-width:0}.said{color:var(--accent);margin:.3rem 0 0}.directory-list li.offline .recent-label{opacity:.55}.directory-list li.offline .detail{color:var(--warn)}.share-line{align-items:center;gap:8px;margin:.4rem 0;display:flex}.share-what{color:var(--muted);letter-spacing:.06em;text-transform:uppercase;flex:none;font-size:12px}.share-line input{flex:auto;min-width:0}#share-phone{color:var(--fg);font-size:1.05em}#share-phone b{color:var(--accent)}.admin-table{border-collapse:collapse;width:100%;min-height:5.5em;max-height:14rem;margin:.4rem 0;font-size:.85em;display:block;overflow-y:auto}.admin-table th{text-align:left;opacity:.6;padding:.2rem .4rem .2rem 0;font-weight:400}.admin-table td{white-space:nowrap;text-overflow:ellipsis;max-width:12rem;padding:.2rem .4rem .2rem 0;overflow:hidden}.admin-table td.network-public{color:var(--warning,#e0b341)}.admin-table td.network-private{color:var(--success,#7fd18b)}.admin-table tr.ended{opacity:.45}body.route-directory .player-only{display:none}.directory-list li{align-items:stretch;gap:.4rem;display:flex}.directory-list li>:first-child{flex:auto;min-width:0}.directory-list li>button:first-child{text-align:left;width:100%}.directory-list li>.button,.directory-list li>.ghost{white-space:nowrap;flex:none;align-items:center;display:flex}.directory-list .follow{white-space:nowrap;width:auto;color:var(--muted);flex:none;padding-inline:.6rem}.directory-list .follow[data-following=yes]{border-color:var(--accent);color:var(--accent)}.toggle{color:var(--muted);cursor:pointer;align-items:center;gap:.35rem;display:inline-flex}.toggle input{accent-color:var(--accent)}.recent-label{border:1px solid var(--line);border-radius:4px;flex:auto;min-width:0;padding:.4rem .5rem}.pager{border-top:1px solid var(--edge);justify-content:space-between;align-items:center;gap:8px;margin-top:6px;padding-top:6px;display:flex}.pager .pager-where{color:var(--muted);font-variant-numeric:tabular-nums}.pager button:disabled{opacity:.35;cursor:default}body.tv{font-size:19px}body.tv .playlist,body.tv #catalogs-entries,body.tv .directory-list,body.tv .admin-table{max-height:none;overflow-y:visible}body.tv #app{max-width:1400px}body.tv .row,body.tv .folder{border-radius:4px;padding:5px 10px}body.tv .row:focus-visible,body.tv .folder:focus-visible{outline:2px solid var(--green);outline-offset:-2px;background:#142019}body.tv button,body.tv .button,body.tv .directory-list button{min-height:2.4em;padding:8px 16px}body.tv .row-copy{min-height:0;padding:0 8px}body.tv .icon{min-width:2.4em;height:2.4em}body.tv .directory-list li+li{margin-top:.6rem}body.tv .server-lives li>.button{padding:6px 14px;font-size:.9em}body.tv input,body.tv .filter{padding:8px 10px;font-size:1em}.embed{aspect-ratio:16/9;border:1px solid var(--edge);background:#000;border-radius:4px;width:100%;max-height:46vh;margin-bottom:8px;overflow:hidden}.embed iframe{border:0;width:100%;height:100%;display:block}.way-in{color:var(--fg);padding:0 8px}.way-in svg{width:18px;height:18px}.way-in:disabled{opacity:.4;cursor:not-allowed}.way-in:disabled:hover{color:var(--muted);border-color:var(--edge)}.way-in-here{align-items:center;gap:4px;display:inline-flex}.transcript-list,.trollbox-list,.log-list{overflow-anchor:none}.sr-only{clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.skip-links a{z-index:1000;background:var(--panel);color:var(--fg);border:2px solid var(--accent);padding:12px;position:fixed;top:8px;left:8px;transform:translateY(-200%)}.skip-links a:focus{transform:none}:where(button,a,input,select,textarea,[tabindex]):focus-visible{outline:2px solid var(--accent);outline-offset:3px}.row-main{min-width:0;min-height:24px;color:inherit;text-align:left;font:inherit;cursor:pointer;background:0 0;border:0;border-radius:0;flex:1;align-items:baseline;gap:8px;padding:2px;display:flex}.row-main:focus-visible{outline-offset:-2px}.row-main .name{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.row-copy:focus-visible,.row:focus-within .row-copy{opacity:1}.panel-tools button{min-width:24px;min-height:24px}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition:none!important;animation:none!important}}@media (forced-colors:active){button,input,select,textarea,.button{border:1px solid buttontext}:focus-visible{outline:2px solid highlight!important}a{color:linktext}}.audio-translation{flex-wrap:wrap;align-items:center;gap:8px 16px;margin-top:10px;display:flex}.audio-translation .transcript-switch{margin:0}.audio-language{align-items:center;gap:8px;min-width:0;display:flex}.audio-language select{min-width:0;max-width:230px}.audio-translation>.hint{flex-basis:100%;margin:0}.audio-translation details{flex-basis:100%}.audio-translation summary{cursor:pointer;color:var(--muted);padding:4px 0}.background-level{align-items:center;gap:8px;margin-top:6px;font-size:12px;display:flex}.background-level input{flex:1;min-width:0;max-width:180px}.background-level output{min-width:4ch}
|
|
1
|
+
:root{--bg:#080c09;--panel:#0c120e;--edge:#1d2c22;--green:#4af689;--green-dim:#55a97b;--fg:#cfe8d8;--muted:#819d8c;--warn:#e8c35a;--accent:#7ef0c4;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}input:not([type=range]):not([type=file]),select,textarea{min-height:28px}input[type=checkbox],input[type=radio]{vertical-align:middle;min-width:24px;min-height:24px}[hidden]{display:none!important}html,body{background:var(--bg);min-height:100%;color:var(--fg);margin:0;font:14px/1.45 ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace}body{background-image:repeating-linear-gradient(#4af68906 0 1px,#0000 1px 3px)}#app{max-width:1100px;padding:calc(12px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right)) calc(16px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left));flex-direction:column;gap:10px;margin:0 auto;display:flex}.welcome-title{color:var(--green);margin:4px 0 8px;font-size:22px;font-weight:600;line-height:1.25}.welcome-lead{max-width:70ch;color:var(--fg);margin:0 0 14px}.welcome-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin-bottom:14px;display:grid}.welcome-grid h2{letter-spacing:.06em;text-transform:uppercase;color:var(--green-dim);margin:0 0 4px;font-size:13px}.welcome-grid p{color:var(--muted);margin:0;font-size:13px}.welcome-grid code,.welcome-install{color:var(--accent)}.welcome-actions{gap:10px}.welcome-install{border:1px dashed var(--edge);overflow-wrap:anywhere;border-radius:4px;padding:6px 10px;font-size:12px}.welcome .button.primary{color:var(--green);border-color:var(--green-dim)}@media (max-width:720px){.welcome-grid{grid-template-columns:1fr}.welcome-title{font-size:19px}}.bar{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.brand{color:var(--green);letter-spacing:.14em;align-items:center;gap:6px;font-weight:700;display:inline-flex}.brand-mark{flex:none;width:1.15em;height:1.15em}.status{color:var(--muted)}.status[data-playing=true]{color:var(--green)}.status[data-playing=loading]{color:var(--accent)}.status[data-playing=loading]:before,.row.loading>.name:after{content:"";vertical-align:-.1em;border:2px solid;border-right-color:#0000;border-radius:50%;width:.8em;height:.8em;animation:.8s linear infinite turn;display:inline-block}.status[data-playing=loading]:before{margin-right:6px}.row.loading>.name:after{color:var(--accent);margin-left:8px}@keyframes turn{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.status[data-playing=loading]:before,.row.loading>.name:after{animation-duration:2.4s}}.chip{color:var(--muted);border:1px solid var(--edge);text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:46vw;margin-left:auto;padding:1px 10px;font-size:12px;overflow:hidden}.panel{background:linear-gradient(var(--panel), var(--panel)) 0 9px / 100% calc(100% - 9px) no-repeat;min-width:0;padding:22px 12px 12px;position:relative}.panel:after{content:"";border:1px solid var(--edge);pointer-events:none;border-radius:6px;position:absolute;inset:9px 0 0}.panel:before{content:none}.panel>.panel-heading{white-space:nowrap;text-overflow:ellipsis;background:var(--panel);max-width:calc(100% - 108px);color:var(--green-dim);letter-spacing:.06em;z-index:1;margin:0;padding:0 6px;font-size:12px;font-weight:400;line-height:18px;position:absolute;top:0;left:10px;overflow:hidden}.panel-tools{background:var(--panel);z-index:1;align-items:center;gap:2px;height:18px;padding:0 4px;display:flex;position:absolute;top:0;right:8px}.panel-tools button{font:inherit;border:1px solid var(--edge);height:16px;color:var(--muted);cursor:pointer;background:#121a15;border-radius:3px;padding:0 5px;font-size:11px;line-height:1}.panel-tools button:hover{color:var(--fg);border-color:var(--green-dim)}.panel-tools .grip{cursor:grab}.panel[data-collapsed]{padding:18px 12px 0}.panel[data-collapsed]>:not(.panel-tools):not(.panel-heading),.panel[data-closed]{display:none!important}.panel.dragging{opacity:.45}.panel.drop-before{box-shadow:0 -4px 0 0 var(--accent)}.panel.drop-after{box-shadow:0 4px 0 0 var(--accent)}.link-note{color:var(--warn);margin:6px 0 0}.log-list{max-height:220px;margin:0;padding:0;font-size:12px;list-style:none;overflow-y:auto}.log-list li{border-bottom:1px dotted var(--edge);align-items:baseline;gap:8px;padding:2px 0;display:flex}.log-list time{color:var(--muted);flex:none}.log-list span{overflow-wrap:anywhere;min-width:0}.trollbox-list{max-height:320px;margin:0 0 8px;padding:0;font-size:13px;list-style:none;overflow-y:auto}.trollbox-list li{border-bottom:1px dotted var(--edge);align-items:baseline;gap:6px;padding:2px 0;display:flex}.trollbox-list .when{color:var(--muted);flex:none;font-size:11px}.trollbox-list .who{color:var(--accent);flex:none}.trollbox-list .who:after{content:":"}.trollbox-list .line{overflow-wrap:anywhere;flex:auto;min-width:0}.button.mic{min-width:44px}.button.mic[aria-pressed=true]{color:#ff6b6b;border-color:#ff6b6b;animation:1.2s ease-in-out infinite listening}.button.mic[disabled]{opacity:.6}@keyframes listening{50%{opacity:.55}}.transcript-list{max-height:320px;margin:0;padding:0;font-size:13px;list-style:none;overflow-y:auto}.transcript-list li{border-bottom:1px dotted var(--edge);align-items:baseline;gap:6px;padding:2px 0;display:flex}.transcript-list .when{color:var(--muted);flex:none;font-size:11px}.transcript-list .line{overflow-wrap:anywhere;flex:auto;min-width:0}.transcript-list li.now .line{color:var(--accent)}.transcript-switch{color:var(--muted);align-items:center;gap:6px;margin:0 0 6px;font-size:12px;display:inline-flex}.transcript-switch+.transcript-switch{margin-left:12px}.transcript-language{font:inherit;color:inherit;border:1px solid var(--edge);background:0 0;border-radius:3px;max-width:12em;padding:1px 4px;font-size:12px}.transcript-list .lang{color:var(--muted);flex:none;font-size:11px}.transcript-actions{flex-wrap:wrap;gap:6px;margin:4px 0 10px;display:flex}.transcript-audio{border-top:1px solid var(--edge);padding-top:8px}.transcript-audio select,#transcript-speakers select{max-width:min(18em,100%);font:inherit;color:var(--fg);background:var(--panel);border:1px solid var(--edge);border-radius:3px;padding:3px}.transcript-audio .transcript-switch,#transcript-speakers .transcript-switch{flex-wrap:wrap;max-width:100%;margin-left:0;margin-right:10px}#transcript-speakers{max-height:150px;overflow:auto}.subtitle{z-index:2;pointer-events:none;text-align:center;color:#fff;text-shadow:0 1px 2px #000;background:#000000a8;border-radius:4px;padding:4px 10px;font-size:clamp(14px,2.4vw,22px);line-height:1.3;position:absolute;left:12px;right:12px}body.tv .subtitle{font-size:28px}.panels-list li{align-items:center;gap:8px;padding:3px 0;display:flex}.panels-list li label{cursor:pointer;flex:auto;align-items:center;gap:8px;display:flex}.panels-list li .name{flex:auto}.panels-list li.off .name{color:var(--muted)}.panels-list li .detail{color:var(--muted);white-space:nowrap;font-size:12px}.split{columns:2;column-gap:10px}.split>*{break-inside:avoid;width:100%;margin-bottom:14px;display:block}@supports (display:grid-lanes){.split{display:grid-lanes;columns:auto;grid-template-columns:1.3fr 1fr;gap:14px 10px}.split>*{margin-bottom:0}.split>.col-a{grid-column:1}.split>.col-b{grid-column:2}}.directory-list li.onair{border-bottom:1px solid var(--edge);flex-direction:column;align-items:stretch;gap:6px;padding:6px 0}.directory-list li.onair:last-child{border-bottom:0}.directory-list li.onair>.recent-label{flex:none}.directory-list li.onair .detail{white-space:normal;overflow-wrap:anywhere}.onair-actions{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.icon{border:1px solid var(--edge);min-width:2.2em;height:2.2em;color:var(--muted);cursor:pointer;background:0 0;border-radius:3px;flex:none;padding:0 6px;font-size:1em;line-height:1}.icon:hover{color:var(--accent);border-color:var(--accent)}.directory-list li>.button:disabled,.directory-list li>.way-in:disabled{opacity:.4;cursor:not-allowed}.directory-list li>.way-in:disabled:hover{color:var(--muted);border-color:var(--edge)}.directory-list li>.way-in{color:var(--fg);flex:none;align-self:center;padding:0 8px}.directory-list li>.way-in svg{width:18px;height:18px}.heart{color:var(--muted);cursor:pointer;background:0 0;border:1px solid #0000;border-radius:3px;padding:2px 6px;font-size:1.1em;line-height:1}.heart:hover{color:var(--accent);border-color:var(--accent)}.heart[data-on=yes]{color:var(--accent)}.directory-list li>span.server-label{flex-direction:column;flex:1 1 0;gap:2px;min-width:0;padding:4px 2px;display:flex}.directory-list li>.server-label .name{font-weight:600}.directory-list li>.server-label .detail{color:var(--muted);white-space:nowrap}.directory-list li>.server-label .live{color:var(--green)}@media (max-width:720px){.split{columns:1}@supports (display:grid-lanes){.split{grid-template-columns:1fr}.split>.col-a,.split>.col-b{grid-column:auto}}}.track-title{color:var(--accent);text-overflow:ellipsis;white-space:nowrap;font-weight:700;overflow:hidden}.track-sub{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.track-title.with-context,.track-sub.with-context{white-space:normal;overflow-wrap:anywhere}.track-live{color:var(--muted);overflow-wrap:anywhere;margin-top:6px;line-height:1.5}.track-live b{color:var(--green);font-weight:700}.track-meta{flex-wrap:wrap;align-items:center;gap:6px;margin-top:6px;margin-bottom:4px;display:flex}.meta-chip{color:var(--muted);border:1px solid var(--edge);letter-spacing:.04em;text-overflow:ellipsis;white-space:nowrap;border-radius:999px;max-width:100%;padding:0 8px;font-size:11px;line-height:1.7;overflow:hidden}.meta-chip:first-of-type{color:var(--green);border-color:var(--green-dim)}.meta-logo{object-fit:contain;background:#000;border-radius:3px;max-width:64px;height:22px}.meta-logo.meta-poster{max-width:52px;height:72px}.live-art{object-fit:cover;vertical-align:middle;background:#000;border-radius:3px;flex:0 0 28px;width:28px;height:28px;margin-right:6px}.meta-score{flex-wrap:wrap;flex-basis:100%;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:1.4;display:flex}.meta-team{color:var(--fg);text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:38vw;overflow:hidden}.meta-points{font-variant-numeric:tabular-nums;font-size:16px;font-weight:700}.meta-dash{color:var(--muted);padding:0 2px}.meta-team-logo{object-fit:contain;flex:none;width:20px;height:20px}.meta-chip.chip-live{color:var(--fg);border-color:#b3261e}.meta-chip.chip-live:before{content:"";vertical-align:1px;background:#e53935;border-radius:50%;width:7px;height:7px;margin-right:5px;animation:1.4s ease-in-out infinite pulse;display:inline-block}@keyframes pulse{50%{opacity:.35}}@media (prefers-reduced-motion:reduce){.meta-chip.chip-live:before{animation:none}}.track-blurb{color:var(--muted);-webkit-line-clamp:2;-webkit-box-orient:vertical;margin:4px 0 0;font-size:12px;line-height:1.5;display:-webkit-box;overflow:hidden}#video{border:1px solid var(--edge);background:#000;border-radius:4px;width:100%;max-height:46vh;margin-bottom:8px}.scrub{align-items:center;gap:10px;margin-top:6px;display:flex}.time{color:var(--fg);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}input[type=range]{appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:14px}input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(var(--edge), var(--edge));border:1px solid var(--edge);border-radius:3px;height:6px}input[type=range]::-moz-range-track{background:var(--edge);border-radius:3px;height:6px}input[type=range]::-webkit-slider-thumb{appearance:none;background:var(--green);border-radius:2px;width:10px;height:16px;margin-top:-6px}input[type=range]::-moz-range-thumb{background:var(--green);border:0;border-radius:2px;width:10px;height:16px}input[type=range]:disabled{opacity:.45;cursor:default}#spectrum{border:1px solid var(--edge);background:#060a07;border-radius:4px;width:100%;height:190px;display:block}.meters{align-items:baseline;gap:10px;margin-top:6px;display:flex;overflow:hidden}.glyphs{color:var(--green);letter-spacing:1px;white-space:pre;flex:auto;min-width:0;min-height:1.4em;font-family:ui-monospace,SF Mono,Menlo,Consolas,monospace;overflow:hidden}.levelmeter{color:var(--accent);white-space:pre;font-variant-ligatures:none;text-align:right;flex:none;min-width:15ch;margin-left:auto;font-family:ui-monospace,SF Mono,Menlo,Consolas,monospace}.filter{border:1px solid var(--edge);width:100%;color:var(--fg);font:inherit;background:#0a100c;border-radius:4px;margin-bottom:6px;padding:4px 8px}.filter:focus{border-color:var(--accent);outline:none}.crumbs{color:var(--muted);flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:6px;font-size:12px;display:flex}.crumbs button{font:inherit;color:var(--accent);cursor:pointer;background:0 0;border:0;padding:0 2px}.crumbs button:hover{text-decoration:underline}.crumbs .here{color:var(--fg)}.catalog-tag{color:var(--muted);letter-spacing:.06em;flex:none;font-size:11px}.catalog-live{color:var(--green)}#catalogs-entries{max-height:320px;overflow-y:auto}#catalogs-list{max-height:240px}#catalogs-list li{align-items:center}#catalogs-list .name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}#catalogs-list .detail{opacity:1;color:var(--muted);font-size:11px}#catalogs-list button{min-height:24px;padding:2px 8px;font-size:12px}.folder{cursor:pointer;white-space:nowrap;color:var(--accent);border-radius:3px;gap:8px;padding:2px 6px;display:flex}.folder:hover{background:#142019}.folder .name{text-overflow:ellipsis;flex:1;overflow:hidden}.folder .count{color:var(--muted);flex:none}.playlist{scrollbar-color:var(--green-dim) transparent;max-height:214px;margin:0;padding:0;list-style:none;overflow-y:auto}.row{cursor:pointer;white-space:nowrap;border-radius:3px;gap:8px;padding:2px 6px;display:flex}.row:hover{background:#142019}.row.selected{background:#16241c}.row.selected .name{color:var(--accent)}.row.playing .name{color:var(--green)}.group{color:var(--muted);letter-spacing:.08em;text-transform:uppercase;border-top:1px solid var(--edge);align-items:center;gap:8px;margin-top:4px;padding:6px 6px 2px;font-size:11px;display:flex}.group:first-child{border-top:0;margin-top:0}.group-name{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.group-remove{border:1px solid var(--edge);color:var(--muted);cursor:pointer;background:0 0;border-radius:3px;flex:none;padding:0 6px;line-height:1.4}.group-remove:hover{color:var(--accent);border-color:var(--accent)}.row .n{color:var(--muted);text-align:right;flex:none;width:2.4em}.row .name{text-overflow:ellipsis;flex:1;overflow:hidden}.row .time{color:var(--muted);flex:none}.row-copy{color:var(--muted);cursor:pointer;opacity:.55;background:0 0;border:1px solid #0000;border-radius:3px;flex:none;padding:0 5px;line-height:1.4}.row:hover .row-copy,.row.selected .row-copy{opacity:1}.row-copy svg,.icon svg{vertical-align:-2px;width:14px;height:14px;display:inline-block}.icon{justify-content:center;align-items:center;display:inline-flex}.row-copy:hover{color:var(--accent);border-color:var(--accent)}.transport{border:1px solid var(--edge);background:var(--panel);border-radius:6px;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 10px;display:flex}button{font:inherit;color:var(--fg);border:1px solid var(--edge);cursor:pointer;background:#121a15;border-radius:4px;padding:6px 12px}button:hover{border-color:var(--green-dim);color:var(--green)}button:active{transform:translateY(1px)}button:focus-visible,input:focus-visible,a:focus-visible{outline:2px solid var(--green);outline-offset:2px}button.primary{color:var(--green);border-color:var(--green-dim);min-width:64px}button.ghost{color:var(--muted);background:0 0}button.share{color:var(--accent);border-color:var(--edge);letter-spacing:.04em}button.share:hover{background:#16241c}button.golive{color:#ff5c5c;letter-spacing:.06em;border-color:#7a2b2b}button.golive:hover{color:#fff;background:#7a2b2b;border-color:#ff5c5c}button.golive:disabled{opacity:.5;cursor:progress}.row-live:hover{color:#ff5c5c;border-color:#ff5c5c}.directory-list li:has(>.server-lives){flex-wrap:wrap}.directory-list li>.server-lives{border-left:2px solid var(--green-dim);flex-direction:column;flex:100%;gap:4px;margin:2px 0 0;padding:0 0 0 12px;list-style:none;display:flex}.server-lives li{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.server-lives li>.detail{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.server-lives li>.button{flex:none;padding:2px 10px;font-size:12px}.volume{flex:1;align-items:center;gap:8px;min-width:120px;max-width:220px;margin-left:auto;display:flex}.vol{color:var(--muted);letter-spacing:.08em;font-size:12px}.picker{flex-wrap:wrap;align-items:center;gap:8px;display:flex}#party-form label{color:var(--muted);flex-basis:100%}#party-code{flex:12ch;min-width:0}.link-form{margin-top:8px}.link-form input{flex:240px;min-width:0}.link-form select{min-width:0;font:inherit;color:inherit;background:#0b120e;border:1px solid #2a3a30;border-radius:4px;flex:0 180px;padding:6px 8px}.button{border:1px solid var(--edge);cursor:pointer;color:var(--fg);background:#121a15;border-radius:4px;padding:6px 12px;display:inline-block}.button:hover{border-color:var(--green-dim);color:var(--green)}.button:has(input[type=file]){position:relative}.button input[type=file]{opacity:0;cursor:pointer;width:100%;height:100%;position:absolute;inset:0}.button:focus-within{outline:2px solid var(--green);outline-offset:2px}#remote-url{font:inherit;min-width:12ch;color:var(--fg);border:1px solid var(--edge);background:#060a07;border-radius:4px;flex:1;padding:6px 10px}.check{color:var(--muted);align-items:center;gap:8px;margin-top:10px;display:flex}.check input{accent-color:var(--green)}.hint{color:var(--muted);margin:0 0 8px;font-size:13px}.hint code{color:var(--accent)}#remote-state[data-status=live]{color:var(--green)}#remote-state[data-status=connecting],#remote-state[data-status=error]{color:var(--warn)}.note{color:var(--warn);border-left:2px solid var(--warn);margin:0;padding-left:8px}.statusbar{color:var(--muted);border-top:1px solid var(--edge);flex-wrap:wrap;gap:14px;padding-top:8px;font-size:12px;display:flex}.statusbar b{color:var(--green);font-weight:700}.statusbar .tv-toggle{color:var(--accent);font:inherit;cursor:pointer;background:0 0;border:0;padding:0;text-decoration:underline}body.tv .statusbar .tv-toggle{color:var(--green)}.statusbar .spacer{flex:1}.statusbar a{color:var(--muted)}.directory{border-top:1px solid var(--line);margin-top:.6rem;padding-top:.6rem}.directory-list{max-height:12rem;margin:0;padding:0;list-style:none;overflow-y:auto}.directory-list li+li{margin-top:.3rem}.directory-list button{border:1px solid var(--line);color:inherit;font:inherit;cursor:pointer;background:0 0;border-radius:4px;padding:.4rem .5rem}.directory-list button:hover,.directory-list button:focus-visible{border-color:var(--accent);background:#ffffff0a}.directory-list .name{color:var(--accent);display:block}.directory-list .detail{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:.85em;display:block;overflow:hidden}.publish-list{margin:.3rem 0 0;padding:0;list-style:none}.publish-list li{align-items:center;gap:8px;margin-top:.3rem;display:flex}.publish-list li.in-use .slot{color:var(--green)}.publish-list .slot{color:var(--muted);flex:none;min-width:4.5em;font-size:12px}.publish-list input{flex:auto;min-width:0}.said{color:var(--accent);margin:.3rem 0 0}.directory-list li.offline .recent-label{opacity:.55}.directory-list li.offline .detail{color:var(--warn)}.share-line{align-items:center;gap:8px;margin:.4rem 0;display:flex}.share-what{color:var(--muted);letter-spacing:.06em;text-transform:uppercase;flex:none;font-size:12px}.share-line input{flex:auto;min-width:0}#share-phone{color:var(--fg);font-size:1.05em}#share-phone b{color:var(--accent)}.admin-table{border-collapse:collapse;width:100%;min-height:5.5em;max-height:14rem;margin:.4rem 0;font-size:.85em;display:block;overflow-y:auto}.admin-table th{text-align:left;opacity:.6;padding:.2rem .4rem .2rem 0;font-weight:400}.admin-table td{white-space:nowrap;text-overflow:ellipsis;max-width:12rem;padding:.2rem .4rem .2rem 0;overflow:hidden}.admin-table td.network-public{color:var(--warning,#e0b341)}.admin-table td.network-private{color:var(--success,#7fd18b)}.admin-table tr.ended{opacity:.45}body.route-directory .player-only{display:none}.directory-list li{align-items:stretch;gap:.4rem;display:flex}.directory-list li>:first-child{flex:auto;min-width:0}.directory-list li>button:first-child{text-align:left;width:100%}.directory-list li>.button,.directory-list li>.ghost{white-space:nowrap;flex:none;align-items:center;display:flex}.directory-list .follow{white-space:nowrap;width:auto;color:var(--muted);flex:none;padding-inline:.6rem}.directory-list .follow[data-following=yes]{border-color:var(--accent);color:var(--accent)}.toggle{color:var(--muted);cursor:pointer;align-items:center;gap:.35rem;display:inline-flex}.toggle input{accent-color:var(--accent)}.recent-label{border:1px solid var(--line);border-radius:4px;flex:auto;min-width:0;padding:.4rem .5rem}.pager{border-top:1px solid var(--edge);justify-content:space-between;align-items:center;gap:8px;margin-top:6px;padding-top:6px;display:flex}.pager .pager-where{color:var(--muted);font-variant-numeric:tabular-nums}.pager button:disabled{opacity:.35;cursor:default}body.tv{font-size:19px}body.tv .playlist,body.tv #catalogs-entries,body.tv .directory-list,body.tv .admin-table{max-height:none;overflow-y:visible}body.tv #app{max-width:1400px}body.tv .row,body.tv .folder{border-radius:4px;padding:5px 10px}body.tv .row:focus-visible,body.tv .folder:focus-visible{outline:2px solid var(--green);outline-offset:-2px;background:#142019}body.tv button,body.tv .button,body.tv .directory-list button{min-height:2.4em;padding:8px 16px}body.tv .row-copy{min-height:0;padding:0 8px}body.tv .icon{min-width:2.4em;height:2.4em}body.tv .directory-list li+li{margin-top:.6rem}body.tv .server-lives li>.button{padding:6px 14px;font-size:.9em}body.tv input,body.tv .filter{padding:8px 10px;font-size:1em}.embed{aspect-ratio:16/9;border:1px solid var(--edge);background:#000;border-radius:4px;width:100%;max-height:46vh;margin-bottom:8px;overflow:hidden}.embed iframe{border:0;width:100%;height:100%;display:block}.way-in{color:var(--fg);padding:0 8px}.way-in svg{width:18px;height:18px}.way-in:disabled{opacity:.4;cursor:not-allowed}.way-in:disabled:hover{color:var(--muted);border-color:var(--edge)}.way-in-here{align-items:center;gap:4px;display:inline-flex}.transcript-list,.trollbox-list,.log-list{overflow-anchor:none}.sr-only{clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.skip-links a{z-index:1000;background:var(--panel);color:var(--fg);border:2px solid var(--accent);padding:12px;position:fixed;top:8px;left:8px;transform:translateY(-200%)}.skip-links a:focus{transform:none}:where(button,a,input,select,textarea,[tabindex]):focus-visible{outline:2px solid var(--accent);outline-offset:3px}.row-main{min-width:0;min-height:24px;color:inherit;text-align:left;font:inherit;cursor:pointer;background:0 0;border:0;border-radius:0;flex:1;align-items:baseline;gap:8px;padding:2px;display:flex}.row-main:focus-visible{outline-offset:-2px}.row-main .name{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.row-copy:focus-visible,.row:focus-within .row-copy{opacity:1}.panel-tools button{min-width:24px;min-height:24px}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition:none!important;animation:none!important}}@media (forced-colors:active){button,input,select,textarea,.button{border:1px solid buttontext}:focus-visible{outline:2px solid highlight!important}a{color:linktext}}.audio-translation{flex-wrap:wrap;align-items:center;gap:8px 16px;margin-top:10px;display:flex}.audio-translation .transcript-switch{margin:0}.audio-language{align-items:center;gap:8px;min-width:0;display:flex}.audio-language select{min-width:0;max-width:230px}.audio-translation>.hint{flex-basis:100%;margin:0}.audio-translation details{flex-basis:100%}.audio-translation summary{cursor:pointer;color:var(--muted);padding:4px 0}.background-level{align-items:center;gap:8px;margin-top:6px;font-size:12px;display:flex}.background-level input{flex:1;min-width:0;max-width:180px}.background-level output{min-width:4ch}#translation-purchase{color:var(--fg);background:var(--panel);border:1px solid var(--edge);border-radius:8px;width:min(480px,100vw - 32px);max-height:85vh;padding:20px;overflow:auto}#translation-purchase::backdrop{background:#000a}.purchase-heading{justify-content:space-between;align-items:center;gap:16px;display:flex}.purchase-heading h2{margin:0;font-size:18px}#translation-purchase p{margin:12px 0;line-height:1.5}#translation-purchase label{margin-top:12px;display:block}#translation-purchase select{width:100%;min-height:36px;margin-top:4px}.purchase-actions{flex-wrap:wrap;align-items:center;gap:12px;margin-top:16px;display:flex}#translation-balance:empty{display:none}.purchase-terms{margin-top:16px}#transcript-panel>.panel-heading{max-width:calc(100% - 140px)}
|