@ogygie/studio 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -2203,7 +2203,7 @@ async function registerStudioRoutes(app, resolver) {
2203
2203
  }
2204
2204
 
2205
2205
  // src/server/hosted/routes.ts
2206
- import { z as z9 } from "zod";
2206
+ import { z as z10 } from "zod";
2207
2207
 
2208
2208
  // src/server/hosted/auth.ts
2209
2209
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
@@ -2258,19 +2258,27 @@ function consumeMagicLink(deps, token) {
2258
2258
  deps.consumed.set(nonce, exp);
2259
2259
  return email;
2260
2260
  }
2261
- function issueSession(deps, email) {
2262
- return seal({ kind: "session", email: email.toLowerCase(), exp: deps.now() + deps.ttlHours * 36e5 }, deps.secret);
2261
+ function issueSession(deps, user) {
2262
+ return seal(
2263
+ { kind: "session", email: user.email.toLowerCase(), displayName: user.displayName, role: user.role, via: user.via, exp: deps.now() + deps.ttlHours * 36e5 },
2264
+ deps.secret
2265
+ );
2263
2266
  }
2267
+ var ROLES = /* @__PURE__ */ new Set(["reader", "author", "approver", "operator"]);
2268
+ var VIAS = /* @__PURE__ */ new Set(["magic-link", "entra"]);
2264
2269
  function readSession(deps, cookieHeader) {
2265
2270
  const token = cookieValue(cookieHeader, SESSION_COOKIE);
2266
2271
  if (token === null) return null;
2267
2272
  const value = open(token, deps.secret);
2268
2273
  const email = value?.["email"];
2274
+ const displayName = value?.["displayName"];
2275
+ const role = value?.["role"];
2276
+ const via = value?.["via"];
2269
2277
  const exp = value?.["exp"];
2270
- if (value?.["kind"] !== "session" || typeof email !== "string" || typeof exp !== "number" || exp <= deps.now()) {
2278
+ if (value?.["kind"] !== "session" || typeof email !== "string" || typeof displayName !== "string" || typeof role !== "string" || !ROLES.has(role) || typeof via !== "string" || !VIAS.has(via) || typeof exp !== "number" || exp <= deps.now()) {
2271
2279
  return null;
2272
2280
  }
2273
- return email;
2281
+ return { email, displayName, role, via };
2274
2282
  }
2275
2283
  function cookieValue(cookieHeader, name) {
2276
2284
  if (cookieHeader === void 0) return null;
@@ -2319,6 +2327,21 @@ function unauthenticated(cause) {
2319
2327
  // src/server/hosted/config.ts
2320
2328
  import { z as z8 } from "zod";
2321
2329
  var HostRoleSchema = z8.enum(["reader", "author", "approver", "operator"]);
2330
+ var HOST_ROLES = HostRoleSchema.options;
2331
+ var HostIdentitySchema = z8.object({
2332
+ kind: z8.literal("entra"),
2333
+ tenantId: z8.string().min(1),
2334
+ clientId: z8.string().min(1),
2335
+ clientSecretPath: z8.string().min(1),
2336
+ /** Where the tenant writes the groups in the id token — a setting of the app registration. */
2337
+ groupsClaim: z8.string().min(1).default("groups"),
2338
+ roles: z8.object({
2339
+ reader: z8.array(z8.string().min(1)).default([]),
2340
+ author: z8.array(z8.string().min(1)).default([]),
2341
+ approver: z8.array(z8.string().min(1)).default([]),
2342
+ operator: z8.array(z8.string().min(1)).default([])
2343
+ })
2344
+ });
2322
2345
  var MergePolicySchema = z8.enum(["client", "operator"]);
2323
2346
  var HostUserSchema = z8.object({
2324
2347
  email: z8.email(),
@@ -2332,7 +2355,9 @@ var HostConfigSchema = z8.object({
2332
2355
  installationId: z8.string().min(1),
2333
2356
  privateKeyPath: z8.string().min(1)
2334
2357
  }),
2335
- users: z8.array(HostUserSchema).min(1),
2358
+ /** The addresses the magic link serves; optional once `identity` opens the door (D59). */
2359
+ users: z8.array(HostUserSchema).default([]),
2360
+ identity: HostIdentitySchema.optional(),
2336
2361
  /** "operator" at onboarding: the agency keeps the merge until it hands it over (D48). */
2337
2362
  mergePolicy: MergePolicySchema.default("operator"),
2338
2363
  writeRateLimit: z8.number().int().positive().default(20),
@@ -2365,6 +2390,9 @@ var HostConfigSchema = z8.object({
2365
2390
  /** USD per million tokens, by model id — overrides the built-in table of the estimate (B8). */
2366
2391
  costPerMillionUsd: z8.record(z8.string(), z8.object({ input: z8.number().nonnegative(), output: z8.number().nonnegative() })).optional()
2367
2392
  }).optional()
2393
+ }).refine((config) => config.users.length > 0 || config.identity !== void 0, {
2394
+ path: ["users"],
2395
+ message: "declare at least one user, or an identity block (D59): a host nobody can enter is a configuration error"
2368
2396
  });
2369
2397
  function parseHostConfig(raw, configPath) {
2370
2398
  const parsed = HostConfigSchema.safeParse(raw);
@@ -2384,20 +2412,190 @@ function findHostUser(config, email) {
2384
2412
  return config.users.find((user) => user.email.toLowerCase() === wanted) ?? null;
2385
2413
  }
2386
2414
 
2415
+ // src/server/hosted/entra.ts
2416
+ import { createHash, createPublicKey, randomBytes as randomBytes2, verify as verifySignature } from "node:crypto";
2417
+ import { z as z9 } from "zod";
2418
+ var OIDC_SCOPE = "openid profile email";
2419
+ var CLOCK_SKEW_SECONDS = 60;
2420
+ var FILE = "packages/ogygie-studio/src/server/hosted/entra.ts";
2421
+ var baseUrlOf = (config) => `https://login.microsoftonline.com/${config.tenantId}`;
2422
+ var issuerOf = (config) => `${baseUrlOf(config)}/v2.0`;
2423
+ var tokenEndpointOf = (config) => `${baseUrlOf(config)}/oauth2/v2.0/token`;
2424
+ var jwksEndpointOf = (config) => `${baseUrlOf(config)}/discovery/v2.0/keys`;
2425
+ function codeChallengeOf(codeVerifier) {
2426
+ return createHash("sha256").update(codeVerifier).digest("base64url");
2427
+ }
2428
+ function newHandshake() {
2429
+ return { state: randomBytes2(16).toString("hex"), nonce: randomBytes2(16).toString("hex"), codeVerifier: randomBytes2(48).toString("base64url") };
2430
+ }
2431
+ function authorizationUrlOf(config, handshake) {
2432
+ const url = new URL(`${baseUrlOf(config)}/oauth2/v2.0/authorize`);
2433
+ url.searchParams.set("client_id", config.clientId);
2434
+ url.searchParams.set("response_type", "code");
2435
+ url.searchParams.set("redirect_uri", config.callbackUrl);
2436
+ url.searchParams.set("response_mode", "query");
2437
+ url.searchParams.set("scope", OIDC_SCOPE);
2438
+ url.searchParams.set("state", handshake.state);
2439
+ url.searchParams.set("nonce", handshake.nonce);
2440
+ url.searchParams.set("code_challenge", codeChallengeOf(handshake.codeVerifier));
2441
+ url.searchParams.set("code_challenge_method", "S256");
2442
+ return url.toString();
2443
+ }
2444
+ function unauthenticated2(cause, suggestedFix) {
2445
+ return throwOgygieError(
2446
+ { code: "HOST_UNAUTHENTICATED", message: "The Microsoft sign-in did not establish an identity.", file: FILE, cause, suggestedFix },
2447
+ 401
2448
+ );
2449
+ }
2450
+ var IdTokenClaimsSchema = z9.looseObject({
2451
+ sub: z9.string().min(1),
2452
+ iss: z9.string().min(1),
2453
+ aud: z9.union([z9.string(), z9.array(z9.string())]),
2454
+ exp: z9.number(),
2455
+ nonce: z9.string().optional(),
2456
+ email: z9.string().optional(),
2457
+ preferred_username: z9.string().optional(),
2458
+ upn: z9.string().optional(),
2459
+ name: z9.string().optional()
2460
+ });
2461
+ var RsaSigningKeySchema = z9.object({ kid: z9.string().min(1), kty: z9.literal("RSA"), n: z9.string().min(1), e: z9.string().min(1) });
2462
+ var JwksSchema = z9.object({ keys: z9.array(z9.unknown()) });
2463
+ var TokenResponseSchema = z9.looseObject({ id_token: z9.string().min(1) });
2464
+ function decodeJwt(token) {
2465
+ const [encodedHeader, encodedPayload, encodedSignature, ...rest] = token.split(".");
2466
+ if (encodedHeader === void 0 || encodedPayload === void 0 || encodedSignature === void 0 || rest.length > 0) return null;
2467
+ try {
2468
+ const header = JSON.parse(Buffer.from(encodedHeader, "base64url").toString("utf8"));
2469
+ if (typeof header !== "object" || header === null) return null;
2470
+ const { alg, kid } = header;
2471
+ return {
2472
+ header: { ...typeof alg === "string" ? { alg } : {}, ...typeof kid === "string" ? { kid } : {} },
2473
+ payload: JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8")),
2474
+ signingInput: `${encodedHeader}.${encodedPayload}`,
2475
+ signature: Buffer.from(encodedSignature, "base64url")
2476
+ };
2477
+ } catch {
2478
+ return null;
2479
+ }
2480
+ }
2481
+ function verifyIdToken(input) {
2482
+ const decoded = decodeJwt(input.idToken);
2483
+ if (decoded === null) return unauthenticated2("id token is not a readable JWT", "Retry the sign-in.");
2484
+ if (decoded.header.alg !== "RS256") {
2485
+ return unauthenticated2(`id token signed with alg=${decoded.header.alg ?? "none"}, only RS256 is accepted`, "Check the signing algorithm of the app registration (RS256).");
2486
+ }
2487
+ const candidates = input.keys.map((key) => RsaSigningKeySchema.safeParse(key)).flatMap((parsed) => parsed.success ? [parsed.data] : []).filter((key) => decoded.header.kid === void 0 || key.kid === decoded.header.kid);
2488
+ if (candidates.length === 0) {
2489
+ return unauthenticated2(`no published RSA key matches kid=${decoded.header.kid ?? "(absent)"}`, "Check that the id token comes from the configured tenant.");
2490
+ }
2491
+ const signed = candidates.some(
2492
+ (key) => verifySignature("sha256", Buffer.from(decoded.signingInput, "utf8"), createPublicKey({ key: { kty: "RSA", n: key.n, e: key.e }, format: "jwk" }), decoded.signature)
2493
+ );
2494
+ if (!signed) return unauthenticated2("id token signature does not match the keys published by the tenant", "Retry the sign-in; if it persists, check the tenant of this host.");
2495
+ const claims = IdTokenClaimsSchema.safeParse(decoded.payload);
2496
+ if (!claims.success) return unauthenticated2("id token claims do not have the expected shape", "Check the app registration (openid scope, id token).");
2497
+ const expectedIssuer = issuerOf(input.config);
2498
+ if (claims.data.iss !== expectedIssuer) return unauthenticated2(`id token issued by ${claims.data.iss}, expected ${expectedIssuer}`, "Point identity.tenantId at the tenant that issues the token.");
2499
+ const audiences = Array.isArray(claims.data.aud) ? claims.data.aud : [claims.data.aud];
2500
+ if (!audiences.includes(input.config.clientId)) return unauthenticated2("id token was issued for another application", "Check identity.clientId against the app registration the sign-in used.");
2501
+ if (claims.data.exp + CLOCK_SKEW_SECONDS < Math.floor(input.nowMs / 1e3)) return unauthenticated2("id token has expired", "Retry the sign-in.");
2502
+ if (claims.data.nonce !== input.nonce) return unauthenticated2("id token nonce does not match the one this handshake sent", "Retry the sign-in from the start; a token cannot be replayed.");
2503
+ return claims.data;
2504
+ }
2505
+ function groupsOfClaim(value) {
2506
+ if (typeof value === "string") return [value];
2507
+ if (!Array.isArray(value)) return [];
2508
+ return value.filter((group) => typeof group === "string" && group.length > 0);
2509
+ }
2510
+ function identityOfClaims(claims, groupsClaim) {
2511
+ const email = claims.email ?? claims.preferred_username ?? claims.upn;
2512
+ if (email === void 0 || email === "") {
2513
+ return unauthenticated2("the id token carries no email, preferred_username nor upn claim", "Add the optional email claim on the app registration.");
2514
+ }
2515
+ return { email: email.toLowerCase(), displayName: claims.name ?? email, groups: groupsOfClaim(claims[groupsClaim]) };
2516
+ }
2517
+ function roleOfGroups(groups, roles) {
2518
+ let found = null;
2519
+ for (const role of HOST_ROLES) if (roles[role].some((group) => group !== "" && groups.includes(group))) found = role;
2520
+ return found;
2521
+ }
2522
+ function hostUserOfIdentity(identity, declared, roles) {
2523
+ if (declared !== null) return { email: declared.email, displayName: declared.displayName, role: declared.role };
2524
+ const role = roleOfGroups(identity.groups, roles);
2525
+ if (role === null) {
2526
+ return throwOgygieError(
2527
+ {
2528
+ code: "HOST_ACCESS_DENIED",
2529
+ message: "This account is authenticated but not invited on this host.",
2530
+ file: null,
2531
+ cause: `${identity.email}: none of its ${identity.groups.length} group(s) is declared under identity.roles, and the address is not in users`,
2532
+ suggestedFix: "Add the account to one of the directory groups the host declares (identity.roles), or declare its address in users."
2533
+ },
2534
+ 403
2535
+ );
2536
+ }
2537
+ return { email: identity.email, displayName: identity.displayName, role };
2538
+ }
2539
+ async function readJson(response, endpoint) {
2540
+ if (!response.ok) return unauthenticated2(`${endpoint} answered ${response.status}`, "Retry the sign-in; if it persists, check the tenant, the client id and the client secret.");
2541
+ try {
2542
+ return await response.json();
2543
+ } catch {
2544
+ return unauthenticated2(`${endpoint} answered a body that is not JSON`, "Retry the sign-in.");
2545
+ }
2546
+ }
2547
+ async function exchangeAuthorizationCode(deps, config, input) {
2548
+ const endpoint = tokenEndpointOf(config);
2549
+ const response = await deps.fetchImpl(endpoint, {
2550
+ method: "POST",
2551
+ headers: { "content-type": "application/x-www-form-urlencoded" },
2552
+ body: new URLSearchParams({
2553
+ grant_type: "authorization_code",
2554
+ client_id: config.clientId,
2555
+ client_secret: config.clientSecret,
2556
+ code: input.code,
2557
+ code_verifier: input.codeVerifier,
2558
+ redirect_uri: config.callbackUrl
2559
+ }).toString()
2560
+ });
2561
+ const parsed = TokenResponseSchema.safeParse(await readJson(response, endpoint));
2562
+ if (!parsed.success) return unauthenticated2(`${endpoint} answered without an id_token`, "Check that the app registration requests the openid scope.");
2563
+ return parsed.data.id_token;
2564
+ }
2565
+ async function fetchSigningKeys(deps, config) {
2566
+ const endpoint = jwksEndpointOf(config);
2567
+ const response = await deps.fetchImpl(endpoint, { method: "GET", headers: {} });
2568
+ const parsed = JwksSchema.safeParse(await readJson(response, endpoint));
2569
+ if (!parsed.success) return unauthenticated2(`${endpoint} answered a document that is not a JWKS`, "Check identity.tenantId \u2014 the JWKS of the tenant is what verifies the id token.");
2570
+ return parsed.data.keys;
2571
+ }
2572
+
2387
2573
  // src/server/hosted/routes.ts
2574
+ var OIDC_COOKIE = "ogygie_host_oidc";
2575
+ var OIDC_TTL_MS = 10 * 60 * 1e3;
2576
+ var oidcCookie = (value) => `${OIDC_COOKIE}=${encodeURIComponent(value)}; Path=/api/auth/entra; Max-Age=${OIDC_TTL_MS / 1e3}; HttpOnly; Secure; SameSite=Lax`;
2577
+ var clearedOidcCookie = () => `${OIDC_COOKIE}=; Path=/api/auth/entra; Max-Age=0; HttpOnly; Secure; SameSite=Lax`;
2578
+ function cookieOf(cookieHeader, name) {
2579
+ if (cookieHeader === void 0) return null;
2580
+ for (const part of cookieHeader.split(";")) {
2581
+ const [key, ...rest] = part.trim().split("=");
2582
+ if (key === name) return decodeURIComponent(rest.join("="));
2583
+ }
2584
+ return null;
2585
+ }
2388
2586
  var usersByRequest = /* @__PURE__ */ new WeakMap();
2389
2587
  function hostedUserOf(request) {
2390
2588
  const user = usersByRequest.get(request);
2391
2589
  if (user === void 0) return unauthenticated("no session attached to this request");
2392
2590
  return user;
2393
2591
  }
2394
- var MagicLinkInputSchema = z9.object({ email: z9.email() });
2395
- var CallbackQuerySchema = z9.object({ token: z9.string().min(1) });
2396
- var MeOutputSchema = z9.object({
2397
- email: z9.email(),
2398
- displayName: z9.string(),
2592
+ var MagicLinkInputSchema = z10.object({ email: z10.email() });
2593
+ var CallbackQuerySchema = z10.object({ token: z10.string().min(1) });
2594
+ var MeOutputSchema = z10.object({
2595
+ email: z10.email(),
2596
+ displayName: z10.string(),
2399
2597
  role: HostRoleSchema,
2400
- host: z9.object({ environments: z9.boolean(), mergePolicy: z9.enum(["client", "operator"]) })
2598
+ host: z10.object({ environments: z10.boolean(), mergePolicy: z10.enum(["client", "operator"]) })
2401
2599
  });
2402
2600
  async function registerHostedRoutes(app, deps) {
2403
2601
  const sessions = {
@@ -2411,21 +2609,74 @@ async function registerHostedRoutes(app, deps) {
2411
2609
  consumed: deps.consumedLinks,
2412
2610
  ...deps.magicLinkTtlMs === void 0 ? {} : { ttlMs: deps.magicLinkTtlMs }
2413
2611
  };
2414
- const PUBLIC_ROUTES = /* @__PURE__ */ new Set(["/api/auth/magic-link", "/api/auth/callback", "/api/auth/logout"]);
2612
+ const PUBLIC_ROUTES = /* @__PURE__ */ new Set(["/api/auth/options", "/api/auth/magic-link", "/api/auth/callback", "/api/auth/logout", "/api/auth/entra/start", "/api/auth/entra/callback"]);
2415
2613
  app.addHook("onRequest", async (request) => {
2416
2614
  const url = request.url.split("?")[0] ?? request.url;
2417
2615
  if (!url.startsWith("/api/") || PUBLIC_ROUTES.has(url)) return;
2418
- const email = readSession(sessions, request.headers.cookie);
2419
- if (email === null) return unauthenticated("missing or expired session cookie");
2420
- const user = findHostUser(deps.config, email);
2421
- if (user === null) return unauthenticated(`session for ${email}, no longer a declared user`);
2422
- usersByRequest.set(request, user);
2616
+ const session = readSession(sessions, request.headers.cookie);
2617
+ if (session === null) return unauthenticated("missing or expired session cookie");
2618
+ if (session.via === "magic-link") {
2619
+ const declared = findHostUser(deps.config, session.email);
2620
+ if (declared === null) return unauthenticated(`session for ${session.email}, no longer a declared user`);
2621
+ usersByRequest.set(request, declared);
2622
+ return;
2623
+ }
2624
+ usersByRequest.set(request, { email: session.email, displayName: session.displayName, role: session.role });
2423
2625
  });
2424
2626
  const route = app.withTypeProvider();
2627
+ route.route({
2628
+ method: "GET",
2629
+ url: "/api/auth/options",
2630
+ schema: { response: { 200: z10.object({ magicLink: z10.boolean(), entra: z10.boolean() }) } },
2631
+ handler: async (_request, reply) => reply.code(200).send({ magicLink: deps.config.users.length > 0, entra: deps.entra !== void 0 })
2632
+ });
2633
+ if (deps.entra !== void 0) {
2634
+ const entra = deps.entra;
2635
+ route.route({
2636
+ method: "GET",
2637
+ url: "/api/auth/entra/start",
2638
+ handler: async (_request, reply) => {
2639
+ const handshake = newHandshake();
2640
+ const state = seal({ kind: "oidc", ...handshake, exp: deps.now() + OIDC_TTL_MS }, deps.sessionSecret);
2641
+ return reply.header("set-cookie", oidcCookie(state)).redirect(authorizationUrlOf(entra.config, handshake), 302);
2642
+ }
2643
+ });
2644
+ route.route({
2645
+ method: "GET",
2646
+ url: "/api/auth/entra/callback",
2647
+ schema: { querystring: z10.object({ code: z10.string().min(1).optional(), state: z10.string().min(1).optional(), error: z10.string().optional(), error_description: z10.string().optional() }) },
2648
+ handler: async (request, reply) => {
2649
+ const refused = (code, cause) => {
2650
+ deps.log(JSON.stringify({ action: "sign-in", provider: "entra", outcome: code, cause }));
2651
+ return reply.header("set-cookie", clearedOidcCookie()).redirect(`/login?error=${encodeURIComponent(code)}`, 302);
2652
+ };
2653
+ if (request.query.error !== void 0) return refused("HOST_UNAUTHENTICATED", `tenant answered ${request.query.error}: ${request.query.error_description ?? ""}`);
2654
+ const sealed = cookieOf(request.headers.cookie, OIDC_COOKIE);
2655
+ const handshake = sealed === null ? null : open(sealed, deps.sessionSecret);
2656
+ const expected = { state: handshake?.["state"], nonce: handshake?.["nonce"], codeVerifier: handshake?.["codeVerifier"], exp: handshake?.["exp"] };
2657
+ if (handshake?.["kind"] !== "oidc" || typeof expected.state !== "string" || typeof expected.nonce !== "string" || typeof expected.codeVerifier !== "string" || typeof expected.exp !== "number" || expected.exp <= deps.now()) {
2658
+ return refused("HOST_UNAUTHENTICATED", "no live handshake cookie: the sign-in took more than ten minutes, or did not start here");
2659
+ }
2660
+ if (request.query.state !== expected.state || request.query.code === void 0) return refused("HOST_UNAUTHENTICATED", "state mismatch or no code");
2661
+ try {
2662
+ const idToken = await exchangeAuthorizationCode(entra, entra.config, { code: request.query.code, codeVerifier: expected.codeVerifier });
2663
+ const keys = await fetchSigningKeys(entra, entra.config);
2664
+ const claims = verifyIdToken({ idToken, keys, config: entra.config, nonce: expected.nonce, nowMs: deps.now() });
2665
+ const identity = identityOfClaims(claims, entra.config.groupsClaim);
2666
+ const user = hostUserOfIdentity(identity, findHostUser(deps.config, identity.email), entra.config.roles);
2667
+ deps.log(JSON.stringify({ action: "sign-in", provider: "entra", actor: user.email, role: user.role, outcome: "ok" }));
2668
+ return reply.header("set-cookie", [clearedOidcCookie(), sessionCookie(issueSession(sessions, { ...user, via: "entra" }), deps.config.session.ttlHours)]).redirect("/", 302);
2669
+ } catch (thrown) {
2670
+ if (thrown instanceof OgygieFailure) return refused(thrown.error.code, thrown.error.cause);
2671
+ throw thrown;
2672
+ }
2673
+ }
2674
+ });
2675
+ }
2425
2676
  route.route({
2426
2677
  method: "POST",
2427
2678
  url: "/api/auth/magic-link",
2428
- schema: { body: MagicLinkInputSchema, response: { 202: z9.object({ accepted: z9.literal(true) }) } },
2679
+ schema: { body: MagicLinkInputSchema, response: { 202: z10.object({ accepted: z10.literal(true) }) } },
2429
2680
  handler: async (request, reply) => {
2430
2681
  const user = findHostUser(deps.config, request.body.email);
2431
2682
  if (user === null) {
@@ -2455,8 +2706,8 @@ ${link}
2455
2706
  const email = consumeMagicLink(links, request.query.token);
2456
2707
  const user = findHostUser(deps.config, email);
2457
2708
  if (user === null) return unauthenticated(`link for ${email}, no longer a declared user`);
2458
- deps.log(JSON.stringify({ action: "sign-in", actor: user.email, outcome: "ok" }));
2459
- return reply.header("set-cookie", sessionCookie(issueSession(sessions, user.email), deps.config.session.ttlHours)).redirect("/", 302);
2709
+ deps.log(JSON.stringify({ action: "sign-in", provider: "magic-link", actor: user.email, outcome: "ok" }));
2710
+ return reply.header("set-cookie", sessionCookie(issueSession(sessions, { ...user, via: "magic-link" }), deps.config.session.ttlHours)).redirect("/", 302);
2460
2711
  }
2461
2712
  });
2462
2713
  route.route({
@@ -2477,10 +2728,10 @@ ${link}
2477
2728
  }
2478
2729
 
2479
2730
  // src/server/hosted/brief-routes.ts
2480
- import { z as z11 } from "zod";
2731
+ import { z as z12 } from "zod";
2481
2732
 
2482
2733
  // src/server/hosted/attachments.ts
2483
- import { createHash } from "node:crypto";
2734
+ import { createHash as createHash2 } from "node:crypto";
2484
2735
  import { inflateRawSync } from "node:zlib";
2485
2736
  var ATTACHMENT_FORMATS = ["md", "txt", "csv", "json", "html", "xlsx", "pdf"];
2486
2737
  var MAX_ATTACHMENTS = 5;
@@ -2662,7 +2913,7 @@ function prepareBriefAttachments(raw) {
2662
2913
  if (buffer.length > MAX_ATTACHMENT_BYTES) return tooLarge(piece.name, `${buffer.length} bytes`, MAX_ATTACHMENT_BYTES, "bytes");
2663
2914
  totalBytes += buffer.length;
2664
2915
  if (totalBytes > MAX_ATTACHMENTS_BYTES) return tooLarge(piece.name, `${totalBytes} bytes in all`, MAX_ATTACHMENTS_BYTES, "bytes");
2665
- const identity = { name: piece.name, format, bytes: buffer.length, sha256: createHash("sha256").update(buffer).digest("hex") };
2916
+ const identity = { name: piece.name, format, bytes: buffer.length, sha256: createHash2("sha256").update(buffer).digest("hex") };
2666
2917
  if (format === "pdf") {
2667
2918
  if (!isPdf(buffer)) unsupported(piece.name, "the content does not start like a PDF");
2668
2919
  prepared.push({ ...identity, kind: "pdf", data: buffer.toString("base64") });
@@ -2710,7 +2961,7 @@ ${attachments.map((piece) => `- ${piece.name} (${piece.format}, ${sizeOf(piece.b
2710
2961
  var attachmentDocumentsOf = (attachments) => attachments.flatMap((piece) => piece.kind === "pdf" ? [{ name: piece.name, data: piece.data }] : []);
2711
2962
 
2712
2963
  // src/server/hosted/brief.ts
2713
- import { z as z10 } from "zod";
2964
+ import { z as z11 } from "zod";
2714
2965
  import { checkSpec, generationGrammarCard, readManifestGeneration } from "@ogygie/generate";
2715
2966
  var TEMPLATE_PATH2 = "specs/_TEMPLATE.spec.md";
2716
2967
  var PRODUCT_RULES_PATH = "AGENTS.product.md";
@@ -2735,10 +2986,10 @@ function asksUngenerated(brief) {
2735
2986
  async function readIfPresent(storage, path) {
2736
2987
  return await storage.exists(path) ? storage.readFile(path) : null;
2737
2988
  }
2738
- var ManifestRouteSchema = z10.looseObject({ method: z10.string(), path: z10.string(), schemaInput: z10.string().optional(), schemaOutput: z10.string().optional(), invalidates: z10.array(z10.string()).optional() });
2739
- var ManifestForPromptSchema = z10.looseObject({
2740
- product: z10.looseObject({ name: z10.string(), description: z10.string().optional() }).optional(),
2741
- features: z10.array(z10.looseObject({ name: z10.string(), intent: z10.string().optional(), status: z10.string().optional(), spec: z10.string().nullable().optional(), routes: z10.array(ManifestRouteSchema).optional() })).optional()
2989
+ var ManifestRouteSchema = z11.looseObject({ method: z11.string(), path: z11.string(), schemaInput: z11.string().optional(), schemaOutput: z11.string().optional(), invalidates: z11.array(z11.string()).optional() });
2990
+ var ManifestForPromptSchema = z11.looseObject({
2991
+ product: z11.looseObject({ name: z11.string(), description: z11.string().optional() }).optional(),
2992
+ features: z11.array(z11.looseObject({ name: z11.string(), intent: z11.string().optional(), status: z11.string().optional(), spec: z11.string().nullable().optional(), routes: z11.array(ManifestRouteSchema).optional() })).optional()
2742
2993
  });
2743
2994
  async function manifestSummary(storage) {
2744
2995
  const raw = await readIfPresent(storage, MANIFEST_PATH2);
@@ -2805,8 +3056,8 @@ function extractQuestions(text) {
2805
3056
  } catch {
2806
3057
  return { problem: "QUESTIONS: the JSON after the marker does not parse" };
2807
3058
  }
2808
- const shape = z10.object({
2809
- questions: z10.array(z10.object({ id: FeatureNameSchema, text: z10.string().trim().min(1), options: z10.array(z10.string().trim().min(1)).min(2).max(4) })).min(3).max(5)
3059
+ const shape = z11.object({
3060
+ questions: z11.array(z11.object({ id: FeatureNameSchema, text: z11.string().trim().min(1), options: z11.array(z11.string().trim().min(1)).min(2).max(4) })).min(3).max(5)
2810
3061
  });
2811
3062
  const checked = shape.safeParse(parsed);
2812
3063
  if (!checked.success) return { problem: `QUESTIONS: ${checked.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" ; ")}` };
@@ -3265,7 +3516,7 @@ async function registerBriefRoutes(app, deps) {
3265
3516
  route.route({
3266
3517
  method: "GET",
3267
3518
  url: "/api/briefs/:jobId",
3268
- schema: { params: z11.object({ jobId: z11.string().min(1) }), response: { 200: BriefJobSchema } },
3519
+ schema: { params: z12.object({ jobId: z12.string().min(1) }), response: { 200: BriefJobSchema } },
3269
3520
  handler: async (request, reply) => {
3270
3521
  hostedUserOf(request);
3271
3522
  return reply.code(200).send(jobs.read(request.params.jobId));
@@ -3885,17 +4136,17 @@ async function registerPromotionRoutes(app, deps) {
3885
4136
  }
3886
4137
 
3887
4138
  // src/server/hosted/usage.ts
3888
- import { z as z12 } from "zod";
3889
- var UsageEntrySchema = z12.object({
3890
- at: z12.string(),
4139
+ import { z as z13 } from "zod";
4140
+ var UsageEntrySchema = z13.object({
4141
+ at: z13.string(),
3891
4142
  /** brief → spec from the instance; `speculate` entries arrive with the generator's lot. */
3892
- action: z12.enum(["brief", "speculate"]),
3893
- attempt: z12.number().int().positive().optional(),
3894
- model: z12.string(),
3895
- inputTokens: z12.number().int().nonnegative(),
3896
- outputTokens: z12.number().int().nonnegative(),
3897
- actor: z12.string().optional(),
3898
- specId: z12.string().optional()
4143
+ action: z13.enum(["brief", "speculate"]),
4144
+ attempt: z13.number().int().positive().optional(),
4145
+ model: z13.string(),
4146
+ inputTokens: z13.number().int().nonnegative(),
4147
+ outputTokens: z13.number().int().nonnegative(),
4148
+ actor: z13.string().optional(),
4149
+ specId: z13.string().optional()
3899
4150
  });
3900
4151
  var USAGE_MARKER = "<!-- ogygie-usage -->";
3901
4152
  var DEFAULT_MODEL_COSTS = {
@@ -3930,7 +4181,7 @@ function parseUsageComment(body) {
3930
4181
  const block = /```json\n([\s\S]*?)\n```/.exec(body)?.[1];
3931
4182
  if (block === void 0) return [];
3932
4183
  try {
3933
- const parsed = z12.array(UsageEntrySchema).safeParse(JSON.parse(block));
4184
+ const parsed = z13.array(UsageEntrySchema).safeParse(JSON.parse(block));
3934
4185
  return parsed.success ? parsed.data : [];
3935
4186
  } catch {
3936
4187
  return [];
@@ -4005,9 +4256,9 @@ async function registerConsumptionRoutes(app, deps) {
4005
4256
  }
4006
4257
 
4007
4258
  // src/server/hosted/preview-routes.ts
4008
- import { z as z13 } from "zod";
4009
- var PreviewParamsSchema = SpecParamsSchema.extend({ state: z13.string().min(1).max(80) });
4010
- var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z13.string().min(1).max(80) });
4259
+ import { z as z14 } from "zod";
4260
+ var PreviewParamsSchema = SpecParamsSchema.extend({ state: z14.string().min(1).max(80) });
4261
+ var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z14.string().min(1).max(80) });
4011
4262
  var PREVIEW_CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
4012
4263
  async function registerPreviewRoutes(app, deps) {
4013
4264
  const route = app.withTypeProvider();
@@ -4049,7 +4300,7 @@ async function registerPreviewRoutes(app, deps) {
4049
4300
  }
4050
4301
 
4051
4302
  // src/server/hosted/composition-routes.ts
4052
- import { z as z14 } from "zod";
4303
+ import { z as z15 } from "zod";
4053
4304
 
4054
4305
  // src/server/hosted/compositions.ts
4055
4306
  var COMPOSITION_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
@@ -4093,7 +4344,7 @@ La composition \xAB ${composition} \xBB est retenue parmi celles propos\xE9es ;
4093
4344
  }
4094
4345
 
4095
4346
  // src/server/hosted/composition-routes.ts
4096
- var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z14.string().min(1).max(80) });
4347
+ var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z15.string().min(1).max(80) });
4097
4348
  async function registerCompositionRoutes(app, deps) {
4098
4349
  const route = app.withTypeProvider();
4099
4350
  route.route({
@@ -4826,6 +5077,7 @@ async function startHostedStudio(options) {
4826
5077
  const privateKeyPem = readSecret(config.githubApp.privateKeyPath, "the GitHub App private key");
4827
5078
  const sessionSecret = readSecret(config.session.secretPath, "the session secret", MIN_SESSION_SECRET_LENGTH);
4828
5079
  const mailApiKey = readSecret(config.mail.apiKeyPath, "the mail provider key");
5080
+ const entraClientSecret = config.identity === void 0 ? void 0 : readSecret(config.identity.clientSecretPath, "the Entra client secret");
4829
5081
  const modelApiKey = config.model === void 0 ? void 0 : process.env["OGYGIE_MODEL_API_KEY"] ?? "";
4830
5082
  if (modelApiKey !== void 0 && modelApiKey.length === 0) {
4831
5083
  throwOgygieError({
@@ -4892,6 +5144,19 @@ async function startHostedStudio(options) {
4892
5144
  now,
4893
5145
  log,
4894
5146
  consumedLinks: /* @__PURE__ */ new Map(),
5147
+ ...config.identity === void 0 || entraClientSecret === void 0 ? {} : {
5148
+ entra: {
5149
+ fetchImpl: fetch,
5150
+ config: {
5151
+ tenantId: config.identity.tenantId,
5152
+ clientId: config.identity.clientId,
5153
+ clientSecret: entraClientSecret,
5154
+ callbackUrl: `${config.publicUrl.replace(/\/$/, "")}/api/auth/entra/callback`,
5155
+ groupsClaim: config.identity.groupsClaim,
5156
+ roles: config.identity.roles
5157
+ }
5158
+ }
5159
+ },
4895
5160
  pulls: createGithubPullsClient({ github: config.repo, fetchImpl: githubApp.installationFetch }),
4896
5161
  readerFor,
4897
5162
  usage: createGithubUsageClient({ github: config.repo, fetchImpl: githubApp.installationFetch }),
@@ -0,0 +1 @@
1
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:"";--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:"Hanken Grotesk", "Helvetica Neue", Arial, sans-serif;--font-mono:"Fragment Mono", ui-monospace, "SF Mono", Menlo, monospace;--spacing:.25rem;--container-md:28rem;--container-2xl:42rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.8125rem;--text-sm--line-height:calc(1.25 / .875);--text-base:.9375rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.75rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:3rem;--text-3xl--line-height: 1.2 ;--text-4xl:4.5rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--leading-tight:1.2;--leading-normal:1.6;--leading-relaxed:1.625;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.5rem;--radius-lg:.75rem;--radius-xl:1.25rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--default-transition-duration:var(--duration-fast);--default-transition-timing-function:var(--ease-swell);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-display:"Cormorant Garamond", "Iowan Old Style", Georgia, serif;--radius-full:9999px;--spacing-1:.25rem;--spacing-2:.5rem;--spacing-3:.75rem;--spacing-4:1rem;--spacing-6:1.5rem;--spacing-8:2rem;--spacing-12:3rem;--spacing-16:4rem;--spacing-24:6rem;--ease-swell:cubic-bezier(.2, .7, .2, 1);--ease-retreat:cubic-bezier(.4, 0, .2, 1);--duration-fast:.16s;--duration-base:.32s;--duration-slow:.64s;--duration-count:.8s}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.top-0{top:0}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.-mx-8{margin-inline:calc(var(--spacing-8) * -1)}.mx-auto{margin-inline:auto}.-mt-8{margin-top:calc(var(--spacing-8) * -1)}.mt-1{margin-top:var(--spacing-1)}.mt-2{margin-top:var(--spacing-2)}.mt-3{margin-top:var(--spacing-3)}.mt-4{margin-top:var(--spacing-4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:var(--spacing-6)}.mt-auto{margin-top:auto}.mb-1{margin-bottom:var(--spacing-1)}.mb-3{margin-bottom:var(--spacing-3)}.mb-6{margin-bottom:var(--spacing-6)}.ml-1{margin-left:var(--spacing-1)}.ml-2{margin-left:var(--spacing-2)}.ml-3{margin-left:var(--spacing-3)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1{height:var(--spacing-1)}.h-2{height:var(--spacing-2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:var(--spacing-4)}.h-10{height:calc(var(--spacing) * 10)}.h-20{height:calc(var(--spacing) * 20)}.h-24{height:var(--spacing-24)}.h-40{height:calc(var(--spacing) * 40)}.h-64{height:calc(var(--spacing) * 64)}.h-96{height:calc(var(--spacing) * 96)}.h-\[480px\]{height:480px}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-2{width:var(--spacing-2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-4{width:var(--spacing-4)}.w-16{width:var(--spacing-16)}.w-44{width:calc(var(--spacing) * 44)}.w-60{width:calc(var(--spacing) * 60)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.animate-pulse{animation:var(--animate-pulse)}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[1\.6rem_minmax\(0\,1fr\)\]{grid-template-columns:1.6rem minmax(0,1fr)}.grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing-1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:var(--spacing-2)}.gap-3{gap:var(--spacing-3)}.gap-4{gap:var(--spacing-4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing-1) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing-1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing-2) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing-2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing-3) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing-3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing-4) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing-4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing-6) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing-6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-2{column-gap:var(--spacing-2)}.gap-x-3{column-gap:var(--spacing-3)}.gap-y-0\.5{row-gap:calc(var(--spacing) * .5)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--theme-border)}.self-center{align-self:center}.justify-self-center{justify-self:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:var(--radius-full)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-accent,.border-accent\/30{border-color:var(--theme-accent)}@supports (color:color-mix(in lab,red,red)){.border-accent\/30{border-color:color-mix(in oklab,var(--theme-accent) 30%,transparent)}}.border-accent\/35{border-color:var(--theme-accent)}@supports (color:color-mix(in lab,red,red)){.border-accent\/35{border-color:color-mix(in oklab,var(--theme-accent) 35%,transparent)}}.border-border{border-color:var(--theme-border)}.border-border-strong,.border-border-strong\/60{border-color:var(--theme-border-strong)}@supports (color:color-mix(in lab,red,red)){.border-border-strong\/60{border-color:color-mix(in oklab,var(--theme-border-strong) 60%,transparent)}}.border-danger\/30{border-color:var(--theme-danger)}@supports (color:color-mix(in lab,red,red)){.border-danger\/30{border-color:color-mix(in oklab,var(--theme-danger) 30%,transparent)}}.border-danger\/40{border-color:var(--theme-danger)}@supports (color:color-mix(in lab,red,red)){.border-danger\/40{border-color:color-mix(in oklab,var(--theme-danger) 40%,transparent)}}.border-orichalcum,.border-orichalcum\/40{border-color:var(--theme-orichalcum)}@supports (color:color-mix(in lab,red,red)){.border-orichalcum\/40{border-color:color-mix(in oklab,var(--theme-orichalcum) 40%,transparent)}}.border-positive\/30{border-color:var(--theme-positive)}@supports (color:color-mix(in lab,red,red)){.border-positive\/30{border-color:color-mix(in oklab,var(--theme-positive) 30%,transparent)}}.border-positive\/40{border-color:var(--theme-positive)}@supports (color:color-mix(in lab,red,red)){.border-positive\/40{border-color:color-mix(in oklab,var(--theme-positive) 40%,transparent)}}.border-transparent{border-color:#0000}.border-warning\/30{border-color:var(--theme-warning)}@supports (color:color-mix(in lab,red,red)){.border-warning\/30{border-color:color-mix(in oklab,var(--theme-warning) 30%,transparent)}}.border-warning\/40{border-color:var(--theme-warning)}@supports (color:color-mix(in lab,red,red)){.border-warning\/40{border-color:color-mix(in oklab,var(--theme-warning) 40%,transparent)}}.border-warning\/50{border-color:var(--theme-warning)}@supports (color:color-mix(in lab,red,red)){.border-warning\/50{border-color:color-mix(in oklab,var(--theme-warning) 50%,transparent)}}.border-t-accent{border-top-color:var(--theme-accent)}.border-t-border-strong{border-top-color:var(--theme-border-strong)}.border-t-positive{border-top-color:var(--theme-positive)}.bg-accent{background-color:var(--theme-accent)}.bg-accent-soft{background-color:var(--theme-accent-soft)}.bg-border{background-color:var(--theme-border)}.bg-danger-soft{background-color:var(--theme-danger-soft)}.bg-positive-soft{background-color:var(--theme-positive-soft)}.bg-surface{background-color:var(--theme-surface)}.bg-surface-raised,.bg-surface-raised\/60{background-color:var(--theme-surface-raised)}@supports (color:color-mix(in lab,red,red)){.bg-surface-raised\/60{background-color:color-mix(in oklab,var(--theme-surface-raised) 60%,transparent)}}.bg-surface-sunken,.bg-surface-sunken\/50{background-color:var(--theme-surface-sunken)}@supports (color:color-mix(in lab,red,red)){.bg-surface-sunken\/50{background-color:color-mix(in oklab,var(--theme-surface-sunken) 50%,transparent)}}.bg-surface-sunken\/60{background-color:var(--theme-surface-sunken)}@supports (color:color-mix(in lab,red,red)){.bg-surface-sunken\/60{background-color:color-mix(in oklab,var(--theme-surface-sunken) 60%,transparent)}}.bg-surface-sunken\/70{background-color:var(--theme-surface-sunken)}@supports (color:color-mix(in lab,red,red)){.bg-surface-sunken\/70{background-color:color-mix(in oklab,var(--theme-surface-sunken) 70%,transparent)}}.bg-surface\/85{background-color:var(--theme-surface)}@supports (color:color-mix(in lab,red,red)){.bg-surface\/85{background-color:color-mix(in oklab,var(--theme-surface) 85%,transparent)}}.bg-transparent{background-color:#0000}.bg-warning{background-color:var(--theme-warning)}.bg-warning-soft,.bg-warning-soft\/40{background-color:var(--theme-warning-soft)}@supports (color:color-mix(in lab,red,red)){.bg-warning-soft\/40{background-color:color-mix(in oklab,var(--theme-warning-soft) 40%,transparent)}}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing-1)}.p-3{padding:var(--spacing-3)}.p-4{padding:var(--spacing-4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:var(--spacing-6)}.px-1{padding-inline:var(--spacing-1)}.px-2{padding-inline:var(--spacing-2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:var(--spacing-3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:var(--spacing-4)}.px-6{padding-inline:var(--spacing-6)}.px-8{padding-inline:var(--spacing-8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing-1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:var(--spacing-2)}.py-3{padding-block:var(--spacing-3)}.py-4{padding-block:var(--spacing-4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:var(--spacing-6)}.py-8{padding-block:var(--spacing-8)}.py-12{padding-block:var(--spacing-12)}.pt-4{padding-top:var(--spacing-4)}.pt-6{padding-top:var(--spacing-6)}.pt-12{padding-top:var(--spacing-12)}.pr-1{padding-right:var(--spacing-1)}.pb-1{padding-bottom:var(--spacing-1)}.pb-2{padding-bottom:var(--spacing-2)}.pb-3{padding-bottom:var(--spacing-3)}.pb-4{padding-bottom:var(--spacing-4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-display{font-family:var(--font-display)}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.3em\]{--tw-tracking:.3em;letter-spacing:.3em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-\[0\.22em\]{--tw-tracking:.22em;letter-spacing:.22em}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-balance{text-wrap:balance}.break-all{word-break:break-all}.whitespace-normal{white-space:normal}.text-accent{color:var(--theme-accent)}.text-accent-contrast{color:var(--theme-accent-contrast)}.text-danger{color:var(--theme-danger)}.text-orichalcum{color:var(--theme-orichalcum)}.text-positive{color:var(--theme-positive)}.text-text{color:var(--theme-text)}.text-text-faint{color:var(--theme-text-faint)}.text-text-muted{color:var(--theme-text-muted)}.text-warning{color:var(--theme-warning)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline-offset-4{text-underline-offset:4px}.opacity-55{opacity:.55}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_4px_var\(--theme-accent-glow\)\]{--tw-shadow:0 0 0 4px var(--tw-shadow-color,var(--theme-accent-glow));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-glow{--tw-shadow:var(--theme-shadow-glow);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-overlay{--tw-shadow:var(--theme-shadow-overlay);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-raised{--tw-shadow:var(--theme-shadow-raised);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[background\,transform\]{transition-property:background,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[background-color\,border-color\,color\,transform\]{transition-property:background-color,border-color,color,transform;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[border-color\,transform\,box-shadow\]{transition-property:border-color,transform,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[transform\,box-shadow\,border-color\]{transition-property:transform,box-shadow,border-color;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[transform\,filter\]{transition-property:transform,filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-\(--duration-base\){--tw-duration:var(--duration-base);transition-duration:var(--duration-base)}.duration-\(--duration-fast\){--tw-duration:var(--duration-fast);transition-duration:var(--duration-fast)}.duration-\(--duration-slow\){--tw-duration:var(--duration-slow);transition-duration:var(--duration-slow)}.ease-swell{--tw-ease:var(--ease-swell);transition-timing-function:var(--ease-swell)}.select-none{-webkit-user-select:none;user-select:none}@media(hover:hover){.group-hover\:text-accent:is(:where(.group):hover *){color:var(--theme-accent)}}.marker\:text-accent ::marker{color:var(--theme-accent)}.marker\:text-accent::marker{color:var(--theme-accent)}.marker\:text-accent ::-webkit-details-marker{color:var(--theme-accent)}.marker\:text-accent::-webkit-details-marker{color:var(--theme-accent)}.marker\:text-orichalcum ::marker{color:var(--theme-orichalcum)}.marker\:text-orichalcum::marker{color:var(--theme-orichalcum)}.marker\:text-orichalcum ::-webkit-details-marker{color:var(--theme-orichalcum)}.marker\:text-orichalcum::-webkit-details-marker{color:var(--theme-orichalcum)}.file\:mr-3::file-selector-button{margin-right:var(--spacing-3)}.file\:rounded-md::file-selector-button{border-radius:var(--radius-md)}.file\:border::file-selector-button{border-style:var(--tw-border-style);border-width:1px}.file\:border-border::file-selector-button{border-color:var(--theme-border)}.file\:bg-surface::file-selector-button{background-color:var(--theme-surface)}.file\:px-3::file-selector-button{padding-inline:var(--spacing-3)}.file\:py-1\.5::file-selector-button{padding-block:calc(var(--spacing) * 1.5)}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:text-text::file-selector-button{color:var(--theme-text)}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:top-2:before{content:var(--tw-content);top:var(--spacing-2)}.before\:bottom-2:before{content:var(--tw-content);bottom:var(--spacing-2)}.before\:left-\[0\.8rem\]:before{content:var(--tw-content);left:.8rem}.before\:w-0\.5:before{content:var(--tw-content);width:calc(var(--spacing) * .5)}.before\:bg-border:before{content:var(--tw-content);background-color:var(--theme-border)}.first\:mt-0:first-child{margin-top:0}@media(hover:hover){.hover\:translate-x-0\.5:hover{--tw-translate-x:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:-translate-y-px:hover{--tw-translate-y:-1px;translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-accent:hover,.hover\:border-accent\/60:hover{border-color:var(--theme-accent)}@supports (color:color-mix(in lab,red,red)){.hover\:border-accent\/60:hover{border-color:color-mix(in oklab,var(--theme-accent) 60%,transparent)}}.hover\:border-border-strong:hover{border-color:var(--theme-border-strong)}.hover\:border-danger:hover{border-color:var(--theme-danger)}.hover\:bg-accent-strong:hover{background-color:var(--theme-accent-strong)}.hover\:bg-surface-sunken:hover,.hover\:bg-surface-sunken\/40:hover{background-color:var(--theme-surface-sunken)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-surface-sunken\/40:hover{background-color:color-mix(in oklab,var(--theme-surface-sunken) 40%,transparent)}}.hover\:text-accent:hover{color:var(--theme-accent)}.hover\:text-text:hover{color:var(--theme-text)}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-overlay:hover{--tw-shadow:var(--theme-shadow-overlay);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:brightness-110:hover{--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.focus\:border-accent:focus{border-color:var(--theme-accent)}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-accent:focus-visible{outline-color:var(--theme-accent)}.active\:scale-\[0\.98\]:active{scale:.98}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:48rem){.md\:block{display:block}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-\[1fr_auto_1fr_auto_1fr\]{grid-template-columns:1fr auto 1fr auto 1fr}.md\:items-stretch{align-items:stretch}}@media(min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}.\[\&\.active\]\:bg-accent-soft.active{background-color:var(--theme-accent-soft)}.\[\&\.active\]\:font-medium.active{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.\[\&\.active\]\:text-accent.active{color:var(--theme-accent)}}:root{color-scheme:dark;--theme-surface:#0b1322;--theme-surface-raised:#131d30;--theme-surface-sunken:#060b15;--theme-border:#223049;--theme-border-strong:#5a6d93;--theme-text:#ebe6da;--theme-text-muted:#a2abba;--theme-text-faint:#7b8694;--theme-accent:#5cd6cc;--theme-accent-strong:#86e6de;--theme-accent-soft:#0e2b31;--theme-accent-contrast:#0b1322;--theme-accent-glow:#5cd6cc29;--theme-orichalcum:#d9a35a;--theme-positive:#8fd67a;--theme-positive-soft:#152a16;--theme-warning:#f2d06b;--theme-warning-soft:#2e2410;--theme-danger:#ff8a7a;--theme-danger-soft:#3a1612;--theme-focus:#5cd6cc;--theme-shadow-raised:inset 0 1px 0 #ffffff0d, 0 8px 24px #0006;--theme-shadow-overlay:inset 0 1px 0 #ffffff14, 0 24px 64px #000000a6;--theme-shadow-glow:0 0 0 1px #5cd6cc66, 0 0 48px #5cd6cc33}:root[data-theme=light]{color-scheme:light;--theme-surface:#f6f3ec;--theme-surface-raised:#fff;--theme-surface-sunken:#ece8de;--theme-border:#ddd7ca;--theme-border-strong:#8b8571;--theme-text:#121a2a;--theme-text-muted:#5a6475;--theme-text-faint:#667080;--theme-accent:#0f7580;--theme-accent-strong:#0a5c66;--theme-accent-soft:#dbf2f3;--theme-accent-contrast:#f3fbfb;--theme-accent-glow:#0f758024;--theme-orichalcum:#9e5420;--theme-positive:#2e7a1f;--theme-positive-soft:#e2f3dc;--theme-warning:#8a6400;--theme-warning-soft:#fbeccc;--theme-danger:#b42318;--theme-danger-soft:#fbe3df;--theme-focus:#0f7580;--theme-shadow-raised:0 1px 2px #121a2a14, 0 8px 24px #121a2a14;--theme-shadow-overlay:0 2px 4px #121a2a14, 0 24px 64px #121a2a29;--theme-shadow-glow:0 0 0 1px #0f758066, 0 0 48px #0f758024}body{background:var(--theme-surface);color:var(--theme-text);font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-normal);-webkit-font-smoothing:antialiased;margin:0}:where(a,button,input,select,textarea,[role=tab]):focus-visible{outline:2px solid var(--theme-focus);outline-offset:2px;border-radius:var(--radius-sm)}@media(prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}}*{scrollbar-width:thin;scrollbar-color:var(--theme-border-strong) transparent}::placeholder{color:var(--theme-text-faint)}@keyframes studio-rise{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.studio-rise{animation:studio-rise var(--duration-base) var(--ease-swell) backwards}@keyframes studio-reveal{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.studio-reveal{animation:studio-reveal var(--duration-slow) var(--ease-swell) backwards;animation-delay:calc(min(var(--i,0),7)*80ms)}@keyframes studio-breathe{0%,to{background-color:var(--theme-surface-sunken)}50%{background-color:var(--theme-surface-raised)}}.studio-breathe{animation:2.4s ease-in-out infinite studio-breathe}@keyframes studio-live{0%,to{opacity:.35}50%{opacity:1}}.studio-live{animation:2.4s ease-in-out infinite studio-live}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes pulse{50%{opacity:.5}}