@openparachute/hub 0.7.3-rc.5 → 0.7.3-rc.7

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.
@@ -15,11 +15,21 @@
15
15
  * / `crashed` / `starting` / `restarting`) + pid. Absent when the
16
16
  * hub is in CLI mode (no supervisor injected through HubFetchDeps).
17
17
  *
18
- * `focus` ("core" | "experimental") comes from each module's `module.json`
19
- * when declared, else `focusForShort`'s default map. The SPA groups core first
20
- * + de-emphasizes experimental it NEVER hides a module. This is what makes a
21
- * running, self-registered module (channel) visible + installable; the old
22
- * `CURATED_MODULES = ["vault","scribe"]` whitelist made it invisible.
18
+ * `focus` ("core" | "experimental" | "deprecated") comes from each module's
19
+ * `module.json` when declared, else `focusForShort`'s default map. The SPA
20
+ * groups core first, de-emphasizes experimental, and de-emphasizes
21
+ * `deprecated` (notes-daemon / runner) further it NEVER hides an installed
22
+ * module (so an existing operator can still manage / uninstall a deprecated
23
+ * one). This is what makes a running, self-registered module (channel) visible
24
+ * + installable; the old `CURATED_MODULES = ["vault","scribe"]` whitelist made
25
+ * it invisible.
26
+ *
27
+ * `available_to_install` is the fresh-install OFFER (2026-06-25): a module the
28
+ * hub will push as a new install. It's `available && focus !== "deprecated"`
29
+ * — so a `deprecated` module that is ALREADY installed still surfaces (via the
30
+ * `modules` union + `installed: true`) and is manageable, but a deprecated
31
+ * module is never offered as a fresh install. `agent` (`experimental`) stays
32
+ * offered.
23
33
  *
24
34
  * Bearer-gated on `parachute:host:auth` to match the rest of `/api/auth/*`
25
35
  * and `/api/grants` — the admin SPA mints this scope via
@@ -199,14 +209,33 @@ interface ModuleWireShape {
199
209
  display_name: string;
200
210
  tagline: string;
201
211
  /**
202
- * Discovery tier (2026-06-09 modular-UI architecture). `core` modules render
203
- * in the headline group; `experimental` modules render in a de-emphasized
204
- * "Experimental" group below never hidden. Resolved from the module's
205
- * `module.json` `focus` when declared, else `focusForShort`'s default map
206
- * (vault/scribe/hub/surface core, channel/runner/others experimental).
212
+ * Discovery tier (2026-06-09 modular-UI architecture; `deprecated` added
213
+ * 2026-06-25). `core` modules render in the headline group; `experimental`
214
+ * modules render in a de-emphasized "Experimental" group; `deprecated`
215
+ * modules (notes-daemon / runner) render in a further-de-emphasized
216
+ * "Deprecated" group never hidden when installed. Resolved from the
217
+ * module's `module.json` `focus` when declared, else `focusForShort`'s
218
+ * default map (vault/scribe/hub/surface → core, notes/runner → deprecated,
219
+ * others → experimental).
207
220
  */
208
221
  focus: ModuleFocus;
222
+ /**
223
+ * True iff the hub knows how to install this module (`package`/`manifest`
224
+ * resolvable). Historically "in the curated install catalog". Still set for
225
+ * every known module; a purely third-party services.json row is false.
226
+ * NOTE: this is NOT the fresh-install OFFER — a `deprecated` module is still
227
+ * `available: true` (it's installable for back-compat / re-install) but is
228
+ * excluded from `available_to_install`.
229
+ */
209
230
  available: boolean;
231
+ /**
232
+ * The fresh-install OFFER (2026-06-25): whether the hub presents this module
233
+ * in the "available to install fresh" set. `available && focus !==
234
+ * "deprecated"`. The SPA's "Install a module" catalog filters on this so
235
+ * notes-daemon / runner aren't pushed on a fresh box; an already-installed
236
+ * deprecated module still surfaces (in the Installed section) for management.
237
+ */
238
+ available_to_install: boolean;
210
239
  installed: boolean;
211
240
  installed_version: string | null;
212
241
  latest_version: string | null;
@@ -562,8 +591,8 @@ export async function handleApiModules(req: Request, deps: ApiModulesDeps): Prom
562
591
  // module's manifest-declared `focus` (when installed + declared) wins; else
563
592
  // the `focusForShort` default map. Sort: `core` group first (with the
564
593
  // CURATED_MODULES recommended-install order floated to the top of that
565
- // group), then `experimental` the SPA renders the two groups; `focus`
566
- // never hides a module.
594
+ // group), then `experimental`, then `deprecated` (notes / runner) last the
595
+ // SPA renders the groups; `focus` never hides an installed module.
567
596
  const recommendedOrder = new Map<string, number>(
568
597
  (CURATED_MODULES as readonly string[]).map((s, i) => [s, i]),
569
598
  );
@@ -585,6 +614,11 @@ export async function handleApiModules(req: Request, deps: ApiModulesDeps): Prom
585
614
  // known modules; a purely third-party services.json row (no install
586
615
  // package) is not hub-installable → false.
587
616
  available: m !== undefined,
617
+ // Fresh-install OFFER (2026-06-25): installable AND not deprecated. A
618
+ // deprecated short (notes / runner) stays `available` (re-installable
619
+ // for back-compat) but is dropped from the offer set so the SPA's
620
+ // "Install a module" catalog doesn't push it on a fresh box.
621
+ available_to_install: m !== undefined && focus !== "deprecated",
588
622
  installed: installed !== undefined,
589
623
  installed_version: installed?.version ?? null,
590
624
  latest_version: latestByShort.get(short) ?? null,
@@ -598,7 +632,7 @@ export async function handleApiModules(req: Request, deps: ApiModulesDeps): Prom
598
632
  };
599
633
  return row;
600
634
  });
601
- const focusRank = (f: ModuleFocus): number => (f === "core" ? 0 : 1);
635
+ const focusRank = (f: ModuleFocus): number => (f === "core" ? 0 : f === "experimental" ? 1 : 2);
602
636
  rows.sort((a, b) => {
603
637
  if (a.focus !== b.focus) return focusRank(a.focus) - focusRank(b.focus);
604
638
  const ai = recommendedOrder.get(a.short) ?? Number.POSITIVE_INFINITY;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * `/api/vault-caps*` — admin visibility + edit for per-vault storage caps
3
+ * (DEMO-PREP-2026-06-25 Workstream B5 / D-slice).
4
+ *
5
+ * PR #686 shipped the `vault_caps` table + `setVaultCap`/`getVaultCapBytes`
6
+ * (provision-time persistence) and a separate Phase-2 PR reads + ENFORCES the
7
+ * cap at upload time. This file is the OPERATOR-FACING seam in between: the
8
+ * admin SPA needs to SEE who has what cap and EDIT a cap live in the demo.
9
+ *
10
+ * Surfaces:
11
+ *
12
+ * GET /api/vault-caps list every vault (from services.json) joined
13
+ * with its persisted cap (host:admin)
14
+ * PUT /api/vault-caps/:name set/update a vault's cap to N bytes (host:admin)
15
+ *
16
+ * The list is the JOIN of "what vaults exist" (services.json, the canonical
17
+ * vault-name source) and "what caps are persisted" (`vault_caps`). A vault
18
+ * with no cap row appears with `cap_bytes: null` (uncapped) — the same
19
+ * "uncapped = no row" contract the Phase-2 enforcement reader relies on.
20
+ *
21
+ * Wire shape is snake_case (matches `/api/users`, `/api/invites`). Auth: same
22
+ * `parachute:host:admin` Bearer gate as every other `/api/*` admin surface;
23
+ * the SPA mints it from the session cookie via `/admin/host-admin-token`.
24
+ *
25
+ * Scope discipline: PUT only sets a cap on a vault that is REGISTERED in
26
+ * services.json (rejects a stale / typo name with 400 `vault_not_found`) so an
27
+ * operator can't seed a cap row for a vault that doesn't exist. Additive only —
28
+ * this never changes the provision-time cap-persistence from #686; it's a
29
+ * read + targeted-edit layer on the same table.
30
+ */
31
+ import type { Database } from "bun:sqlite";
32
+ import { type AdminAuthError, adminAuthErrorResponse, requireScope } from "./admin-auth.ts";
33
+ import { HOST_ADMIN_SCOPE } from "./admin-vaults.ts";
34
+ import { SERVICES_MANIFEST_PATH } from "./config.ts";
35
+ import { getVaultCap, setVaultCap } from "./vault-caps.ts";
36
+ import { listVaultNamesFromPath } from "./vault-names.ts";
37
+
38
+ export interface ApiVaultCapsDeps {
39
+ db: Database;
40
+ /** Hub origin — JWT `iss` validation. */
41
+ issuer: string;
42
+ /** Override services.json path. Defaults to `~/.parachute/services.json`. */
43
+ manifestPath?: string;
44
+ }
45
+
46
+ /**
47
+ * One row in the `GET /api/vault-caps` response: a vault name + its cap. A
48
+ * vault with no persisted cap carries `cap_bytes: null` (uncapped); `created_at`
49
+ * / `updated_at` are null too in that case.
50
+ */
51
+ export interface VaultCapWireShape {
52
+ vault_name: string;
53
+ cap_bytes: number | null;
54
+ created_at: string | null;
55
+ updated_at: string | null;
56
+ }
57
+
58
+ function jsonError(status: number, error: string, description: string): Response {
59
+ return new Response(JSON.stringify({ error, error_description: description }), {
60
+ status,
61
+ headers: { "content-type": "application/json" },
62
+ });
63
+ }
64
+
65
+ /**
66
+ * GET /api/vault-caps — every vault registered in services.json, joined with
67
+ * its persisted cap (null = uncapped). Ordered by vault name.
68
+ */
69
+ export async function handleListVaultCaps(req: Request, deps: ApiVaultCapsDeps): Promise<Response> {
70
+ if (req.method !== "GET") {
71
+ return jsonError(405, "method_not_allowed", "use GET");
72
+ }
73
+ try {
74
+ await requireScope(deps.db, req, HOST_ADMIN_SCOPE, deps.issuer);
75
+ } catch (err) {
76
+ return adminAuthErrorResponse(err as AdminAuthError);
77
+ }
78
+ const manifestPath = deps.manifestPath ?? SERVICES_MANIFEST_PATH;
79
+ const names = listVaultNamesFromPath(manifestPath);
80
+ const caps: VaultCapWireShape[] = names.map((vaultName) => {
81
+ const cap = getVaultCap(deps.db, vaultName);
82
+ return {
83
+ vault_name: vaultName,
84
+ cap_bytes: cap?.capBytes ?? null,
85
+ created_at: cap?.createdAt ?? null,
86
+ updated_at: cap?.updatedAt ?? null,
87
+ };
88
+ });
89
+ return new Response(JSON.stringify({ vault_caps: caps }), {
90
+ status: 200,
91
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
92
+ });
93
+ }
94
+
95
+ interface SetCapBody {
96
+ cap_bytes: number;
97
+ }
98
+
99
+ interface ParseErr {
100
+ ok: false;
101
+ status: number;
102
+ error: string;
103
+ description: string;
104
+ }
105
+
106
+ async function parseSetCapBody(req: Request): Promise<{ ok: true; body: SetCapBody } | ParseErr> {
107
+ const ctype = req.headers.get("content-type") ?? "";
108
+ if (!ctype.toLowerCase().includes("application/json")) {
109
+ return {
110
+ ok: false,
111
+ status: 400,
112
+ error: "invalid_request",
113
+ description: "Content-Type must be application/json",
114
+ };
115
+ }
116
+ let raw: unknown;
117
+ try {
118
+ raw = await req.json();
119
+ } catch (err) {
120
+ const msg = err instanceof Error ? err.message : String(err);
121
+ return {
122
+ ok: false,
123
+ status: 400,
124
+ error: "invalid_request",
125
+ description: `invalid JSON body: ${msg}`,
126
+ };
127
+ }
128
+ if (!raw || typeof raw !== "object") {
129
+ return {
130
+ ok: false,
131
+ status: 400,
132
+ error: "invalid_request",
133
+ description: "request body must be a JSON object",
134
+ };
135
+ }
136
+ const obj = raw as Record<string, unknown>;
137
+ const capBytes = obj.cap_bytes;
138
+ // Positive integer only — the table's CHECK (cap_bytes > 0) is the at-rest
139
+ // backstop; this is the edge validation so the operator gets a clean 400
140
+ // instead of a sqlite constraint error. A fractional byte count (which a byte
141
+ // count can never be) is rejected rather than silently floored downstream.
142
+ if (typeof capBytes !== "number" || !Number.isInteger(capBytes) || capBytes <= 0) {
143
+ return {
144
+ ok: false,
145
+ status: 400,
146
+ error: "invalid_request",
147
+ description: '"cap_bytes" must be a positive integer number of bytes',
148
+ };
149
+ }
150
+ return { ok: true, body: { cap_bytes: capBytes } };
151
+ }
152
+
153
+ /**
154
+ * PUT /api/vault-caps/:name — set or update a vault's storage cap.
155
+ *
156
+ * Order of checks (mirrors the /api/users handlers):
157
+ *
158
+ * 1. Method gate (405 on non-PUT).
159
+ * 2. Bearer carries `parachute:host:admin` (401 / 403 via `requireScope`).
160
+ * 3. Parse + validate body (400 on shape / non-positive cap).
161
+ * 4. Vault is registered in services.json (400 `vault_not_found`) — refuse
162
+ * to seed a cap for a vault that doesn't exist.
163
+ * 5. `setVaultCap` (upsert) — overwrites the cap, bumps `updated_at`,
164
+ * preserves the original `created_at`.
165
+ *
166
+ * Response on success: `200 { vault_cap: <wire shape> }`.
167
+ */
168
+ export async function handleSetVaultCap(
169
+ req: Request,
170
+ vaultName: string,
171
+ deps: ApiVaultCapsDeps,
172
+ ): Promise<Response> {
173
+ if (req.method !== "PUT") {
174
+ return jsonError(405, "method_not_allowed", "use PUT");
175
+ }
176
+ try {
177
+ await requireScope(deps.db, req, HOST_ADMIN_SCOPE, deps.issuer);
178
+ } catch (err) {
179
+ return adminAuthErrorResponse(err as AdminAuthError);
180
+ }
181
+ const parsed = await parseSetCapBody(req);
182
+ if (!parsed.ok) {
183
+ return jsonError(parsed.status, parsed.error, parsed.description);
184
+ }
185
+ const manifestPath = deps.manifestPath ?? SERVICES_MANIFEST_PATH;
186
+ const known = new Set(listVaultNamesFromPath(manifestPath));
187
+ if (!known.has(vaultName)) {
188
+ return jsonError(
189
+ 400,
190
+ "vault_not_found",
191
+ `vault "${vaultName}" is not registered in services.json`,
192
+ );
193
+ }
194
+ const cap = setVaultCap(deps.db, vaultName, parsed.body.cap_bytes);
195
+ console.log(`vault cap set: vault=${vaultName} cap_bytes=${cap.capBytes}`);
196
+ const wire: VaultCapWireShape = {
197
+ vault_name: cap.vaultName,
198
+ cap_bytes: cap.capBytes,
199
+ created_at: cap.createdAt,
200
+ updated_at: cap.updatedAt,
201
+ };
202
+ return new Response(JSON.stringify({ vault_cap: wire }), {
203
+ status: 200,
204
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
205
+ });
206
+ }
@@ -11,6 +11,7 @@ import {
11
11
  KNOWN_MODULES,
12
12
  type ServiceSpec,
13
13
  composeServiceSpec,
14
+ focusForShort,
14
15
  knownServices,
15
16
  } from "../service-spec.ts";
16
17
  import type { ServiceEntry } from "../services-manifest.ts";
@@ -108,10 +109,14 @@ function defaultAvailability(): InteractiveAvailability {
108
109
  }
109
110
 
110
111
  /**
111
- * Survey the eligible services. We include the four first-party shortnames
112
- * (vault / notes / scribe / agent + runner) but flag agent as exploratory
113
- * in the blurb so operators don't grab it by reflex. `installed` is true when
114
- * the service has a row in services.json.
112
+ * Survey ALL known first-party shortnames (vault / notes / scribe / agent /
113
+ * runner / surface) regardless of tier `installed` is true when the service
114
+ * has a row in services.json. The fresh-install OFFER is narrowed downstream
115
+ * by `isOfferable` (drops already-installed + `deprecated`-tier shorts —
116
+ * notes / runner); agent (`experimental`) is flagged exploratory in its blurb
117
+ * but stays offered. Surveying everything keeps `installed` detection complete
118
+ * (the "already installed" banner still lists a deprecated module an operator
119
+ * has on disk).
115
120
  *
116
121
  * The full ServiceSpec is only available pre-install for FIRST_PARTY_FALLBACKS
117
122
  * shorts (notes — it carries a vendored manifest). KNOWN_MODULES shorts
@@ -149,10 +154,30 @@ function surveyServices(manifestPath: string): ServiceChoice[] {
149
154
  });
150
155
  }
151
156
 
157
+ /**
158
+ * A surveyed service is OFFERED on a fresh setup iff it is not already
159
+ * installed AND its discovery tier is not `deprecated` (2026-06-25). The
160
+ * deprecated tier (notes-daemon, runner) stays resolvable + manageable for an
161
+ * existing install — it just isn't pushed on a fresh box. `agent`
162
+ * (`experimental`) is still offered. Exported so the setup tests can pin the
163
+ * exclusion directly.
164
+ */
165
+ export function isOfferable(choice: { short: string; installed: boolean }): boolean {
166
+ if (choice.installed) return false;
167
+ // No `declared` arg: the survey fires PRE-install, before any module.json is
168
+ // on disk to read a self-declared `focus` from — so the static default map is
169
+ // the only signal available + the right one here.
170
+ return focusForShort(choice.short) !== "deprecated";
171
+ }
172
+
152
173
  const BLURBS: Record<string, string> = {
153
174
  vault: "knowledge graph (MCP) — your owner-authenticated note + tag store",
175
+ surface: "Parachute UI host — auto-installs Notes on first boot (the recommended UI path)",
176
+ // `app` is the pre-2026-05-27 name for `surface`; kept for any legacy survey row.
154
177
  app: "Parachute UI host — auto-installs Notes on first boot (recommended over notes-daemon)",
155
- notes: "Notes PWA web/mobile UI on top of vault (notes-daemon; superseded by `app`)",
178
+ // notes / runner are `deprecated` (not offered on a fresh setup) these
179
+ // blurbs only render if a legacy install surfaces them in the survey.
180
+ notes: "Notes PWA — web/mobile UI on top of vault (notes-daemon; superseded by `surface`)",
156
181
  scribe: "audio transcription for dictation + recordings",
157
182
  runner: "vault-as-job-substrate — scheduled claude -p against vault job notes",
158
183
  agent:
@@ -306,7 +331,7 @@ export async function setup(opts: SetupOpts = {}): Promise<number> {
306
331
 
307
332
  const survey = surveyServices(manifestPath);
308
333
  const installed = survey.filter((s) => s.installed);
309
- const offered = survey.filter((s) => !s.installed);
334
+ const offered = survey.filter(isOfferable);
310
335
 
311
336
  if (installed.length > 0) {
312
337
  log("Already installed:");
package/src/hub-server.ts CHANGED
@@ -96,6 +96,8 @@
96
96
  * /api/users/vaults (GET) → vault-name list for assigned-vault picker (host:admin)
97
97
  * /api/users/<id> (DELETE) → hard-delete user + revoke tokens (host:admin)
98
98
  * /api/users/<id>/reset-password (POST) → admin-initiated password reset (host:admin)
99
+ * /api/vault-caps (GET) → list vaults + persisted storage caps (host:admin)
100
+ * /api/vault-caps/<name> (PUT) → set/update a vault's storage cap (host:admin)
99
101
  * /login (GET + POST) → operator password login
100
102
  * /login/2fa (POST) → second-factor (TOTP/backup) step
101
103
  * (hub#473; reached after a correct
@@ -221,6 +223,7 @@ import {
221
223
  handleResetUserPassword,
222
224
  handleUpdateUserVaults,
223
225
  } from "./api-users.ts";
226
+ import { handleListVaultCaps, handleSetVaultCap } from "./api-vault-caps.ts";
224
227
  import { gateUiAudience, resolveUiMount } from "./audience-gate.ts";
225
228
  import {
226
229
  CHROME_OPT_OUT_PREFIXES,
@@ -3351,6 +3354,30 @@ export function hubFetch(
3351
3354
  });
3352
3355
  }
3353
3356
 
3357
+ // Per-vault storage caps (B5 admin visibility / D-slice). GET lists every
3358
+ // vault from services.json joined with its persisted cap; PUT /:name
3359
+ // sets/updates a cap. host:admin-gated, same gate flavor as /api/users.
3360
+ if (pathname === "/api/vault-caps") {
3361
+ if (!getDb) return dbNotConfigured();
3362
+ return handleListVaultCaps(req, {
3363
+ db: getDb(),
3364
+ issuer: oauthDeps(req).issuer,
3365
+ manifestPath,
3366
+ });
3367
+ }
3368
+ if (pathname.startsWith("/api/vault-caps/")) {
3369
+ if (!getDb) return dbNotConfigured();
3370
+ const name = decodeURIComponent(pathname.slice("/api/vault-caps/".length));
3371
+ if (!name || name.includes("/")) {
3372
+ return new Response("not found", { status: 404 });
3373
+ }
3374
+ return handleSetVaultCap(req, name, {
3375
+ db: getDb(),
3376
+ issuer: oauthDeps(req).issuer,
3377
+ manifestPath,
3378
+ });
3379
+ }
3380
+
3354
3381
  // Canonical login/logout. The handlers themselves are unchanged from
3355
3382
  // when they lived at /admin/login + /admin/logout; the rename surfaced
3356
3383
  // via #231-followup so the URL reflects the surface's actual scope
@@ -66,16 +66,26 @@ export interface ConfigSchema {
66
66
  }
67
67
 
68
68
  /**
69
- * Discovery tier (2026-06-09 modular-UI architecture). `core` modules are the
70
- * product surface (vault / scribe / hub / surface); `experimental` modules
71
- * (channel / runner / others) render in a de-emphasized group on the Modules
72
- * screen. **Show all; never hide** `focus` only sorts + labels.
69
+ * Discovery tier (2026-06-09 modular-UI architecture). Three tiers today:
70
+ * - `core` — the product surface (vault / scribe / hub / surface).
71
+ * - `experimental` legit previews (agent) that render in a de-emphasized
72
+ * group but ARE offered on a fresh install.
73
+ * - `deprecated` — modules retained for back-compat (notes-daemon, runner)
74
+ * that stay resolvable + shown-if-installed (so an existing operator can
75
+ * still manage / uninstall them) but are NOT offered on a fresh setup
76
+ * (2026-06-25). Distinct from a fully retired module (`RETIRED_MODULES`),
77
+ * whose services.json row is GC'd on load.
78
+ *
79
+ * **Show all installed; never hide** — `focus` only sorts + labels for an
80
+ * installed/known module. The one behavioral lever it pulls is the fresh-install
81
+ * OFFER: `deprecated` shorts are dropped from the setup wizard + the admin SPA's
82
+ * "available to install" set.
73
83
  *
74
84
  * Absent in a `module.json` ⇒ the hub falls back to its default map (see
75
85
  * `service-spec.focusForShort`), which defaults unlisted modules to
76
86
  * `experimental`.
77
87
  */
78
- export type ModuleFocus = "core" | "experimental";
88
+ export type ModuleFocus = "core" | "experimental" | "deprecated";
79
89
 
80
90
  /**
81
91
  * An event a module EMITS — the left-hand side of a Connection (2026-06-09
@@ -324,8 +334,10 @@ export interface ModuleManifest {
324
334
  * Discovery tier (2026-06-09 modular-UI architecture). When a module
325
335
  * declares `focus`, the hub's Modules screen uses it verbatim; otherwise it
326
336
  * falls back to `service-spec.focusForShort` (vault/scribe/hub/surface →
327
- * `core`, everything else → `experimental`). **Show all; never hide** —
328
- * `focus` only groups + de-emphasizes. Additive + back-compatible.
337
+ * `core`, notes/runner → `deprecated`, everything else → `experimental`).
338
+ * **Show all installed; never hide** — `focus` groups + de-emphasizes; the
339
+ * one behavioral lever is the fresh-install OFFER, which excludes
340
+ * `deprecated`. Additive + back-compatible.
329
341
  */
330
342
  readonly focus?: ModuleFocus;
331
343
  /**
@@ -711,12 +723,14 @@ function asCredentials(v: unknown, where: string): readonly ModuleCredential[] |
711
723
  });
712
724
  }
713
725
 
714
- const MODULE_FOCUS_VALUES = new Set<ModuleFocus>(["core", "experimental"]);
726
+ const MODULE_FOCUS_VALUES = new Set<ModuleFocus>(["core", "experimental", "deprecated"]);
715
727
 
716
728
  function asFocus(v: unknown, where: string): ModuleFocus | undefined {
717
729
  if (v === undefined) return undefined;
718
730
  if (typeof v !== "string" || !MODULE_FOCUS_VALUES.has(v as ModuleFocus)) {
719
- throw new ModuleManifestError(`${where}: "focus" must be "core" | "experimental" if present`);
731
+ throw new ModuleManifestError(
732
+ `${where}: "focus" must be "core" | "experimental" | "deprecated" if present`,
733
+ );
720
734
  }
721
735
  return v as ModuleFocus;
722
736
  }
@@ -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 manifest-declared `focus`; this map is the
642
- * fallback when `module.json` omits it (and the bootstrap value before a
643
- * module is installed). vault / scribe / hub / surface are `core`; everything
644
- * else (agent / runner / notes / unknown third-party) defaults to
645
- * `experimental`. **Show all; never hide** `focus` only groups + labels.
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: "experimental",
654
- notes: "experimental",
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` is intentionally included — it's still resolvable (vendored fallback)
678
- * for legacy installs; it surfaces as `experimental` and isn't pushed as a
679
- * fresh install. Callers that want only the "recommended fresh-install" subset
680
- * can filter by `focus === "core"`.
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>();
@@ -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)}