@openparachute/hub 0.7.3-rc.6 → 0.7.3-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/api-modules.test.ts +65 -10
- package/src/__tests__/module-manifest.test.ts +3 -1
- package/src/__tests__/scribe-config.test.ts +117 -0
- package/src/__tests__/service-spec-discovery.test.ts +22 -2
- package/src/__tests__/setup-wizard.test.ts +18 -0
- package/src/__tests__/setup.test.ts +129 -15
- package/src/__tests__/wizard-transcription.test.ts +334 -0
- package/src/__tests__/wizard.test.ts +146 -0
- package/src/api-modules.ts +47 -13
- package/src/commands/init.ts +10 -2
- package/src/commands/setup.ts +31 -6
- package/src/commands/wizard-transcription.ts +296 -0
- package/src/commands/wizard.ts +73 -3
- package/src/module-manifest.ts +23 -9
- package/src/scribe-config.ts +145 -0
- package/src/service-spec.ts +31 -12
- package/src/setup-wizard.ts +37 -4
- package/web/ui/dist/assets/{index-DR6R8EFf.css → index--728BX3j.css} +1 -1
- package/web/ui/dist/assets/{index-CKv8qCCQ.js → index-DZzX_Enf.js} +10 -10
- package/web/ui/dist/index.html +2 -2
package/src/scribe-config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { totalmem } from "node:os";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { parseEnvFile, upsertEnvLine, writeEnvFile } from "./env-file.ts";
|
|
4
5
|
|
|
@@ -60,6 +61,118 @@ export type ScribeProviderKey = (typeof SCRIBE_PROVIDERS)[number]["key"];
|
|
|
60
61
|
/** Default provider scribe falls back to when the config doesn't pick one. */
|
|
61
62
|
export const SCRIBE_DEFAULT_PROVIDER: ScribeProviderKey = "parakeet-mlx";
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the "local" choice to the CORRECT platform backend. The setup
|
|
66
|
+
* wizard (browser + CLI) lets the operator pick "local" without knowing the
|
|
67
|
+
* engine name; this picks the one that actually runs here.
|
|
68
|
+
*
|
|
69
|
+
* - macOS → `parakeet-mlx` (Apple Silicon MLX)
|
|
70
|
+
* - Linux → `onnx-asr` (cross-platform Sherpa-ONNX)
|
|
71
|
+
* - other → `null` (no local backend — steer to cloud)
|
|
72
|
+
*
|
|
73
|
+
* Mirrors scribe's own `platformLocalProvider` (parachute-scribe
|
|
74
|
+
* src/install-backend.ts) so hub and scribe can't drift on the mapping.
|
|
75
|
+
* Fixes the long-standing bug where the wizard mapped `local` UNCONDITIONALLY
|
|
76
|
+
* to `parakeet-mlx`, which silently fails on every Linux box (the common
|
|
77
|
+
* DigitalOcean / VPS deploy).
|
|
78
|
+
*/
|
|
79
|
+
export function platformLocalProvider(
|
|
80
|
+
platform: NodeJS.Platform,
|
|
81
|
+
): "parakeet-mlx" | "onnx-asr" | null {
|
|
82
|
+
if (platform === "darwin") return "parakeet-mlx";
|
|
83
|
+
if (platform === "linux") return "onnx-asr";
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Minimum available RAM (MiB) below which a local ASR model would be
|
|
89
|
+
* OOM-killed. Mirrors scribe's `MIN_RAM_MIB` (parachute-scribe
|
|
90
|
+
* src/install-backend.ts) — the 1 GB DigitalOcean droplet is the box this
|
|
91
|
+
* guards against; a local Parakeet/ONNX model needs ~2 GB to load.
|
|
92
|
+
*/
|
|
93
|
+
export const MIN_RAM_MIB = 2048;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Available RAM in MiB, or `null` when it can't be determined.
|
|
97
|
+
*
|
|
98
|
+
* Linux: reads `MemAvailable` from `/proc/meminfo` (the honest figure — free
|
|
99
|
+
* plus reclaimable cache), matching scribe's probe so the two layers agree on
|
|
100
|
+
* the same droplet. Falls back to `MemFree` on very old kernels that predate
|
|
101
|
+
* MemAvailable.
|
|
102
|
+
*
|
|
103
|
+
* Non-Linux: falls back to `os.totalmem()` (there's no MemAvailable analogue;
|
|
104
|
+
* total is a coarse upper bound but enough to keep a tiny VM from offering
|
|
105
|
+
* local). macOS dev boxes comfortably clear the floor, so the coarseness is
|
|
106
|
+
* harmless there.
|
|
107
|
+
*
|
|
108
|
+
* Sync by design: the wizard's provider-decision path is synchronous, and the
|
|
109
|
+
* `/proc/meminfo` read is a tiny file. Tests inject `availableRamMib` directly
|
|
110
|
+
* to exercise the gate without touching the real host.
|
|
111
|
+
*/
|
|
112
|
+
export function readAvailableRamMib(platform: NodeJS.Platform = process.platform): number | null {
|
|
113
|
+
if (platform === "linux") {
|
|
114
|
+
try {
|
|
115
|
+
const text = readFileSync("/proc/meminfo", "utf8");
|
|
116
|
+
const avail = /^MemAvailable:\s+(\d+)\s*kB/m.exec(text);
|
|
117
|
+
const free = /^MemFree:\s+(\d+)\s*kB/m.exec(text);
|
|
118
|
+
const kb = avail ? Number(avail[1]) : free ? Number(free[1]) : null;
|
|
119
|
+
if (kb === null || !Number.isFinite(kb)) return null;
|
|
120
|
+
return Math.floor(kb / 1024);
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Non-Linux: total physical memory as a coarse fallback.
|
|
126
|
+
try {
|
|
127
|
+
const bytes = totalmem();
|
|
128
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return null;
|
|
129
|
+
return Math.floor(bytes / (1024 * 1024));
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Decide whether a LOCAL transcription provider is offerable / acceptable on
|
|
137
|
+
* this host, and if not, why + what to steer to. Both wizard surfaces (browser
|
|
138
|
+
* POST handler + CLI) consult this BEFORE recording a `local` choice so we
|
|
139
|
+
* never write a dead provider string that scribe can't run.
|
|
140
|
+
*
|
|
141
|
+
* `ok: false` carries a `reason` (shown inline) and a `steerTo` cloud provider
|
|
142
|
+
* the caller should redirect to.
|
|
143
|
+
*/
|
|
144
|
+
export interface LocalProviderDecision {
|
|
145
|
+
ok: boolean;
|
|
146
|
+
/** The resolved platform backend when `ok` (parakeet-mlx / onnx-asr). */
|
|
147
|
+
provider?: "parakeet-mlx" | "onnx-asr";
|
|
148
|
+
/** Human-readable reason when `ok` is false (no local backend / too little RAM). */
|
|
149
|
+
reason?: string;
|
|
150
|
+
/** Cloud provider to redirect to when local is unavailable. */
|
|
151
|
+
steerTo?: "groq";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function decideLocalProvider(
|
|
155
|
+
platform: NodeJS.Platform,
|
|
156
|
+
availableRamMib: number | null,
|
|
157
|
+
): LocalProviderDecision {
|
|
158
|
+
const provider = platformLocalProvider(platform);
|
|
159
|
+
if (provider === null) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
reason: `No local transcription backend runs on "${platform}". Use a cloud provider instead.`,
|
|
163
|
+
steerTo: "groq",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (availableRamMib !== null && availableRamMib < MIN_RAM_MIB) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
reason: `This box has ${availableRamMib} MiB available RAM, below the ${MIN_RAM_MIB} MiB a local ASR model needs (it would be OOM-killed). Use a cloud provider instead — groq is fast (~$0.04/hr of audio).`,
|
|
170
|
+
steerTo: "groq",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, provider };
|
|
174
|
+
}
|
|
175
|
+
|
|
63
176
|
export function isKnownScribeProvider(value: string): value is ScribeProviderKey {
|
|
64
177
|
return SCRIBE_PROVIDERS.some((p) => p.key === value);
|
|
65
178
|
}
|
|
@@ -136,6 +249,38 @@ export function writeScribeProvider(configDir: string, provider: ScribeProviderK
|
|
|
136
249
|
renameSync(tmp, path);
|
|
137
250
|
}
|
|
138
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Remove `transcribe.provider` from scribe's config.json (preserving every
|
|
254
|
+
* other key). Used by the wizard's local-install path to UNDO a provisional
|
|
255
|
+
* provider record when the engine install fails — so we never leave a dead
|
|
256
|
+
* provider string scribe can't honor. A no-op when the file or the
|
|
257
|
+
* `transcribe` block is absent.
|
|
258
|
+
*/
|
|
259
|
+
export function clearScribeProvider(configDir: string): void {
|
|
260
|
+
const path = scribeConfigPath(configDir);
|
|
261
|
+
if (!existsSync(path)) return;
|
|
262
|
+
let current: Record<string, unknown>;
|
|
263
|
+
try {
|
|
264
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
265
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
266
|
+
current = parsed as Record<string, unknown>;
|
|
267
|
+
} catch {
|
|
268
|
+
return; // malformed → leave it; auto-wire repairs on next run
|
|
269
|
+
}
|
|
270
|
+
const transcribe = current.transcribe;
|
|
271
|
+
if (typeof transcribe !== "object" || transcribe === null || Array.isArray(transcribe)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Drop `provider` from the transcribe block (destructure-omit, no `delete`).
|
|
275
|
+
const { provider: _dropped, ...block } = transcribe as Record<string, unknown>;
|
|
276
|
+
const { transcribe: _omit, ...rest } = current;
|
|
277
|
+
const next: Record<string, unknown> =
|
|
278
|
+
Object.keys(block).length === 0 ? rest : { ...rest, transcribe: block };
|
|
279
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
280
|
+
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`);
|
|
281
|
+
renameSync(tmp, path);
|
|
282
|
+
}
|
|
283
|
+
|
|
139
284
|
/**
|
|
140
285
|
* Idempotent upsert of a single `KEY=value` into `<configDir>/scribe/.env`.
|
|
141
286
|
* Used for the API-key prompt result. Other lines (auto-wire keys, manual
|
package/src/service-spec.ts
CHANGED
|
@@ -637,12 +637,22 @@ export function knownServices(): string[] {
|
|
|
637
637
|
}
|
|
638
638
|
|
|
639
639
|
/**
|
|
640
|
-
* Default discovery tier per short name (2026-06-09 modular-UI architecture
|
|
641
|
-
* The hub PREFERS a module's
|
|
642
|
-
* fallback when `module.json` omits
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* `
|
|
640
|
+
* Default discovery tier per short name (2026-06-09 modular-UI architecture;
|
|
641
|
+
* `deprecated` tier added 2026-06-25). The hub PREFERS a module's
|
|
642
|
+
* manifest-declared `focus`; this map is the fallback when `module.json` omits
|
|
643
|
+
* it (and the bootstrap value before a module is installed).
|
|
644
|
+
*
|
|
645
|
+
* - `core` — vault / scribe / hub / surface (the product surface).
|
|
646
|
+
* - `experimental` — agent (legit preview; still OFFERED on a fresh install)
|
|
647
|
+
* + any unlisted third-party short.
|
|
648
|
+
* - `deprecated` — notes (notes-daemon deprecated 2026-05-22; notes-ui moved
|
|
649
|
+
* into parachute-surface) + runner (per Aaron 2026-06-25: not for new
|
|
650
|
+
* installs). Still RESOLVABLE (discoverableShorts unchanged) and
|
|
651
|
+
* SHOWN-IF-INSTALLED so an existing operator can manage/uninstall, but NOT
|
|
652
|
+
* OFFERED on a fresh setup.
|
|
653
|
+
*
|
|
654
|
+
* **Show all installed; never hide** — `focus` groups + labels; the one
|
|
655
|
+
* behavioral lever is the fresh-install OFFER, which drops `deprecated` shorts.
|
|
646
656
|
*/
|
|
647
657
|
const FOCUS_DEFAULTS: Record<string, ModuleFocus> = {
|
|
648
658
|
vault: "core",
|
|
@@ -650,8 +660,8 @@ const FOCUS_DEFAULTS: Record<string, ModuleFocus> = {
|
|
|
650
660
|
hub: "core",
|
|
651
661
|
surface: "core",
|
|
652
662
|
agent: "experimental",
|
|
653
|
-
runner: "
|
|
654
|
-
notes: "
|
|
663
|
+
runner: "deprecated",
|
|
664
|
+
notes: "deprecated",
|
|
655
665
|
};
|
|
656
666
|
|
|
657
667
|
/**
|
|
@@ -659,6 +669,13 @@ const FOCUS_DEFAULTS: Record<string, ModuleFocus> = {
|
|
|
659
669
|
* `module.json` `focus`) is present it wins; otherwise fall back to
|
|
660
670
|
* `FOCUS_DEFAULTS`, defaulting any unlisted short to `experimental`. Never
|
|
661
671
|
* returns undefined — the Modules screen always has a tier to group by.
|
|
672
|
+
*
|
|
673
|
+
* Tier semantics: `core`/`experimental` are both OFFERED on a fresh install;
|
|
674
|
+
* `deprecated` (notes / runner) is NOT offered on a fresh setup but stays
|
|
675
|
+
* resolvable + shown-if-installed (the `isKnownModuleShort` /
|
|
676
|
+
* `discoverableShorts` resolution surface is unchanged). The fresh-install
|
|
677
|
+
* filters in `setup.ts` + `api-modules.ts` consult this tier to drop
|
|
678
|
+
* `deprecated` shorts from the "available to install" set.
|
|
662
679
|
*/
|
|
663
680
|
export function focusForShort(short: string, declared?: ModuleFocus): ModuleFocus {
|
|
664
681
|
if (declared !== undefined) return declared;
|
|
@@ -674,10 +691,12 @@ export function focusForShort(short: string, declared?: ModuleFocus): ModuleFocu
|
|
|
674
691
|
* regardless of `focus` tier. Deduped, with FIRST_PARTY_FALLBACKS shorts first
|
|
675
692
|
* (notes) then KNOWN_MODULES (vault / scribe / runner / agent / surface).
|
|
676
693
|
*
|
|
677
|
-
* `notes`
|
|
678
|
-
* for legacy installs;
|
|
679
|
-
*
|
|
680
|
-
*
|
|
694
|
+
* `notes` (and `runner`) are intentionally included — still resolvable
|
|
695
|
+
* (vendored fallback / KNOWN_MODULES) for legacy installs; they surface as
|
|
696
|
+
* `deprecated` (2026-06-25) and aren't OFFERED on a fresh install. The
|
|
697
|
+
* fresh-install OFFER (setup wizard + admin SPA) filters by tier
|
|
698
|
+
* (`focus !== "deprecated"`); `discoverableShorts` itself stays the full
|
|
699
|
+
* resolution surface so existing installs keep working.
|
|
681
700
|
*/
|
|
682
701
|
export function discoverableShorts(): string[] {
|
|
683
702
|
const seen = new Set<string>();
|
package/src/setup-wizard.ts
CHANGED
|
@@ -74,6 +74,11 @@ import {
|
|
|
74
74
|
readOperatorTokenFile,
|
|
75
75
|
} from "./operator-token.ts";
|
|
76
76
|
import { isHttpsRequest } from "./request-protocol.ts";
|
|
77
|
+
import {
|
|
78
|
+
decideLocalProvider,
|
|
79
|
+
platformLocalProvider,
|
|
80
|
+
readAvailableRamMib,
|
|
81
|
+
} from "./scribe-config.ts";
|
|
77
82
|
import { SEED_VERSION } from "./service-spec.ts";
|
|
78
83
|
import { findService, readManifestLenient } from "./services-manifest.ts";
|
|
79
84
|
import {
|
|
@@ -2410,8 +2415,23 @@ export async function handleSetupVaultPost(req: Request, deps: SetupWizardDeps):
|
|
|
2410
2415
|
// Cleanup-without-transcribe is a valid combo: the operator can
|
|
2411
2416
|
// hit scribe's REST cleanup endpoint directly with their own raw
|
|
2412
2417
|
// text. We install scribe + write the cleanup block in that case.
|
|
2413
|
-
|
|
2418
|
+
let scribeProvider = String(form.get("scribe_provider") ?? "").trim();
|
|
2414
2419
|
const scribeCleanupProvider = String(form.get("scribe_cleanup_provider") ?? "").trim();
|
|
2420
|
+
// RAM/platform gate: if the operator asked for `local` on a box that can't
|
|
2421
|
+
// run a local ASR model (no local backend for the platform, or too little
|
|
2422
|
+
// RAM — the 1 GB droplet would OOM), redirect the choice to a cloud provider
|
|
2423
|
+
// (groq) rather than recording a dead `local` string scribe can never honor.
|
|
2424
|
+
// The reason is logged; the inline UI surfaces it via the scribe op poll.
|
|
2425
|
+
if (scribeProvider === "local") {
|
|
2426
|
+
const decision = decideLocalProvider(process.platform, readAvailableRamMib());
|
|
2427
|
+
if (!decision.ok) {
|
|
2428
|
+
console.warn(
|
|
2429
|
+
`[setup-wizard] local transcription unavailable on this host: ${decision.reason} ` +
|
|
2430
|
+
`Steering to "${decision.steerTo}".`,
|
|
2431
|
+
);
|
|
2432
|
+
scribeProvider = decision.steerTo ?? "groq";
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2415
2435
|
const wantsTranscribe = scribeProvider !== "" && scribeProvider !== "none";
|
|
2416
2436
|
const wantsCleanup = scribeCleanupProvider !== "" && scribeCleanupProvider !== "none";
|
|
2417
2437
|
let scribeOpId: string | undefined;
|
|
@@ -2590,16 +2610,29 @@ interface WizardScribeConfig {
|
|
|
2590
2610
|
transcribe?: { provider: string; apiKey: string };
|
|
2591
2611
|
/** Set when the operator chose a cleanup provider (anything other than "none"). */
|
|
2592
2612
|
cleanup?: { provider: string; apiKey: string };
|
|
2613
|
+
/**
|
|
2614
|
+
* Platform override for resolving the `local` choice (test seam). Defaults to
|
|
2615
|
+
* the real host platform. Mac → parakeet-mlx, Linux → onnx-asr.
|
|
2616
|
+
*/
|
|
2617
|
+
platform?: NodeJS.Platform;
|
|
2593
2618
|
}
|
|
2594
2619
|
function writeScribeConfigForWizard(configDir: string, config: WizardScribeConfig): void {
|
|
2595
2620
|
const update: Record<string, unknown> = {};
|
|
2621
|
+
const platform = config.platform ?? process.platform;
|
|
2596
2622
|
|
|
2597
2623
|
if (config.transcribe) {
|
|
2598
2624
|
const { provider, apiKey } = config.transcribe;
|
|
2599
|
-
// For `local
|
|
2600
|
-
//
|
|
2625
|
+
// For `local`, resolve to the CORRECT platform backend — parakeet-mlx on
|
|
2626
|
+
// macOS, onnx-asr on Linux. (Was hardcoded to parakeet-mlx, which silently
|
|
2627
|
+
// fails on every Linux box.) No key needed for local. The caller's
|
|
2628
|
+
// RAM/platform gate is the single place that decides "local isn't possible
|
|
2629
|
+
// here" and should have steered to cloud before reaching this writer — but
|
|
2630
|
+
// if that gate is ever bypassed and the platform has no local backend, we
|
|
2631
|
+
// write "none" (transcription off) rather than a dead provider string, so
|
|
2632
|
+
// this writer can never record something that silently fails.
|
|
2601
2633
|
if (provider === "local") {
|
|
2602
|
-
|
|
2634
|
+
const resolved = platformLocalProvider(platform);
|
|
2635
|
+
update.transcribe = { provider: resolved ?? "none" };
|
|
2603
2636
|
} else {
|
|
2604
2637
|
// Cloud providers need a key. Empty key → just set provider;
|
|
2605
2638
|
// the operator can paste the key later via /scribe/admin
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--bg: #faf8f4;--bg-soft: #f3f0ea;--fg: #2c2a26;--fg-muted: #6b6860;--fg-dim: #9a9690;--accent: #4a7c59;--accent-soft: rgba(74, 124, 89, .08);--accent-hover: #3d6849;--border: #e4e0d8;--border-light: #ece9e2;--card-bg: #ffffff;--error: #a3392b;--error-soft: rgba(163, 57, 43, .08);--warn: #b08023;--warn-soft: rgba(176, 128, 35, .08);--success: #3d6849;--success-soft: rgba(61, 104, 73, .08);--font-serif: Georgia, "Times New Roman", serif;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", monospace;font-family:var(--font-sans)}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}button{font:inherit;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:.55rem 1.1rem;cursor:pointer;transition:background .15s ease}button:hover{background:var(--accent-hover)}button:disabled{opacity:.5;cursor:not-allowed}button.secondary{background:#fff;color:var(--fg);border:1px solid var(--border)}button.secondary:hover{background:var(--bg-soft)}input,select,textarea{font:inherit;background:#fff;border:1px solid var(--border);border-radius:6px;padding:.55rem .75rem;color:var(--fg)}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent)}code{font-family:var(--font-mono);font-size:.85em;background:var(--bg-soft);padding:.1em .3em;border-radius:3px}.page{max-width:880px;margin:0 auto;padding:1.5rem 1.5rem 6rem}.nav{display:flex;flex-wrap:wrap;gap:.6rem 1rem;align-items:center;padding-bottom:1rem;border-bottom:1px solid var(--border);margin-bottom:2rem}.nav .brand{font-weight:600;font-family:var(--font-serif);font-size:1.15rem;margin-right:auto;display:inline-flex;align-items:center;gap:.45rem;color:var(--accent);text-decoration:none}.nav .brand:hover{color:var(--accent-hover);text-decoration:none}.nav .brand-mark-icon{flex-shrink:0;line-height:0}.nav .brand-wordmark{color:var(--fg);letter-spacing:-.005em}.nav .brand .sub{color:var(--fg-dim);font-size:.78rem;font-weight:400;margin-left:.4rem;font-family:var(--font-sans)}.nav a{color:var(--fg-muted);font-size:.95rem}.nav a:hover{text-decoration:none;color:var(--fg)}.nav a.nav-link-active{color:var(--accent);font-weight:500;text-decoration:underline;text-underline-offset:.3em;text-decoration-thickness:2px}.nav .nav-divider{display:inline-block;width:1px;height:1.1em;background:var(--border);align-self:center}.nav .nav-dropdown{position:relative}.nav .nav-dropdown-summary{list-style:none;cursor:pointer;color:var(--fg-muted);font-size:.95rem;-webkit-user-select:none;user-select:none}.nav .nav-dropdown-summary::-webkit-details-marker{display:none}.nav .nav-dropdown-summary:hover{color:var(--fg)}.nav .nav-dropdown[open]>.nav-dropdown-summary{color:var(--fg)}.nav .nav-dropdown-summary:after{content:" ▾";font-size:.7em;color:var(--fg-dim)}.nav .nav-dropdown-panel{position:absolute;top:calc(100% + .4rem);left:0;z-index:10;min-width:12rem;background:var(--card-bg);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 12px #00000014;padding:.4rem 0;display:flex;flex-direction:column}.nav .nav-dropdown-item{padding:.4rem .85rem;color:var(--fg);font-size:.9rem;text-decoration:none}.nav .nav-dropdown-item:hover{background:var(--bg-soft);color:var(--fg);text-decoration:none}.nav .nav-dropdown-item-disabled{color:var(--fg-dim);cursor:not-allowed}.nav .nav-dropdown-item-disabled:hover{background:transparent;color:var(--fg-dim)}.nav .auth-spa{font-size:.85rem;color:var(--fg-muted)}.nav .auth-spa strong{font-weight:600;color:var(--fg)}.nav .auth-spa-signout{background:none;border:none;padding:0;color:var(--accent);font:inherit;cursor:pointer;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px}.nav .auth-spa-signout:hover:not(:disabled){color:var(--accent-hover)}.nav .auth-spa-signout:disabled{color:var(--fg-dim);cursor:not-allowed}h1{margin:0 0 .5rem;font-family:var(--font-serif);font-size:1.85rem;font-weight:400;letter-spacing:-.01em;line-height:1.2;color:var(--fg)}h2{margin:0 0 1rem;font-size:1.4rem;font-weight:500}.muted{color:var(--fg-muted);font-size:.92rem}.dim{color:var(--fg-dim);font-size:.85rem}.error-banner{background:var(--error-soft);border:1px solid var(--error);color:var(--error);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.warn-banner{background:var(--warn-soft);border:1px solid var(--warn);color:var(--warn);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.empty{padding:3rem 1.5rem;text-align:center;color:var(--fg-muted);background:var(--bg-soft);border-radius:10px}@keyframes pc-loading-pulse{0%,to{opacity:.55}50%{opacity:1}}[data-loading=true]{animation:pc-loading-pulse 1.4s ease-in-out infinite}.user-table tbody tr,.tokens-table tbody tr,.channel-table tbody tr{transition:background-color .12s ease}.user-table tbody tr:hover,.tokens-table tbody tr:hover,.channel-table tbody tr:hover{background:var(--bg-soft)}@keyframes pc-route-fade-up{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}[data-route-content]{animation:pc-route-fade-up .32s ease forwards}@media(prefers-reduced-motion:reduce){[data-loading=true],[data-route-content]{animation:none}}.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;background:linear-gradient(to right,var(--card-bg),var(--card-bg)) left center / 20px 100% no-repeat,linear-gradient(to right,#2c2a2614,#2c2a2600) left center / 8px 100% no-repeat,linear-gradient(to left,var(--card-bg),var(--card-bg)) right center / 20px 100% no-repeat,linear-gradient(to left,#2c2a2614,#2c2a2600) right center / 8px 100% no-repeat;background-attachment:local,scroll,local,scroll}.table-scroll>table{min-width:100%}.empty-rich{text-align:left;padding:2rem 1.75rem;background:#fff;border:1px solid var(--border)}.empty-rich .empty-headline{font-size:1.05rem;color:var(--fg);margin:0 0 .5rem;font-weight:500}.list-header{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:1rem}.list-header h1,.list-header h2{margin:0}.tag{display:inline-block;padding:.1em .55em;background:var(--accent-soft);color:var(--accent);border-radius:4px;font-size:.78rem;font-weight:500}.tag.muted{background:var(--bg-soft);color:var(--fg-muted)}.tag.source-oauth{background:#4a7cc61f;color:#3b6aa6}.tag.source-operator{background:#c6984a24;color:#8a5e1f}.tag.source-cli{background:#4a7c5924;color:#2f5a3f}.tag.source-unknown{background:var(--bg-soft);color:var(--fg-muted)}@media(prefers-color-scheme:dark){.tag.source-oauth{background:#7a9cdc24;color:#9bb6d8}.tag.source-operator{background:#dcb46e24;color:#d4b27a}.tag.source-cli{background:#7ab08a24;color:#8fc49e}.tag.source-unknown{background:#e8e4dc0f;color:#a8a49a}}.vault-row{display:flex;align-items:center;gap:1rem;padding:.85rem 1rem;background:#fff;border:1px solid var(--border);border-radius:8px;margin-bottom:.5rem;text-decoration:none;color:inherit;transition:border-color .15s ease}.vault-row:hover{border-color:var(--accent);text-decoration:none}.vault-row .body{flex:1;min-width:0}.vault-row .name{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.vault-row .name code{font-size:.95em}.vault-row .url{margin-top:.25rem;word-break:break-all}.vault-row .chev{color:var(--fg-dim);font-size:1.2rem}.vault-row-group{margin-bottom:.5rem}.vault-row-group .vault-row{margin-bottom:0}.vault-row-actions{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.mcp-connect-card{background:var(--bg-soft);border:1px solid var(--border);border-radius:8px;padding:1.1rem 1.25rem;margin:0 0 .5rem}.mcp-connect-card-embedded{background:#fff;margin-bottom:0}.mcp-connect-card h3{margin:0 0 .4rem;font-size:1rem}.mcp-connect-card>p{margin-top:0}.mcp-connect-card .token-box{display:flex;align-items:center;gap:.5rem;margin:.35rem 0 .25rem}.mcp-connect-card .token-box code{flex:1;font-size:.85rem;padding:.55rem .7rem;background:#fff;border:1px solid var(--border);border-radius:6px;word-break:break-all;-webkit-user-select:all;user-select:all}.mcp-field{margin-top:.9rem}.mcp-field-label{display:block;font-size:.82rem;font-weight:600;color:var(--fg-muted)}.mcp-field .dim{margin:.3rem 0 0}.mcp-token-path{margin-top:1rem;border-top:1px solid var(--border);padding-top:.75rem}.mcp-token-path>summary{cursor:pointer;font-size:.9rem;color:var(--fg-muted)}.mcp-token-path>summary:hover{color:var(--accent)}.mcp-token-path .mint-banner{margin-top:.75rem;margin-bottom:0}.mcp-docs-link{margin:.9rem 0 0}form .row{margin-bottom:1rem}form label{display:block;font-size:.9rem;color:var(--fg-muted);margin-bottom:.3rem;font-weight:500}form input[type=text]{width:100%}form .actions{display:flex;gap:.6rem;align-items:center;margin-top:1rem}form .field-hint{margin-top:.35rem;font-size:.82rem;color:var(--fg-dim)}form .field-error{margin-top:.35rem;font-size:.85rem;color:var(--error)}.section{background:#fff;border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:1.5rem}.mint-banner{background:var(--success-soft);border:1px solid var(--success);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:1.5rem}.mint-banner h3{margin:0 0 .5rem;font-size:1rem;color:var(--success)}.mint-banner .token-box{display:flex;align-items:center;gap:.5rem;margin:.85rem 0 .5rem}.mint-banner code{flex:1;font-size:.9rem;padding:.6rem .75rem;background:#fff;border:1px solid var(--border);word-break:break-all;-webkit-user-select:all;user-select:all}.mint-banner .warn{margin:.75rem 0 0;font-size:.85rem;color:var(--warn)}.mint-banner .actions{margin-top:1rem;display:flex;gap:.5rem}.kv{display:grid;grid-template-columns:8.5rem 1fr;gap:.5rem 1rem;font-size:.92rem}.kv>div:nth-child(odd){color:var(--fg-muted)}.kv code{word-break:break-all}.channel-toggle{margin:1.25rem 0 1.5rem;padding:.75rem 1rem;border:1px solid var(--border, #ddd);border-radius:6px;background:var(--bg-soft, #fafafa)}.channel-toggle legend{padding:0 .25rem;font-weight:600;font-size:.95rem}.channel-toggle label{display:inline-flex;align-items:center;gap:.4rem;margin-right:1.5rem;cursor:pointer;font-size:.95rem}.channel-toggle label input[type=radio]:disabled+*{opacity:.5}.channel-toggle code{font-size:.85em}.channel-toggle p.muted{margin:.4rem 0 0;font-size:.85rem}.modules-installed,.modules-installable{margin-top:1.75rem}.modules-installed>h2,.modules-installable>h2{font-size:1.15rem;font-weight:600;margin:0 0 .75rem;color:var(--fg)}.modules-installed>p.muted,.modules-installable>p.muted{margin:0 0 .5rem}.modules-experimental{margin-top:1.25rem;padding-top:.75rem;border-top:1px dashed var(--border, #d0d0d0);opacity:.78;transition:opacity .15s ease}.modules-experimental:hover,.modules-experimental:focus-within{opacity:1}.experimental-heading{font-size:.95rem;font-weight:600;margin:0 0 .6rem;text-transform:uppercase;letter-spacing:.04em}.experimental-heading>span.muted{text-transform:none;letter-spacing:normal;font-weight:400}.install-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.6rem}.install-card{display:flex;flex-direction:row;align-items:center;gap:1rem;flex-wrap:wrap;padding:.85rem 1rem;background:#fff;border:1px solid var(--border);border-radius:8px;transition:border-color .15s ease}.install-card:hover{border-color:var(--accent)}.install-card-body{flex:1 1 0;min-width:0}.install-card-body h3{margin:0 0 .2rem;font-size:1rem;font-weight:600;color:var(--fg)}.install-card-body .tagline{margin:0 0 .35rem;color:var(--fg-muted);font-size:.92rem}.install-card-meta{margin:0;font-size:.82rem}.install-card-actions{flex:0 0 auto}.install-card .error{flex-basis:100%;margin-top:.5rem;color:var(--error);font-size:.85rem}.hub-upgrade-card{border-left:3px solid var(--accent);margin-top:1.25rem}.hub-upgrade-card .warn-banner,.hub-upgrade-card .error-banner{flex-basis:100%;margin:.5rem 0 0;font-size:.85rem}.module-row .actions .btn,a.btn{display:inline-block;font:inherit;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:.55rem 1.1rem;cursor:pointer;transition:background .15s ease;text-decoration:none}.module-row .actions .btn:hover,a.btn:hover{background:var(--accent-hover);text-decoration:none}.module-uis{margin:.5rem 0 0;padding:.5rem 0 0;border-top:1px solid var(--border-light)}.module-uis>summary{cursor:pointer;font-size:.88rem;color:var(--fg-muted);font-weight:500;padding:.15rem 0;list-style:revert}.module-uis>summary:hover{color:var(--fg)}.ui-sub-units{list-style:none;padding:0;margin:.5rem 0 0 1.1rem;display:flex;flex-direction:column;gap:.35rem}.ui-sub-unit{display:flex;flex-direction:row;align-items:center;gap:.65rem;padding:.5rem .75rem;background:var(--bg-soft);border:1px solid var(--border-light);border-radius:6px;transition:border-color .15s ease,background .15s ease}.ui-sub-unit:hover{border-color:var(--accent);background:#fff}.ui-icon{flex:0 0 auto;width:20px;height:20px;border-radius:4px;object-fit:contain}.ui-sub-unit-body{flex:1 1 0;min-width:0}.ui-sub-unit-link{color:var(--fg);font-size:.95rem;text-decoration:none}.ui-sub-unit-link:hover{color:var(--accent);text-decoration:underline}.ui-sub-unit-link strong{font-weight:600}.ui-sub-unit .tagline{margin:.2rem 0 0;font-size:.82rem;color:var(--fg-muted)}.status{flex:0 0 auto;display:inline-block;padding:.1em .55em;background:var(--bg-soft);color:var(--fg-muted);border-radius:4px;font-size:.78rem;font-weight:500;white-space:nowrap}.status-active{background:var(--success-soft);color:var(--success)}.status-pending{background:var(--warn-soft);color:var(--warn)}.status-inactive{background:var(--bg-soft);color:var(--fg-dim)}.status-failing{background:var(--error-soft);color:var(--error)}.status-absent{background:var(--bg-soft);color:var(--fg-dim)}.status-redeemed{background:var(--success-soft);color:var(--success)}.status-expired,.status-revoked{background:var(--bg-soft);color:var(--fg-dim)}.status-approved{background:var(--success-soft);color:var(--success)}.error-inline{color:var(--error);font-size:.85rem}.status-pending-oauth{background:var(--warn-soft);color:var(--warn)}.status-disabled{background:var(--bg-soft);color:var(--fg-dim)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.hub-version-badge{margin-top:3rem;padding-top:1rem;border-top:1px solid var(--border-light);display:flex;flex-direction:column;align-items:flex-start;gap:.75rem;color:var(--fg-muted);font-size:.8rem}.hub-version-badge-summary{background:transparent;border:0;padding:0;margin:0;color:var(--fg-muted);font:inherit;cursor:pointer;text-align:left;border-radius:4px}.hub-version-badge-summary:hover{color:var(--fg);background:transparent}.hub-version-badge-summary strong{color:var(--fg);font-weight:600}.hub-version-badge-source{font-variant:small-caps;letter-spacing:.04em}.hub-version-badge-panel{background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:.85rem 1rem;font-size:.85rem;color:var(--fg);width:100%;max-width:28rem}.hub-version-badge-panel dl{margin:0 0 .75rem;display:grid;grid-template-columns:max-content 1fr;gap:.3rem .85rem}.hub-version-badge-panel dt{color:var(--fg-muted);font-size:.78rem;text-transform:uppercase;letter-spacing:.06em;padding-top:.1rem}.hub-version-badge-panel dd{margin:0;color:var(--fg);word-break:break-all}.hub-version-badge-refresh{font-size:.8rem;padding:.35rem .85rem}.depcard-wrap{margin-top:.6rem}.depcard{border:1px solid var(--warn);background:var(--warn-soft);border-radius:8px;padding:.9rem 1rem}.depcard-heading{margin:0 0 .25rem;font-size:1rem}.depcard-why{margin:0 0 .75rem;font-size:.9rem}.depcard-installs-label{margin:0 0 .4rem;font-size:.85rem;font-weight:600}.depcard-install{margin-bottom:.55rem}.depcard-install.preferred .depcard-os{color:var(--accent);font-weight:600}.depcard-os{display:block;font-size:.78rem;text-transform:uppercase;letter-spacing:.05em;color:var(--fg-muted);margin-bottom:.2rem}.depcard-cmd{display:flex;align-items:stretch;gap:.4rem}.depcard-cmd-text{flex:1;margin:0;padding:.45rem .6rem;background:var(--card-bg, #fff);border:1px solid var(--border);border-radius:6px;font-size:.82rem;white-space:pre-wrap;overflow-x:auto}.depcard-copy{flex:0 0 auto;font-size:.8rem;padding:.35rem .7rem;align-self:flex-start}.depcard-docs{margin:.5rem 0 .4rem;font-size:.88rem}.depcard-hint{margin:0;font-size:.82rem}.depcard-fallback{color:var(--error);font-size:.9rem}.admin-home>.muted{margin-bottom:1.75rem}.home-group{margin-bottom:2.25rem}.home-group-head{display:flex;align-items:baseline;gap:.6rem;margin-bottom:.25rem}.home-group-head h2{margin:0;font-size:1.25rem}.home-group-sub{margin:0 0 1rem}.home-group-tag{display:inline-block;padding:.1em .55em;border-radius:4px;font-size:.72rem;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.home-group-tag-hub{background:var(--accent-soft);color:var(--accent)}.home-group-tag-module{background:var(--bg-soft);color:var(--fg-muted)}.home-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.85rem}.home-card{display:flex;flex-direction:column;gap:.3rem;padding:.95rem 1.1rem;background:var(--card-bg);border:1px solid var(--border);border-radius:10px;text-decoration:none;color:inherit;transition:border-color .15s ease,box-shadow .15s ease,transform .12s ease}.home-card:hover{text-decoration:none;border-color:var(--accent);box-shadow:0 3px 10px #0000000d;transform:translateY(-1px)}.home-card-module,.home-card-surface{background:var(--bg-soft)}.home-card-disabled{cursor:default;opacity:.65}.home-card-disabled:hover{border-color:var(--border);box-shadow:none;transform:none}.home-card-title{font-weight:600;font-size:1rem;color:var(--fg)}.home-card-desc{font-size:.86rem;color:var(--fg-muted);line-height:1.4}.home-card-owner{margin-top:.15rem;font-size:.76rem;color:var(--accent);font-weight:500}.home-card-owner-empty{color:var(--fg-dim);font-weight:400}.ext-mark{font-size:.85em;color:var(--accent);font-weight:600}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.lock-screen{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:var(--bg);padding:1.5rem}.lock-card{width:100%;max-width:22rem;display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding:2rem 1.75rem;background:var(--bg-soft);border:1px solid var(--border);border-radius:14px}.lock-brand-mark{margin-bottom:.4rem}.lock-title{font-size:1.2rem;margin:0}.lock-sub{margin:0 0 .6rem;font-size:.9rem}.lock-form{display:flex;flex-direction:column;gap:.6rem;width:100%}.lock-pin-input{width:100%;text-align:center;letter-spacing:.45em;font-size:1.3rem;padding:.7rem .5rem;border:1px solid var(--border);border-radius:9px;background:var(--bg);color:var(--fg)}.lock-pin-input:focus{outline:none;border-color:var(--accent)}.lock-error{margin-top:.6rem;color:var(--error);font-size:.88rem}.lock-settings-form{display:flex;flex-direction:column;gap:.7rem;max-width:28rem}.lock-settings-form label{display:flex;flex-direction:column;gap:.3rem;font-size:.9rem}.lock-status-pill{display:inline-block;padding:.1rem .5rem;border-radius:999px;font-size:.78rem;font-weight:500}.lock-status-on{background:var(--accent-soft);color:var(--accent)}.lock-status-off{background:var(--bg-soft);color:var(--fg-muted)}
|
|
1
|
+
:root{--bg: #faf8f4;--bg-soft: #f3f0ea;--fg: #2c2a26;--fg-muted: #6b6860;--fg-dim: #9a9690;--accent: #4a7c59;--accent-soft: rgba(74, 124, 89, .08);--accent-hover: #3d6849;--border: #e4e0d8;--border-light: #ece9e2;--card-bg: #ffffff;--error: #a3392b;--error-soft: rgba(163, 57, 43, .08);--warn: #b08023;--warn-soft: rgba(176, 128, 35, .08);--success: #3d6849;--success-soft: rgba(61, 104, 73, .08);--font-serif: Georgia, "Times New Roman", serif;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", monospace;font-family:var(--font-sans)}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}button{font:inherit;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:.55rem 1.1rem;cursor:pointer;transition:background .15s ease}button:hover{background:var(--accent-hover)}button:disabled{opacity:.5;cursor:not-allowed}button.secondary{background:#fff;color:var(--fg);border:1px solid var(--border)}button.secondary:hover{background:var(--bg-soft)}input,select,textarea{font:inherit;background:#fff;border:1px solid var(--border);border-radius:6px;padding:.55rem .75rem;color:var(--fg)}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent)}code{font-family:var(--font-mono);font-size:.85em;background:var(--bg-soft);padding:.1em .3em;border-radius:3px}.page{max-width:880px;margin:0 auto;padding:1.5rem 1.5rem 6rem}.nav{display:flex;flex-wrap:wrap;gap:.6rem 1rem;align-items:center;padding-bottom:1rem;border-bottom:1px solid var(--border);margin-bottom:2rem}.nav .brand{font-weight:600;font-family:var(--font-serif);font-size:1.15rem;margin-right:auto;display:inline-flex;align-items:center;gap:.45rem;color:var(--accent);text-decoration:none}.nav .brand:hover{color:var(--accent-hover);text-decoration:none}.nav .brand-mark-icon{flex-shrink:0;line-height:0}.nav .brand-wordmark{color:var(--fg);letter-spacing:-.005em}.nav .brand .sub{color:var(--fg-dim);font-size:.78rem;font-weight:400;margin-left:.4rem;font-family:var(--font-sans)}.nav a{color:var(--fg-muted);font-size:.95rem}.nav a:hover{text-decoration:none;color:var(--fg)}.nav a.nav-link-active{color:var(--accent);font-weight:500;text-decoration:underline;text-underline-offset:.3em;text-decoration-thickness:2px}.nav .nav-divider{display:inline-block;width:1px;height:1.1em;background:var(--border);align-self:center}.nav .nav-dropdown{position:relative}.nav .nav-dropdown-summary{list-style:none;cursor:pointer;color:var(--fg-muted);font-size:.95rem;-webkit-user-select:none;user-select:none}.nav .nav-dropdown-summary::-webkit-details-marker{display:none}.nav .nav-dropdown-summary:hover{color:var(--fg)}.nav .nav-dropdown[open]>.nav-dropdown-summary{color:var(--fg)}.nav .nav-dropdown-summary:after{content:" ▾";font-size:.7em;color:var(--fg-dim)}.nav .nav-dropdown-panel{position:absolute;top:calc(100% + .4rem);left:0;z-index:10;min-width:12rem;background:var(--card-bg);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 12px #00000014;padding:.4rem 0;display:flex;flex-direction:column}.nav .nav-dropdown-item{padding:.4rem .85rem;color:var(--fg);font-size:.9rem;text-decoration:none}.nav .nav-dropdown-item:hover{background:var(--bg-soft);color:var(--fg);text-decoration:none}.nav .nav-dropdown-item-disabled{color:var(--fg-dim);cursor:not-allowed}.nav .nav-dropdown-item-disabled:hover{background:transparent;color:var(--fg-dim)}.nav .auth-spa{font-size:.85rem;color:var(--fg-muted)}.nav .auth-spa strong{font-weight:600;color:var(--fg)}.nav .auth-spa-signout{background:none;border:none;padding:0;color:var(--accent);font:inherit;cursor:pointer;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px}.nav .auth-spa-signout:hover:not(:disabled){color:var(--accent-hover)}.nav .auth-spa-signout:disabled{color:var(--fg-dim);cursor:not-allowed}h1{margin:0 0 .5rem;font-family:var(--font-serif);font-size:1.85rem;font-weight:400;letter-spacing:-.01em;line-height:1.2;color:var(--fg)}h2{margin:0 0 1rem;font-size:1.4rem;font-weight:500}.muted{color:var(--fg-muted);font-size:.92rem}.dim{color:var(--fg-dim);font-size:.85rem}.error-banner{background:var(--error-soft);border:1px solid var(--error);color:var(--error);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.warn-banner{background:var(--warn-soft);border:1px solid var(--warn);color:var(--warn);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.empty{padding:3rem 1.5rem;text-align:center;color:var(--fg-muted);background:var(--bg-soft);border-radius:10px}@keyframes pc-loading-pulse{0%,to{opacity:.55}50%{opacity:1}}[data-loading=true]{animation:pc-loading-pulse 1.4s ease-in-out infinite}.user-table tbody tr,.tokens-table tbody tr,.channel-table tbody tr{transition:background-color .12s ease}.user-table tbody tr:hover,.tokens-table tbody tr:hover,.channel-table tbody tr:hover{background:var(--bg-soft)}@keyframes pc-route-fade-up{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}[data-route-content]{animation:pc-route-fade-up .32s ease forwards}@media(prefers-reduced-motion:reduce){[data-loading=true],[data-route-content]{animation:none}}.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;background:linear-gradient(to right,var(--card-bg),var(--card-bg)) left center / 20px 100% no-repeat,linear-gradient(to right,#2c2a2614,#2c2a2600) left center / 8px 100% no-repeat,linear-gradient(to left,var(--card-bg),var(--card-bg)) right center / 20px 100% no-repeat,linear-gradient(to left,#2c2a2614,#2c2a2600) right center / 8px 100% no-repeat;background-attachment:local,scroll,local,scroll}.table-scroll>table{min-width:100%}.empty-rich{text-align:left;padding:2rem 1.75rem;background:#fff;border:1px solid var(--border)}.empty-rich .empty-headline{font-size:1.05rem;color:var(--fg);margin:0 0 .5rem;font-weight:500}.list-header{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:1rem}.list-header h1,.list-header h2{margin:0}.tag{display:inline-block;padding:.1em .55em;background:var(--accent-soft);color:var(--accent);border-radius:4px;font-size:.78rem;font-weight:500}.tag.muted{background:var(--bg-soft);color:var(--fg-muted)}.tag.source-oauth{background:#4a7cc61f;color:#3b6aa6}.tag.source-operator{background:#c6984a24;color:#8a5e1f}.tag.source-cli{background:#4a7c5924;color:#2f5a3f}.tag.source-unknown{background:var(--bg-soft);color:var(--fg-muted)}@media(prefers-color-scheme:dark){.tag.source-oauth{background:#7a9cdc24;color:#9bb6d8}.tag.source-operator{background:#dcb46e24;color:#d4b27a}.tag.source-cli{background:#7ab08a24;color:#8fc49e}.tag.source-unknown{background:#e8e4dc0f;color:#a8a49a}}.vault-row{display:flex;align-items:center;gap:1rem;padding:.85rem 1rem;background:#fff;border:1px solid var(--border);border-radius:8px;margin-bottom:.5rem;text-decoration:none;color:inherit;transition:border-color .15s ease}.vault-row:hover{border-color:var(--accent);text-decoration:none}.vault-row .body{flex:1;min-width:0}.vault-row .name{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.vault-row .name code{font-size:.95em}.vault-row .url{margin-top:.25rem;word-break:break-all}.vault-row .chev{color:var(--fg-dim);font-size:1.2rem}.vault-row-group{margin-bottom:.5rem}.vault-row-group .vault-row{margin-bottom:0}.vault-row-actions{display:flex;gap:.5rem;align-items:center;flex-shrink:0}.mcp-connect-card{background:var(--bg-soft);border:1px solid var(--border);border-radius:8px;padding:1.1rem 1.25rem;margin:0 0 .5rem}.mcp-connect-card-embedded{background:#fff;margin-bottom:0}.mcp-connect-card h3{margin:0 0 .4rem;font-size:1rem}.mcp-connect-card>p{margin-top:0}.mcp-connect-card .token-box{display:flex;align-items:center;gap:.5rem;margin:.35rem 0 .25rem}.mcp-connect-card .token-box code{flex:1;font-size:.85rem;padding:.55rem .7rem;background:#fff;border:1px solid var(--border);border-radius:6px;word-break:break-all;-webkit-user-select:all;user-select:all}.mcp-field{margin-top:.9rem}.mcp-field-label{display:block;font-size:.82rem;font-weight:600;color:var(--fg-muted)}.mcp-field .dim{margin:.3rem 0 0}.mcp-token-path{margin-top:1rem;border-top:1px solid var(--border);padding-top:.75rem}.mcp-token-path>summary{cursor:pointer;font-size:.9rem;color:var(--fg-muted)}.mcp-token-path>summary:hover{color:var(--accent)}.mcp-token-path .mint-banner{margin-top:.75rem;margin-bottom:0}.mcp-docs-link{margin:.9rem 0 0}form .row{margin-bottom:1rem}form label{display:block;font-size:.9rem;color:var(--fg-muted);margin-bottom:.3rem;font-weight:500}form input[type=text]{width:100%}form .actions{display:flex;gap:.6rem;align-items:center;margin-top:1rem}form .field-hint{margin-top:.35rem;font-size:.82rem;color:var(--fg-dim)}form .field-error{margin-top:.35rem;font-size:.85rem;color:var(--error)}.section{background:#fff;border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:1.5rem}.mint-banner{background:var(--success-soft);border:1px solid var(--success);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:1.5rem}.mint-banner h3{margin:0 0 .5rem;font-size:1rem;color:var(--success)}.mint-banner .token-box{display:flex;align-items:center;gap:.5rem;margin:.85rem 0 .5rem}.mint-banner code{flex:1;font-size:.9rem;padding:.6rem .75rem;background:#fff;border:1px solid var(--border);word-break:break-all;-webkit-user-select:all;user-select:all}.mint-banner .warn{margin:.75rem 0 0;font-size:.85rem;color:var(--warn)}.mint-banner .actions{margin-top:1rem;display:flex;gap:.5rem}.kv{display:grid;grid-template-columns:8.5rem 1fr;gap:.5rem 1rem;font-size:.92rem}.kv>div:nth-child(odd){color:var(--fg-muted)}.kv code{word-break:break-all}.channel-toggle{margin:1.25rem 0 1.5rem;padding:.75rem 1rem;border:1px solid var(--border, #ddd);border-radius:6px;background:var(--bg-soft, #fafafa)}.channel-toggle legend{padding:0 .25rem;font-weight:600;font-size:.95rem}.channel-toggle label{display:inline-flex;align-items:center;gap:.4rem;margin-right:1.5rem;cursor:pointer;font-size:.95rem}.channel-toggle label input[type=radio]:disabled+*{opacity:.5}.channel-toggle code{font-size:.85em}.channel-toggle p.muted{margin:.4rem 0 0;font-size:.85rem}.modules-installed,.modules-installable{margin-top:1.75rem}.modules-installed>h2,.modules-installable>h2{font-size:1.15rem;font-weight:600;margin:0 0 .75rem;color:var(--fg)}.modules-installed>p.muted,.modules-installable>p.muted{margin:0 0 .5rem}.modules-experimental{margin-top:1.25rem;padding-top:.75rem;border-top:1px dashed var(--border, #d0d0d0);opacity:.78;transition:opacity .15s ease}.modules-experimental:hover,.modules-experimental:focus-within{opacity:1}.experimental-heading{font-size:.95rem;font-weight:600;margin:0 0 .6rem;text-transform:uppercase;letter-spacing:.04em}.experimental-heading>span.muted{text-transform:none;letter-spacing:normal;font-weight:400}.modules-deprecated{margin-top:1.25rem;padding-top:.75rem;border-top:1px dashed var(--border, #d0d0d0);opacity:.65;transition:opacity .15s ease}.modules-deprecated:hover,.modules-deprecated:focus-within{opacity:1}.deprecated-heading{font-size:.95rem;font-weight:600;margin:0 0 .6rem;text-transform:uppercase;letter-spacing:.04em}.deprecated-heading>span.muted{text-transform:none;letter-spacing:normal;font-weight:400}.install-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.6rem}.install-card{display:flex;flex-direction:row;align-items:center;gap:1rem;flex-wrap:wrap;padding:.85rem 1rem;background:#fff;border:1px solid var(--border);border-radius:8px;transition:border-color .15s ease}.install-card:hover{border-color:var(--accent)}.install-card-body{flex:1 1 0;min-width:0}.install-card-body h3{margin:0 0 .2rem;font-size:1rem;font-weight:600;color:var(--fg)}.install-card-body .tagline{margin:0 0 .35rem;color:var(--fg-muted);font-size:.92rem}.install-card-meta{margin:0;font-size:.82rem}.install-card-actions{flex:0 0 auto}.install-card .error{flex-basis:100%;margin-top:.5rem;color:var(--error);font-size:.85rem}.hub-upgrade-card{border-left:3px solid var(--accent);margin-top:1.25rem}.hub-upgrade-card .warn-banner,.hub-upgrade-card .error-banner{flex-basis:100%;margin:.5rem 0 0;font-size:.85rem}.module-row .actions .btn,a.btn{display:inline-block;font:inherit;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:.55rem 1.1rem;cursor:pointer;transition:background .15s ease;text-decoration:none}.module-row .actions .btn:hover,a.btn:hover{background:var(--accent-hover);text-decoration:none}.module-uis{margin:.5rem 0 0;padding:.5rem 0 0;border-top:1px solid var(--border-light)}.module-uis>summary{cursor:pointer;font-size:.88rem;color:var(--fg-muted);font-weight:500;padding:.15rem 0;list-style:revert}.module-uis>summary:hover{color:var(--fg)}.ui-sub-units{list-style:none;padding:0;margin:.5rem 0 0 1.1rem;display:flex;flex-direction:column;gap:.35rem}.ui-sub-unit{display:flex;flex-direction:row;align-items:center;gap:.65rem;padding:.5rem .75rem;background:var(--bg-soft);border:1px solid var(--border-light);border-radius:6px;transition:border-color .15s ease,background .15s ease}.ui-sub-unit:hover{border-color:var(--accent);background:#fff}.ui-icon{flex:0 0 auto;width:20px;height:20px;border-radius:4px;object-fit:contain}.ui-sub-unit-body{flex:1 1 0;min-width:0}.ui-sub-unit-link{color:var(--fg);font-size:.95rem;text-decoration:none}.ui-sub-unit-link:hover{color:var(--accent);text-decoration:underline}.ui-sub-unit-link strong{font-weight:600}.ui-sub-unit .tagline{margin:.2rem 0 0;font-size:.82rem;color:var(--fg-muted)}.status{flex:0 0 auto;display:inline-block;padding:.1em .55em;background:var(--bg-soft);color:var(--fg-muted);border-radius:4px;font-size:.78rem;font-weight:500;white-space:nowrap}.status-active{background:var(--success-soft);color:var(--success)}.status-pending{background:var(--warn-soft);color:var(--warn)}.status-inactive{background:var(--bg-soft);color:var(--fg-dim)}.status-failing{background:var(--error-soft);color:var(--error)}.status-absent{background:var(--bg-soft);color:var(--fg-dim)}.status-redeemed{background:var(--success-soft);color:var(--success)}.status-expired,.status-revoked{background:var(--bg-soft);color:var(--fg-dim)}.status-approved{background:var(--success-soft);color:var(--success)}.error-inline{color:var(--error);font-size:.85rem}.status-pending-oauth{background:var(--warn-soft);color:var(--warn)}.status-disabled{background:var(--bg-soft);color:var(--fg-dim)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.hub-version-badge{margin-top:3rem;padding-top:1rem;border-top:1px solid var(--border-light);display:flex;flex-direction:column;align-items:flex-start;gap:.75rem;color:var(--fg-muted);font-size:.8rem}.hub-version-badge-summary{background:transparent;border:0;padding:0;margin:0;color:var(--fg-muted);font:inherit;cursor:pointer;text-align:left;border-radius:4px}.hub-version-badge-summary:hover{color:var(--fg);background:transparent}.hub-version-badge-summary strong{color:var(--fg);font-weight:600}.hub-version-badge-source{font-variant:small-caps;letter-spacing:.04em}.hub-version-badge-panel{background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:.85rem 1rem;font-size:.85rem;color:var(--fg);width:100%;max-width:28rem}.hub-version-badge-panel dl{margin:0 0 .75rem;display:grid;grid-template-columns:max-content 1fr;gap:.3rem .85rem}.hub-version-badge-panel dt{color:var(--fg-muted);font-size:.78rem;text-transform:uppercase;letter-spacing:.06em;padding-top:.1rem}.hub-version-badge-panel dd{margin:0;color:var(--fg);word-break:break-all}.hub-version-badge-refresh{font-size:.8rem;padding:.35rem .85rem}.depcard-wrap{margin-top:.6rem}.depcard{border:1px solid var(--warn);background:var(--warn-soft);border-radius:8px;padding:.9rem 1rem}.depcard-heading{margin:0 0 .25rem;font-size:1rem}.depcard-why{margin:0 0 .75rem;font-size:.9rem}.depcard-installs-label{margin:0 0 .4rem;font-size:.85rem;font-weight:600}.depcard-install{margin-bottom:.55rem}.depcard-install.preferred .depcard-os{color:var(--accent);font-weight:600}.depcard-os{display:block;font-size:.78rem;text-transform:uppercase;letter-spacing:.05em;color:var(--fg-muted);margin-bottom:.2rem}.depcard-cmd{display:flex;align-items:stretch;gap:.4rem}.depcard-cmd-text{flex:1;margin:0;padding:.45rem .6rem;background:var(--card-bg, #fff);border:1px solid var(--border);border-radius:6px;font-size:.82rem;white-space:pre-wrap;overflow-x:auto}.depcard-copy{flex:0 0 auto;font-size:.8rem;padding:.35rem .7rem;align-self:flex-start}.depcard-docs{margin:.5rem 0 .4rem;font-size:.88rem}.depcard-hint{margin:0;font-size:.82rem}.depcard-fallback{color:var(--error);font-size:.9rem}.admin-home>.muted{margin-bottom:1.75rem}.home-group{margin-bottom:2.25rem}.home-group-head{display:flex;align-items:baseline;gap:.6rem;margin-bottom:.25rem}.home-group-head h2{margin:0;font-size:1.25rem}.home-group-sub{margin:0 0 1rem}.home-group-tag{display:inline-block;padding:.1em .55em;border-radius:4px;font-size:.72rem;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.home-group-tag-hub{background:var(--accent-soft);color:var(--accent)}.home-group-tag-module{background:var(--bg-soft);color:var(--fg-muted)}.home-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.85rem}.home-card{display:flex;flex-direction:column;gap:.3rem;padding:.95rem 1.1rem;background:var(--card-bg);border:1px solid var(--border);border-radius:10px;text-decoration:none;color:inherit;transition:border-color .15s ease,box-shadow .15s ease,transform .12s ease}.home-card:hover{text-decoration:none;border-color:var(--accent);box-shadow:0 3px 10px #0000000d;transform:translateY(-1px)}.home-card-module,.home-card-surface{background:var(--bg-soft)}.home-card-disabled{cursor:default;opacity:.65}.home-card-disabled:hover{border-color:var(--border);box-shadow:none;transform:none}.home-card-title{font-weight:600;font-size:1rem;color:var(--fg)}.home-card-desc{font-size:.86rem;color:var(--fg-muted);line-height:1.4}.home-card-owner{margin-top:.15rem;font-size:.76rem;color:var(--accent);font-weight:500}.home-card-owner-empty{color:var(--fg-dim);font-weight:400}.ext-mark{font-size:.85em;color:var(--accent);font-weight:600}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.lock-screen{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:var(--bg);padding:1.5rem}.lock-card{width:100%;max-width:22rem;display:flex;flex-direction:column;align-items:center;text-align:center;gap:.35rem;padding:2rem 1.75rem;background:var(--bg-soft);border:1px solid var(--border);border-radius:14px}.lock-brand-mark{margin-bottom:.4rem}.lock-title{font-size:1.2rem;margin:0}.lock-sub{margin:0 0 .6rem;font-size:.9rem}.lock-form{display:flex;flex-direction:column;gap:.6rem;width:100%}.lock-pin-input{width:100%;text-align:center;letter-spacing:.45em;font-size:1.3rem;padding:.7rem .5rem;border:1px solid var(--border);border-radius:9px;background:var(--bg);color:var(--fg)}.lock-pin-input:focus{outline:none;border-color:var(--accent)}.lock-error{margin-top:.6rem;color:var(--error);font-size:.88rem}.lock-settings-form{display:flex;flex-direction:column;gap:.7rem;max-width:28rem}.lock-settings-form label{display:flex;flex-direction:column;gap:.3rem;font-size:.9rem}.lock-status-pill{display:inline-block;padding:.1rem .5rem;border-radius:999px;font-size:.78rem;font-weight:500}.lock-status-on{background:var(--accent-soft);color:var(--accent)}.lock-status-off{background:var(--bg-soft);color:var(--fg-muted)}
|