@ogygie/studio 0.25.2 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -826,6 +826,15 @@ function compositionOfSpec(content) {
826
826
  const section = next === null ? rest : rest.slice(0, next.index);
827
827
  return COMPOSITION_LINE.exec(section)?.[1] ?? null;
828
828
  }
829
+ var ASSUMPTIONS_SECTION_TITLE = "Hypoth\xE8ses";
830
+ function assumptionsOfSpec(content) {
831
+ const heading = new RegExp(`^# ${ASSUMPTIONS_SECTION_TITLE}\\s*$`, "mu").exec(content);
832
+ if (heading === null) return [];
833
+ const rest = content.slice(heading.index + heading[0].length);
834
+ const next = /^# /mu.exec(rest);
835
+ const section = next === null ? rest : rest.slice(0, next.index);
836
+ return section.split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+\S/u.test(line)).map((line) => line.replace(/^[-*]\s+/u, "").trim());
837
+ }
829
838
 
830
839
  // ../ogygie-core/src/proposal.ts
831
840
  var DECISION_FILES = ["DECISIONS.md", "DECISIONS.product.md"];
@@ -1326,6 +1335,16 @@ function parseSpec(filePath, fileName, content) {
1326
1335
  }
1327
1336
  return { ok: true, spec: { frontmatter: checked.data, body: split.body } };
1328
1337
  }
1338
+ function intentOf(body) {
1339
+ const heading = /^# @intent\s*$/mu.exec(body);
1340
+ if (heading === null) return "";
1341
+ const rest = body.slice(heading.index + heading[0].length);
1342
+ const next = /^# /mu.exec(rest);
1343
+ const section = (next === null ? rest : rest.slice(0, next.index)).trim();
1344
+ const paragraph = section.split(/\n\s*\n/u)[0]?.replace(/\s+/gu, " ").trim() ?? "";
1345
+ const sentence = /^.*?[.!?](?:\s|$)/u.exec(paragraph)?.[0]?.trim() ?? paragraph;
1346
+ return sentence.length > 160 ? `${sentence.slice(0, 157).trimEnd()}\u2026` : sentence;
1347
+ }
1329
1348
  function toReadableSummary(filePath, spec) {
1330
1349
  return {
1331
1350
  id: spec.frontmatter.id,
@@ -1333,7 +1352,8 @@ function toReadableSummary(filePath, spec) {
1333
1352
  priority: spec.frontmatter.priority,
1334
1353
  depends_on: spec.frontmatter.depends_on,
1335
1354
  filePath,
1336
- presentSections: presentSectionsOf(spec.body)
1355
+ presentSections: presentSectionsOf(spec.body),
1356
+ intent: intentOf(spec.body)
1337
1357
  };
1338
1358
  }
1339
1359
  async function listSpecFileNames(deps) {
@@ -1540,7 +1560,9 @@ var ReadableSpecSummarySchema = z6.object({
1540
1560
  priority: SpecFrontmatterSchema.shape.priority,
1541
1561
  depends_on: z6.array(FeatureNameSchema),
1542
1562
  filePath: z6.string(),
1543
- presentSections: z6.array(z6.string())
1563
+ presentSections: z6.array(z6.string()),
1564
+ /** The first sentence of `# @intent`, for a list that reads (v1.20: the target-spec suggestions of the brief). */
1565
+ intent: z6.string().default("")
1544
1566
  });
1545
1567
  var UnreadableSpecSummarySchema = z6.object({
1546
1568
  id: z6.string(),
@@ -1584,16 +1606,31 @@ var BriefAttachmentSchema = z6.object({
1584
1606
  name: z6.string().min(1).max(200),
1585
1607
  contentBase64: z6.string().min(1).max(57e5)
1586
1608
  });
1609
+ var BriefAnswerSchema = z6.object({ questionId: FeatureNameSchema, answer: z6.string().min(1).max(2e3) });
1587
1610
  var BriefInputSchema = z6.object({
1588
1611
  brief: z6.string().min(50).max(2e4),
1589
1612
  /** Absent: the model names the new spec. Present and existing: an amendment. */
1590
1613
  specId: FeatureNameSchema.optional(),
1591
1614
  /** The client's material — a mock-up, a workbook, a document; five at most, formats md, txt, csv, json, html, xlsx, pdf. */
1592
- attachments: z6.array(BriefAttachmentSchema).max(5).default([])
1615
+ attachments: z6.array(BriefAttachmentSchema).max(5).default([]),
1616
+ /** D57: the answers of the past rounds of questions, three rounds at most. */
1617
+ answers: z6.array(BriefAnswerSchema).max(15).default([]),
1618
+ /** D57: the round this send opens; at 3 the draft is asked without a question. */
1619
+ round: z6.number().int().min(1).max(4).default(1),
1620
+ /** D57: « Passer les questions » — the draft is asked, the unknown becomes an assumption. */
1621
+ skipQuestions: z6.boolean().default(false)
1622
+ });
1623
+ var BriefQuestionSchema = z6.object({
1624
+ /** kebab-case, stable from one read to the next. */
1625
+ id: FeatureNameSchema,
1626
+ text: z6.string().min(1),
1627
+ /** The proposed answers; a free answer is always possible. */
1628
+ options: z6.array(z6.string().min(1)).min(2).max(4)
1593
1629
  });
1594
1630
  var BriefOutputSchema = z6.object({
1595
1631
  specId: FeatureNameSchema,
1596
- attempts: z6.number().int().min(1).max(2),
1632
+ /** 1 when the first draft passed; up to 4 when the model corrected itself and a refused round of questions cost a turn (D57). */
1633
+ attempts: z6.number().int().min(1).max(4),
1597
1634
  problemsFixed: z6.array(z6.string()),
1598
1635
  /** No feature of the manifest carries this id: its first schemas.ts awaits the operator (D52, lot C). */
1599
1636
  isNewFeature: z6.boolean(),
@@ -1602,10 +1639,30 @@ var BriefOutputSchema = z6.object({
1602
1639
  compositions: z6.array(z6.string()).default([]),
1603
1640
  /** The generator's tally on the draft's `# Génération` block (lines bound without a model, lines carried by a hole); null without a block. */
1604
1641
  generation: z6.object({ derivable: z6.number().int().nonnegative(), hole: z6.number().int().nonnegative() }).nullable().default(null),
1642
+ /** D57: the points the model decided that the brief did not say — the `# Hypothèses` lines of the draft. */
1643
+ assumptions: z6.array(z6.string()).default([]),
1605
1644
  usage: z6.array(
1606
1645
  z6.object({ model: z6.string(), inputTokens: z6.number().int().nonnegative(), outputTokens: z6.number().int().nonnegative() })
1607
1646
  )
1608
1647
  });
1648
+ var BriefStartOutputSchema = z6.object({ jobId: z6.string() });
1649
+ var BriefStageSchema = z6.enum(["questions", "drafting", "judging", "compositions", "writing", "done", "failed"]);
1650
+ var BriefJobSchema = z6.object({
1651
+ jobId: z6.string(),
1652
+ stage: BriefStageSchema,
1653
+ /** On questions: the round in progress. */
1654
+ round: z6.number().int().min(1).max(3).optional(),
1655
+ /** On drafting / judging: the attempt. */
1656
+ attempt: z6.number().int().min(1).max(4).optional(),
1657
+ /** On compositions: k of N. */
1658
+ composition: z6.object({ done: z6.number().int().nonnegative(), total: z6.number().int().nonnegative() }).optional(),
1659
+ elapsedMs: z6.number().int().nonnegative(),
1660
+ /** The P95 of B3: 90 000 ms. */
1661
+ budgetMs: z6.number().int().positive(),
1662
+ questions: z6.array(BriefQuestionSchema).min(3).max(5).optional(),
1663
+ result: BriefOutputSchema.optional(),
1664
+ error: OgygieErrorSchema.optional()
1665
+ });
1609
1666
  var ChecksStateSchema = z6.enum(["green", "red", "pending", "none"]);
1610
1667
  var PullRequestStatusSchema = z6.object({
1611
1668
  specId: FeatureNameSchema,
@@ -1951,7 +2008,8 @@ var CallbackQuerySchema = z8.object({ token: z8.string().min(1) });
1951
2008
  var MeOutputSchema = z8.object({
1952
2009
  email: z8.email(),
1953
2010
  displayName: z8.string(),
1954
- role: HostRoleSchema
2011
+ role: HostRoleSchema,
2012
+ host: z8.object({ environments: z8.boolean(), mergePolicy: z8.enum(["client", "operator"]) })
1955
2013
  });
1956
2014
  async function registerHostedRoutes(app, deps) {
1957
2015
  const sessions = {
@@ -2018,7 +2076,7 @@ ${link}
2018
2076
  url: "/api/auth/me",
2019
2077
  schema: { response: { 200: MeOutputSchema } },
2020
2078
  handler: async (request, reply) => {
2021
- return reply.code(200).send(hostedUserOf(request));
2079
+ return reply.code(200).send({ ...hostedUserOf(request), host: { environments: deps.config.environments !== void 0, mergePolicy: deps.config.mergePolicy } });
2022
2080
  }
2023
2081
  });
2024
2082
  route.route({
@@ -2030,6 +2088,9 @@ ${link}
2030
2088
  });
2031
2089
  }
2032
2090
 
2091
+ // src/server/hosted/brief-routes.ts
2092
+ import { z as z10 } from "zod";
2093
+
2033
2094
  // src/server/hosted/attachments.ts
2034
2095
  import { createHash } from "node:crypto";
2035
2096
  import { inflateRawSync } from "node:zlib";
@@ -2275,6 +2336,9 @@ var MAX_COMPOSITIONS = 3;
2275
2336
  var COMPOSITION_SEPARATOR = "=== COMPOSITION ===";
2276
2337
  var DERIVED_BRIEF_MAX = 6e3;
2277
2338
  var BRIEF_IN_MESSAGE_MAX = 2e3;
2339
+ var MAX_QUESTION_ROUNDS = 3;
2340
+ var QUESTIONS_MARKER = /^\s*QUESTIONS\s*$/mu;
2341
+ var NO_MORE_QUESTIONS = "no more questions: write the draft now, and put what you do not know under # Hypoth\xE8ses";
2278
2342
  async function readIfPresent(storage, path) {
2279
2343
  return await storage.exists(path) ? storage.readFile(path) : null;
2280
2344
  }
@@ -2338,6 +2402,25 @@ async function existingSpecIds(storage) {
2338
2402
  const entries = await storage.listDirectory("specs");
2339
2403
  return entries.map((entry) => entry.path).filter((path) => !path.includes("/") && path.endsWith(".spec.md") && !path.startsWith("_")).map((path) => path.replace(/\.spec\.md$/, "")).sort();
2340
2404
  }
2405
+ function extractQuestions(text) {
2406
+ const marker = QUESTIONS_MARKER.exec(text);
2407
+ if (marker === null) return null;
2408
+ const raw = text.slice(marker.index + marker[0].length).replace(/^\s*```(?:json)?\s*/u, "").replace(/\s*```\s*$/u, "").trim();
2409
+ let parsed;
2410
+ try {
2411
+ parsed = JSON.parse(raw);
2412
+ } catch {
2413
+ return { problem: "QUESTIONS: the JSON after the marker does not parse" };
2414
+ }
2415
+ const shape = z9.object({
2416
+ questions: z9.array(z9.object({ id: FeatureNameSchema, text: z9.string().trim().min(1), options: z9.array(z9.string().trim().min(1)).min(2).max(4) })).min(3).max(5)
2417
+ });
2418
+ const checked = shape.safeParse(parsed);
2419
+ if (!checked.success) return { problem: `QUESTIONS: ${checked.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" ; ")}` };
2420
+ const ids = new Set(checked.data.questions.map((question) => question.id));
2421
+ if (ids.size !== checked.data.questions.length) return { problem: "QUESTIONS: two questions share an id" };
2422
+ return { questions: checked.data.questions };
2423
+ }
2341
2424
  function buildBriefPrompt(context) {
2342
2425
  const system = [
2343
2426
  "Tu r\xE9diges des SPECS de features pour un produit construit avec le framework Ogygie. Une spec est un contrat, relu et sign\xE9 par un humain non d\xE9veloppeur, puis impl\xE9ment\xE9 par une cha\xEEne de g\xE9n\xE9ration : tu \xE9cris pour ce lecteur, en fran\xE7ais, pr\xE9cis, sans jargon.",
@@ -2357,6 +2440,8 @@ function buildBriefPrompt(context) {
2357
2440
  "- Tu n'\xE9cris jamais de code ni de design (couleurs, composants) : la spec dit QUOI, jamais COMMENT. Le bloc YAML de `# G\xE9n\xE9ration` n'est pas du code : c'est la d\xE9claration que le g\xE9n\xE9rateur lit.",
2358
2441
  ...context.grammarCard === null || context.grammarCard === void 0 ? [] : ["- Quand la rubrique apporte des op\xE9rations ou un \xE9cran que le g\xE9n\xE9rateur sait d\xE9river, \xE9cris la section `# G\xE9n\xE9ration` apr\xE8s `# @intent` et ses `# Comportements` dans la grammaire ferm\xE9e (carte ci-dessous) : le brouillon est jug\xE9 par le v\xE9rificateur du g\xE9n\xE9rateur avant d'\xEAtre montr\xE9. Quand elle ne se g\xE9n\xE8re pas, n'\xE9cris pas la section."],
2359
2442
  "- Tu ne proposes jamais `status: validated` ni `implemented` : valider est le geste de l'humain.",
2443
+ "- HYPOTH\xC8SES : quand tu tranches un point que le brief ne dit pas (un nombre, un ordre, un p\xE9rim\xE8tre, un mot), ajoute apr\xE8s `# Budget` (et avant `# Design` si elle existe) une section `# Hypoth\xE8ses`, une puce par choix, en fran\xE7ais, courte ; sans hypoth\xE8se, pas de section.",
2444
+ '- QUESTIONS : quand la demande ci-dessous t\'y autorise et qu\'une r\xE9ponse du client CHANGERAIT la spec, tu peux, AU LIEU du fichier, r\xE9pondre par une ligne `QUESTIONS` seule puis un objet JSON `{"questions":[{"id":"kebab-case","text":"la question, en fran\xE7ais","options":["r\xE9ponse","r\xE9ponse"]}]}` \u2014 trois \xE0 cinq questions ferm\xE9es, deux \xE0 quatre options chacune, rien d\'autre avant ni apr\xE8s. Une question dont la r\xE9ponse ne changerait pas la spec ne se pose pas : elle devient une hypoth\xE8se.',
2360
2445
  "- Quand la rubrique est un \xC9CRAN (une page du tableau de bord), ajoute apr\xE8s `# Budget` une section `# Design` qui commence par une ligne `Composition : <nom-kebab>` (le nom de la disposition, ex. `liste-d-abord`), puis l'ordre des vues, le pattern du catalogue de chacune (liste, formulaire, \xE9tat vide\u2026) et les \xE9tats mis en avant \u2014 jamais une couleur, jamais un composant invent\xE9.",
2361
2446
  "",
2362
2447
  "GABARIT DU REPO (\xE0 respecter \xE0 la lettre) :",
@@ -2378,7 +2463,10 @@ function buildBriefPrompt(context) {
2378
2463
  ...context.existingSpec === null ? [] : ["SPEC EXISTANTE :", context.existingSpec, ""],
2379
2464
  "BRIEF :",
2380
2465
  context.brief.trim(),
2381
- ...attachmentsPromptSection(context.attachments ?? [])
2466
+ ...attachmentsPromptSection(context.attachments ?? []),
2467
+ ...(context.answers ?? []).length === 0 ? [] : ["", "R\xC9PONSES DU CLIENT \xC0 TES QUESTIONS :", ...(context.answers ?? []).map((answer) => `- ${answer.questionId} : ${answer.answer}`)],
2468
+ "",
2469
+ context.questions?.allowed === true ? `Tourn\xE9e ${context.questions.round} sur ${MAX_QUESTION_ROUNDS} : si une r\xE9ponse du client changerait la spec, pose tes questions (format QUESTIONS) ; sinon, r\xE9dige le fichier.` : "N'interroge plus : r\xE9dige le fichier maintenant, et consigne ce que tu ne sais pas sous `# Hypoth\xE8ses`."
2382
2470
  ].join("\n");
2383
2471
  return { system, user, documents: attachmentDocumentsOf(context.attachments ?? []) };
2384
2472
  }
@@ -2470,7 +2558,7 @@ function judgeDraft(input) {
2470
2558
  }
2471
2559
  return { specId, problems: [...new Set(problems)] };
2472
2560
  }
2473
- async function draftSpecFromBrief(deps, input) {
2561
+ async function runBrief(deps, input) {
2474
2562
  if (input.specId !== void 0 && !FeatureNameSchema.safeParse(input.specId).success) {
2475
2563
  throwOgygieError(
2476
2564
  {
@@ -2501,6 +2589,8 @@ async function draftSpecFromBrief(deps, input) {
2501
2589
  const knownDecisionIds = (await Promise.all(DECISION_FILES.map((file) => readIfPresent(deps.storage, file)))).flatMap((content) => parseKnownDecisionIds(content ?? ""));
2502
2590
  const existingSpec = input.specId !== void 0 ? await readIfPresent(deps.storage, `specs/${input.specId}.spec.md`) : null;
2503
2591
  const isAmendment = existingSpec !== null;
2592
+ const round = Math.min(Math.max(input.round ?? 1, 1), MAX_QUESTION_ROUNDS + 1);
2593
+ const questionsAllowed = input.skipQuestions !== true && round <= MAX_QUESTION_ROUNDS;
2504
2594
  const prompt = buildBriefPrompt({
2505
2595
  template,
2506
2596
  productRules,
@@ -2511,15 +2601,40 @@ async function draftSpecFromBrief(deps, input) {
2511
2601
  existingSpec,
2512
2602
  specId: input.specId,
2513
2603
  brief: input.brief,
2514
- attachments: input.attachments
2604
+ attachments: input.attachments,
2605
+ answers: input.answers,
2606
+ questions: { allowed: questionsAllowed, round }
2515
2607
  });
2516
2608
  const usage = [];
2609
+ let refusedQuestions = false;
2517
2610
  let previous;
2518
2611
  let firstProblems = [];
2519
2612
  let maxAttempts = MAX_ATTEMPTS;
2520
2613
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2614
+ deps.onStage?.({ stage: "drafting", attempt });
2521
2615
  const answer = await deps.writer.write({ ...prompt, previous });
2522
2616
  usage.push({ model: answer.model, ...answer.usage });
2617
+ const asked = extractQuestions(answer.text);
2618
+ if (asked !== null) {
2619
+ if ("problem" in asked) {
2620
+ deps.log(JSON.stringify({ action: "brief", attempt, questions: "malformed", ...answer.usage }));
2621
+ if (attempt === 1) firstProblems = [asked.problem];
2622
+ previous = { draft: answer.text, problems: [asked.problem] };
2623
+ continue;
2624
+ }
2625
+ if (questionsAllowed) {
2626
+ deps.log(JSON.stringify({ action: "brief", attempt, round, questions: asked.questions.length, ...answer.usage }));
2627
+ return { kind: "questions", round, questions: asked.questions, usage };
2628
+ }
2629
+ deps.log(JSON.stringify({ action: "brief", attempt, round, questions: "refused", ...answer.usage }));
2630
+ if (!refusedQuestions) {
2631
+ refusedQuestions = true;
2632
+ maxAttempts += 1;
2633
+ }
2634
+ previous = { draft: answer.text, problems: [NO_MORE_QUESTIONS] };
2635
+ continue;
2636
+ }
2637
+ deps.onStage?.({ stage: "judging", attempt });
2523
2638
  const content = extractSpecText(answer.text);
2524
2639
  const judged = judgeDraft({
2525
2640
  content,
@@ -2534,7 +2649,11 @@ async function draftSpecFromBrief(deps, input) {
2534
2649
  deps.log(JSON.stringify({ action: "brief", attempt, specId: judged.specId, problems: judged.problems.length, generated: generationJudgement.tally !== null, ...answer.usage }));
2535
2650
  if (judged.problems.length === 0 && judged.specId !== null) {
2536
2651
  const specId = judged.specId;
2537
- const briefForMessage = (input.brief.length > BRIEF_IN_MESSAGE_MAX ? `${input.brief.slice(0, BRIEF_IN_MESSAGE_MAX)} (\u2026)` : input.brief) + attachmentsProvenance(input.attachments ?? []);
2652
+ const assumptions = assumptionsOfSpec(content);
2653
+ const briefForMessage = (input.brief.length > BRIEF_IN_MESSAGE_MAX ? `${input.brief.slice(0, BRIEF_IN_MESSAGE_MAX)} (\u2026)` : input.brief) + attachmentsProvenance(input.attachments ?? []) + (assumptions.length === 0 ? "" : `
2654
+
2655
+ Hypoth\xE8ses du mod\xE8le (D57) :
2656
+ ${assumptions.map((line) => `- ${line}`).join("\n")}`);
2538
2657
  const composition = isAmendment ? null : compositionOfSpec(content);
2539
2658
  const compositions = [];
2540
2659
  if (composition !== null) {
@@ -2552,7 +2671,9 @@ async function draftSpecFromBrief(deps, input) {
2552
2671
  deps.log(JSON.stringify({ action: "composition", specId, outcome: thrown instanceof OgygieFailure ? thrown.error.code : "error" }));
2553
2672
  }
2554
2673
  const seen = /* @__PURE__ */ new Set([composition]);
2555
- for (const candidate of splitCompositions(answerText)) {
2674
+ const candidates = splitCompositions(answerText);
2675
+ deps.onStage?.({ stage: "compositions", composition: { done: 0, total: Math.min(candidates.length, MAX_COMPOSITIONS - 1) } });
2676
+ for (const candidate of candidates) {
2556
2677
  if (seen.size >= MAX_COMPOSITIONS) break;
2557
2678
  const name = compositionOfSpec(candidate);
2558
2679
  const verdict = judgeDraft({ content: candidate, expectedId: specId, knownDecisionIds, takenIds: specIds, isAmendment: false });
@@ -2560,6 +2681,7 @@ async function draftSpecFromBrief(deps, input) {
2560
2681
  deps.log(JSON.stringify({ action: "composition", specId, composition: name, problems: problems.length }));
2561
2682
  if (problems.length > 0 || name === null) continue;
2562
2683
  seen.add(name);
2684
+ deps.onStage?.({ stage: "compositions", composition: { done: seen.size - 1, total: Math.min(candidates.length, MAX_COMPOSITIONS - 1) } });
2563
2685
  await deps.storage.write(`specs/${specId}.spec.md`, candidate, `spec: composition ${name} pour ${specId}, propos\xE9e par le mod\xE8le
2564
2686
 
2565
2687
  Une autre disposition du m\xEAme \xE9cran ; la composition retenue par d\xE9faut suit dans le commit suivant.
@@ -2570,6 +2692,7 @@ ${briefForMessage}`);
2570
2692
  }
2571
2693
  compositions.unshift(composition);
2572
2694
  }
2695
+ deps.onStage?.({ stage: "writing" });
2573
2696
  await deps.storage.write(
2574
2697
  `specs/${specId}.spec.md`,
2575
2698
  content,
@@ -2579,14 +2702,18 @@ Brief :
2579
2702
  ${briefForMessage}`
2580
2703
  );
2581
2704
  return {
2582
- specId,
2583
- attempts: attempt,
2584
- problemsFixed: firstProblems,
2585
- isNewFeature: !manifest.featureIds.has(specId),
2586
- isAmendment,
2587
- compositions,
2588
- generation: generationJudgement.tally,
2589
- usage
2705
+ kind: "draft",
2706
+ result: {
2707
+ specId,
2708
+ attempts: attempt,
2709
+ problemsFixed: firstProblems,
2710
+ isNewFeature: !manifest.featureIds.has(specId),
2711
+ isAmendment,
2712
+ compositions,
2713
+ generation: generationJudgement.tally,
2714
+ assumptions,
2715
+ usage
2716
+ }
2590
2717
  };
2591
2718
  }
2592
2719
  if (attempt === 1) firstProblems = judged.problems;
@@ -2605,37 +2732,144 @@ ${briefForMessage}`
2605
2732
  );
2606
2733
  }
2607
2734
 
2735
+ // src/server/hosted/brief-jobs.ts
2736
+ import { randomUUID } from "node:crypto";
2737
+ var BRIEF_BUDGET_MS = 9e4;
2738
+ var BRIEF_JOB_TTL_MS = 15 * 6e4;
2739
+ function createBriefJobs(deps) {
2740
+ const budgetMs = deps.budgetMs ?? BRIEF_BUDGET_MS;
2741
+ const ttlMs = deps.ttlMs ?? BRIEF_JOB_TTL_MS;
2742
+ const idOf = deps.idOf ?? randomUUID;
2743
+ const jobs = /* @__PURE__ */ new Map();
2744
+ const settlements = /* @__PURE__ */ new Map();
2745
+ const sweep = () => {
2746
+ const now = deps.now();
2747
+ for (const [jobId, job] of jobs) {
2748
+ if (job.endedAt !== null && job.endedAt + ttlMs <= now) {
2749
+ jobs.delete(jobId);
2750
+ settlements.delete(jobId);
2751
+ }
2752
+ }
2753
+ };
2754
+ const errorOf = (thrown) => thrown instanceof OgygieFailure ? thrown.error : ogygieError({
2755
+ code: "BRIEF_FAILED",
2756
+ message: "The brief failed before a draft or a round of questions came out.",
2757
+ file: null,
2758
+ cause: thrown instanceof Error ? thrown.message : String(thrown),
2759
+ suggestedFix: "Send the brief again; if it repeats, read the instance log."
2760
+ });
2761
+ return {
2762
+ start(owner, run) {
2763
+ sweep();
2764
+ const running = [...jobs.values()].find((job2) => job2.owner === owner && job2.endedAt === null);
2765
+ if (running !== void 0) {
2766
+ throwOgygieError(
2767
+ {
2768
+ code: "BRIEF_IN_PROGRESS",
2769
+ message: "A brief of yours is still running: one at a time.",
2770
+ file: null,
2771
+ cause: `job ${running.jobId} started ${deps.now() - running.startedAt} ms ago`,
2772
+ suggestedFix: "Wait for it to end (GET /api/briefs/<id>), then send the next brief.",
2773
+ details: { jobId: running.jobId }
2774
+ },
2775
+ 409
2776
+ );
2777
+ }
2778
+ const job = { jobId: idOf(), owner, startedAt: deps.now(), endedAt: null, progress: null, outcome: null, error: null };
2779
+ jobs.set(job.jobId, job);
2780
+ const settlement = run((progress) => {
2781
+ job.progress = progress;
2782
+ }).then((outcome) => {
2783
+ job.outcome = outcome;
2784
+ }).catch((thrown) => {
2785
+ job.error = errorOf(thrown);
2786
+ }).finally(() => {
2787
+ job.endedAt = deps.now();
2788
+ });
2789
+ settlements.set(job.jobId, settlement);
2790
+ return job.jobId;
2791
+ },
2792
+ read(jobId) {
2793
+ sweep();
2794
+ const job = jobs.get(jobId);
2795
+ if (job === void 0) {
2796
+ return throwOgygieError(
2797
+ {
2798
+ code: "BRIEF_JOB_LOST",
2799
+ message: "This brief job is unknown here: it expired, or the instance restarted since.",
2800
+ file: null,
2801
+ cause: `no job ${jobId} in memory`,
2802
+ suggestedFix: "Send the brief again; its text is still in your editor."
2803
+ },
2804
+ 404
2805
+ );
2806
+ }
2807
+ const base = { jobId: job.jobId, elapsedMs: Math.max(0, (job.endedAt ?? deps.now()) - job.startedAt), budgetMs };
2808
+ if (job.error !== null) return { ...base, stage: "failed", error: job.error };
2809
+ if (job.outcome !== null) {
2810
+ return job.outcome.kind === "draft" ? { ...base, stage: "done", result: job.outcome.result } : { ...base, stage: "questions", round: job.outcome.round, questions: job.outcome.questions };
2811
+ }
2812
+ const progress = job.progress ?? { stage: "drafting", attempt: 1 };
2813
+ switch (progress.stage) {
2814
+ case "drafting":
2815
+ case "judging":
2816
+ return { ...base, stage: progress.stage, attempt: progress.attempt };
2817
+ case "compositions":
2818
+ return { ...base, stage: "compositions", composition: progress.composition };
2819
+ case "writing":
2820
+ return { ...base, stage: "writing" };
2821
+ }
2822
+ },
2823
+ async settled(jobId) {
2824
+ await (settlements.get(jobId) ?? Promise.resolve());
2825
+ }
2826
+ };
2827
+ }
2828
+
2608
2829
  // src/server/hosted/brief-routes.ts
2609
2830
  var BRIEF_BODY_LIMIT = 16 * 1024 * 1024;
2610
2831
  async function registerBriefRoutes(app, deps) {
2611
2832
  const route = app.withTypeProvider();
2833
+ const now = deps.now ?? Date.now;
2834
+ const jobs = deps.jobs ?? createBriefJobs({ now });
2612
2835
  route.route({
2613
2836
  method: "POST",
2614
2837
  url: "/api/briefs",
2615
2838
  bodyLimit: BRIEF_BODY_LIMIT,
2616
- schema: { body: BriefInputSchema, response: { 201: BriefOutputSchema } },
2839
+ schema: { body: BriefInputSchema, response: { 202: BriefStartOutputSchema } },
2617
2840
  handler: async (request, reply) => {
2618
2841
  const user = hostedUserOf(request);
2619
2842
  requireRole(user, "author", "write a brief");
2620
2843
  const attachments = prepareBriefAttachments(request.body.attachments ?? []);
2621
2844
  const storage = deps.storageFor(user);
2622
- const result = await draftSpecFromBrief(
2623
- { storage, writer: deps.writer, log: deps.log },
2624
- { brief: request.body.brief, specId: request.body.specId, attachments }
2625
- );
2626
- const proposal = "lastProposal" in storage ? storage.lastProposal : null;
2627
- if (deps.usage !== void 0 && proposal !== null) {
2628
- const at = new Date((deps.now ?? Date.now)()).toISOString();
2629
- try {
2630
- await deps.usage.appendUsage(
2631
- proposal.pullRequestNumber,
2632
- result.usage.map((usage, index) => ({ at, action: "brief", attempt: index + 1, model: usage.model, inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, actor: user.email, specId: result.specId }))
2633
- );
2634
- } catch (thrown) {
2635
- deps.log(JSON.stringify({ action: "usage-journal", specId: result.specId, outcome: thrown instanceof Error ? thrown.message : String(thrown) }));
2845
+ const { brief, specId, answers, round, skipQuestions } = request.body;
2846
+ const jobId = jobs.start(user.email, async (onStage) => {
2847
+ const outcome = await runBrief({ storage, writer: deps.writer, log: deps.log, onStage }, { brief, specId, attachments, answers, round, skipQuestions });
2848
+ if (outcome.kind !== "draft") return outcome;
2849
+ const proposal = "lastProposal" in storage ? storage.lastProposal : null;
2850
+ if (deps.usage !== void 0 && proposal !== null) {
2851
+ const at = new Date(now()).toISOString();
2852
+ try {
2853
+ await deps.usage.appendUsage(
2854
+ proposal.pullRequestNumber,
2855
+ outcome.result.usage.map((usage, index) => ({ at, action: "brief", attempt: index + 1, model: usage.model, inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, actor: user.email }))
2856
+ );
2857
+ } catch (thrown) {
2858
+ deps.log(JSON.stringify({ action: "usage-journal", specId: outcome.result.specId, outcome: thrown instanceof Error ? thrown.message : String(thrown) }));
2859
+ }
2636
2860
  }
2637
- }
2638
- return reply.code(201).send(result);
2861
+ return outcome;
2862
+ });
2863
+ return reply.code(202).send({ jobId });
2864
+ }
2865
+ });
2866
+ route.route({
2867
+ method: "GET",
2868
+ url: "/api/briefs/:jobId",
2869
+ schema: { params: z10.object({ jobId: z10.string().min(1) }), response: { 200: BriefJobSchema } },
2870
+ handler: async (request, reply) => {
2871
+ hostedUserOf(request);
2872
+ return reply.code(200).send(jobs.read(request.params.jobId));
2639
2873
  }
2640
2874
  });
2641
2875
  }
@@ -3146,17 +3380,17 @@ async function registerPromotionRoutes(app, deps) {
3146
3380
  }
3147
3381
 
3148
3382
  // src/server/hosted/usage.ts
3149
- import { z as z10 } from "zod";
3150
- var UsageEntrySchema = z10.object({
3151
- at: z10.string(),
3383
+ import { z as z11 } from "zod";
3384
+ var UsageEntrySchema = z11.object({
3385
+ at: z11.string(),
3152
3386
  /** brief → spec from the instance; `speculate` entries arrive with the generator's lot. */
3153
- action: z10.enum(["brief", "speculate"]),
3154
- attempt: z10.number().int().positive().optional(),
3155
- model: z10.string(),
3156
- inputTokens: z10.number().int().nonnegative(),
3157
- outputTokens: z10.number().int().nonnegative(),
3158
- actor: z10.string().optional(),
3159
- specId: z10.string().optional()
3387
+ action: z11.enum(["brief", "speculate"]),
3388
+ attempt: z11.number().int().positive().optional(),
3389
+ model: z11.string(),
3390
+ inputTokens: z11.number().int().nonnegative(),
3391
+ outputTokens: z11.number().int().nonnegative(),
3392
+ actor: z11.string().optional(),
3393
+ specId: z11.string().optional()
3160
3394
  });
3161
3395
  var USAGE_MARKER = "<!-- ogygie-usage -->";
3162
3396
  var DEFAULT_MODEL_COSTS = {
@@ -3191,7 +3425,7 @@ function parseUsageComment(body) {
3191
3425
  const block = /```json\n([\s\S]*?)\n```/.exec(body)?.[1];
3192
3426
  if (block === void 0) return [];
3193
3427
  try {
3194
- const parsed = z10.array(UsageEntrySchema).safeParse(JSON.parse(block));
3428
+ const parsed = z11.array(UsageEntrySchema).safeParse(JSON.parse(block));
3195
3429
  return parsed.success ? parsed.data : [];
3196
3430
  } catch {
3197
3431
  return [];
@@ -3266,9 +3500,9 @@ async function registerConsumptionRoutes(app, deps) {
3266
3500
  }
3267
3501
 
3268
3502
  // src/server/hosted/preview-routes.ts
3269
- import { z as z11 } from "zod";
3270
- var PreviewParamsSchema = SpecParamsSchema.extend({ state: z11.string().min(1).max(80) });
3271
- var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z11.string().min(1).max(80) });
3503
+ import { z as z12 } from "zod";
3504
+ var PreviewParamsSchema = SpecParamsSchema.extend({ state: z12.string().min(1).max(80) });
3505
+ var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z12.string().min(1).max(80) });
3272
3506
  var PREVIEW_CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
3273
3507
  async function registerPreviewRoutes(app, deps) {
3274
3508
  const route = app.withTypeProvider();
@@ -3310,7 +3544,7 @@ async function registerPreviewRoutes(app, deps) {
3310
3544
  }
3311
3545
 
3312
3546
  // src/server/hosted/composition-routes.ts
3313
- import { z as z12 } from "zod";
3547
+ import { z as z13 } from "zod";
3314
3548
 
3315
3549
  // src/server/hosted/compositions.ts
3316
3550
  var COMPOSITION_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
@@ -3354,7 +3588,7 @@ La composition \xAB ${composition} \xBB est retenue parmi celles propos\xE9es ;
3354
3588
  }
3355
3589
 
3356
3590
  // src/server/hosted/composition-routes.ts
3357
- var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z12.string().min(1).max(80) });
3591
+ var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z13.string().min(1).max(80) });
3358
3592
  async function registerCompositionRoutes(app, deps) {
3359
3593
  const route = app.withTypeProvider();
3360
3594
  route.route({
@@ -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-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-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-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-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-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))}.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)))}: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}.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-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-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\/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-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,.bg-danger-soft\/40{background-color:var(--theme-danger-soft)}@supports (color:color-mix(in lab,red,red)){.bg-danger-soft\/40{background-color:color-mix(in oklab,var(--theme-danger-soft) 40%,transparent)}}.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-warning{background-color:var(--theme-warning)}.bg-warning-soft{background-color:var(--theme-warning-soft)}.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)}.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-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)}.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}.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-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-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-\[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)}.first\:mt-0:first-child{margin-top:0}@media(hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);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)}}.focus\:border-accent:focus{border-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-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}}