@ogygie/studio 0.26.0 → 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,
@@ -2031,6 +2088,9 @@ ${link}
2031
2088
  });
2032
2089
  }
2033
2090
 
2091
+ // src/server/hosted/brief-routes.ts
2092
+ import { z as z10 } from "zod";
2093
+
2034
2094
  // src/server/hosted/attachments.ts
2035
2095
  import { createHash } from "node:crypto";
2036
2096
  import { inflateRawSync } from "node:zlib";
@@ -2276,6 +2336,9 @@ var MAX_COMPOSITIONS = 3;
2276
2336
  var COMPOSITION_SEPARATOR = "=== COMPOSITION ===";
2277
2337
  var DERIVED_BRIEF_MAX = 6e3;
2278
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";
2279
2342
  async function readIfPresent(storage, path) {
2280
2343
  return await storage.exists(path) ? storage.readFile(path) : null;
2281
2344
  }
@@ -2339,6 +2402,25 @@ async function existingSpecIds(storage) {
2339
2402
  const entries = await storage.listDirectory("specs");
2340
2403
  return entries.map((entry) => entry.path).filter((path) => !path.includes("/") && path.endsWith(".spec.md") && !path.startsWith("_")).map((path) => path.replace(/\.spec\.md$/, "")).sort();
2341
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
+ }
2342
2424
  function buildBriefPrompt(context) {
2343
2425
  const system = [
2344
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.",
@@ -2358,6 +2440,8 @@ function buildBriefPrompt(context) {
2358
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.",
2359
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."],
2360
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.',
2361
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.",
2362
2446
  "",
2363
2447
  "GABARIT DU REPO (\xE0 respecter \xE0 la lettre) :",
@@ -2379,7 +2463,10 @@ function buildBriefPrompt(context) {
2379
2463
  ...context.existingSpec === null ? [] : ["SPEC EXISTANTE :", context.existingSpec, ""],
2380
2464
  "BRIEF :",
2381
2465
  context.brief.trim(),
2382
- ...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`."
2383
2470
  ].join("\n");
2384
2471
  return { system, user, documents: attachmentDocumentsOf(context.attachments ?? []) };
2385
2472
  }
@@ -2471,7 +2558,7 @@ function judgeDraft(input) {
2471
2558
  }
2472
2559
  return { specId, problems: [...new Set(problems)] };
2473
2560
  }
2474
- async function draftSpecFromBrief(deps, input) {
2561
+ async function runBrief(deps, input) {
2475
2562
  if (input.specId !== void 0 && !FeatureNameSchema.safeParse(input.specId).success) {
2476
2563
  throwOgygieError(
2477
2564
  {
@@ -2502,6 +2589,8 @@ async function draftSpecFromBrief(deps, input) {
2502
2589
  const knownDecisionIds = (await Promise.all(DECISION_FILES.map((file) => readIfPresent(deps.storage, file)))).flatMap((content) => parseKnownDecisionIds(content ?? ""));
2503
2590
  const existingSpec = input.specId !== void 0 ? await readIfPresent(deps.storage, `specs/${input.specId}.spec.md`) : null;
2504
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;
2505
2594
  const prompt = buildBriefPrompt({
2506
2595
  template,
2507
2596
  productRules,
@@ -2512,15 +2601,40 @@ async function draftSpecFromBrief(deps, input) {
2512
2601
  existingSpec,
2513
2602
  specId: input.specId,
2514
2603
  brief: input.brief,
2515
- attachments: input.attachments
2604
+ attachments: input.attachments,
2605
+ answers: input.answers,
2606
+ questions: { allowed: questionsAllowed, round }
2516
2607
  });
2517
2608
  const usage = [];
2609
+ let refusedQuestions = false;
2518
2610
  let previous;
2519
2611
  let firstProblems = [];
2520
2612
  let maxAttempts = MAX_ATTEMPTS;
2521
2613
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2614
+ deps.onStage?.({ stage: "drafting", attempt });
2522
2615
  const answer = await deps.writer.write({ ...prompt, previous });
2523
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 });
2524
2638
  const content = extractSpecText(answer.text);
2525
2639
  const judged = judgeDraft({
2526
2640
  content,
@@ -2535,7 +2649,11 @@ async function draftSpecFromBrief(deps, input) {
2535
2649
  deps.log(JSON.stringify({ action: "brief", attempt, specId: judged.specId, problems: judged.problems.length, generated: generationJudgement.tally !== null, ...answer.usage }));
2536
2650
  if (judged.problems.length === 0 && judged.specId !== null) {
2537
2651
  const specId = judged.specId;
2538
- 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")}`);
2539
2657
  const composition = isAmendment ? null : compositionOfSpec(content);
2540
2658
  const compositions = [];
2541
2659
  if (composition !== null) {
@@ -2553,7 +2671,9 @@ async function draftSpecFromBrief(deps, input) {
2553
2671
  deps.log(JSON.stringify({ action: "composition", specId, outcome: thrown instanceof OgygieFailure ? thrown.error.code : "error" }));
2554
2672
  }
2555
2673
  const seen = /* @__PURE__ */ new Set([composition]);
2556
- 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) {
2557
2677
  if (seen.size >= MAX_COMPOSITIONS) break;
2558
2678
  const name = compositionOfSpec(candidate);
2559
2679
  const verdict = judgeDraft({ content: candidate, expectedId: specId, knownDecisionIds, takenIds: specIds, isAmendment: false });
@@ -2561,6 +2681,7 @@ async function draftSpecFromBrief(deps, input) {
2561
2681
  deps.log(JSON.stringify({ action: "composition", specId, composition: name, problems: problems.length }));
2562
2682
  if (problems.length > 0 || name === null) continue;
2563
2683
  seen.add(name);
2684
+ deps.onStage?.({ stage: "compositions", composition: { done: seen.size - 1, total: Math.min(candidates.length, MAX_COMPOSITIONS - 1) } });
2564
2685
  await deps.storage.write(`specs/${specId}.spec.md`, candidate, `spec: composition ${name} pour ${specId}, propos\xE9e par le mod\xE8le
2565
2686
 
2566
2687
  Une autre disposition du m\xEAme \xE9cran ; la composition retenue par d\xE9faut suit dans le commit suivant.
@@ -2571,6 +2692,7 @@ ${briefForMessage}`);
2571
2692
  }
2572
2693
  compositions.unshift(composition);
2573
2694
  }
2695
+ deps.onStage?.({ stage: "writing" });
2574
2696
  await deps.storage.write(
2575
2697
  `specs/${specId}.spec.md`,
2576
2698
  content,
@@ -2580,14 +2702,18 @@ Brief :
2580
2702
  ${briefForMessage}`
2581
2703
  );
2582
2704
  return {
2583
- specId,
2584
- attempts: attempt,
2585
- problemsFixed: firstProblems,
2586
- isNewFeature: !manifest.featureIds.has(specId),
2587
- isAmendment,
2588
- compositions,
2589
- generation: generationJudgement.tally,
2590
- 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
+ }
2591
2717
  };
2592
2718
  }
2593
2719
  if (attempt === 1) firstProblems = judged.problems;
@@ -2606,37 +2732,144 @@ ${briefForMessage}`
2606
2732
  );
2607
2733
  }
2608
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
+
2609
2829
  // src/server/hosted/brief-routes.ts
2610
2830
  var BRIEF_BODY_LIMIT = 16 * 1024 * 1024;
2611
2831
  async function registerBriefRoutes(app, deps) {
2612
2832
  const route = app.withTypeProvider();
2833
+ const now = deps.now ?? Date.now;
2834
+ const jobs = deps.jobs ?? createBriefJobs({ now });
2613
2835
  route.route({
2614
2836
  method: "POST",
2615
2837
  url: "/api/briefs",
2616
2838
  bodyLimit: BRIEF_BODY_LIMIT,
2617
- schema: { body: BriefInputSchema, response: { 201: BriefOutputSchema } },
2839
+ schema: { body: BriefInputSchema, response: { 202: BriefStartOutputSchema } },
2618
2840
  handler: async (request, reply) => {
2619
2841
  const user = hostedUserOf(request);
2620
2842
  requireRole(user, "author", "write a brief");
2621
2843
  const attachments = prepareBriefAttachments(request.body.attachments ?? []);
2622
2844
  const storage = deps.storageFor(user);
2623
- const result = await draftSpecFromBrief(
2624
- { storage, writer: deps.writer, log: deps.log },
2625
- { brief: request.body.brief, specId: request.body.specId, attachments }
2626
- );
2627
- const proposal = "lastProposal" in storage ? storage.lastProposal : null;
2628
- if (deps.usage !== void 0 && proposal !== null) {
2629
- const at = new Date((deps.now ?? Date.now)()).toISOString();
2630
- try {
2631
- await deps.usage.appendUsage(
2632
- proposal.pullRequestNumber,
2633
- 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 }))
2634
- );
2635
- } catch (thrown) {
2636
- 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
+ }
2637
2860
  }
2638
- }
2639
- 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));
2640
2873
  }
2641
2874
  });
2642
2875
  }
@@ -3147,17 +3380,17 @@ async function registerPromotionRoutes(app, deps) {
3147
3380
  }
3148
3381
 
3149
3382
  // src/server/hosted/usage.ts
3150
- import { z as z10 } from "zod";
3151
- var UsageEntrySchema = z10.object({
3152
- at: z10.string(),
3383
+ import { z as z11 } from "zod";
3384
+ var UsageEntrySchema = z11.object({
3385
+ at: z11.string(),
3153
3386
  /** brief → spec from the instance; `speculate` entries arrive with the generator's lot. */
3154
- action: z10.enum(["brief", "speculate"]),
3155
- attempt: z10.number().int().positive().optional(),
3156
- model: z10.string(),
3157
- inputTokens: z10.number().int().nonnegative(),
3158
- outputTokens: z10.number().int().nonnegative(),
3159
- actor: z10.string().optional(),
3160
- 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()
3161
3394
  });
3162
3395
  var USAGE_MARKER = "<!-- ogygie-usage -->";
3163
3396
  var DEFAULT_MODEL_COSTS = {
@@ -3192,7 +3425,7 @@ function parseUsageComment(body) {
3192
3425
  const block = /```json\n([\s\S]*?)\n```/.exec(body)?.[1];
3193
3426
  if (block === void 0) return [];
3194
3427
  try {
3195
- const parsed = z10.array(UsageEntrySchema).safeParse(JSON.parse(block));
3428
+ const parsed = z11.array(UsageEntrySchema).safeParse(JSON.parse(block));
3196
3429
  return parsed.success ? parsed.data : [];
3197
3430
  } catch {
3198
3431
  return [];
@@ -3267,9 +3500,9 @@ async function registerConsumptionRoutes(app, deps) {
3267
3500
  }
3268
3501
 
3269
3502
  // src/server/hosted/preview-routes.ts
3270
- import { z as z11 } from "zod";
3271
- var PreviewParamsSchema = SpecParamsSchema.extend({ state: z11.string().min(1).max(80) });
3272
- 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) });
3273
3506
  var PREVIEW_CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
3274
3507
  async function registerPreviewRoutes(app, deps) {
3275
3508
  const route = app.withTypeProvider();
@@ -3311,7 +3544,7 @@ async function registerPreviewRoutes(app, deps) {
3311
3544
  }
3312
3545
 
3313
3546
  // src/server/hosted/composition-routes.ts
3314
- import { z as z12 } from "zod";
3547
+ import { z as z13 } from "zod";
3315
3548
 
3316
3549
  // src/server/hosted/compositions.ts
3317
3550
  var COMPOSITION_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
@@ -3355,7 +3588,7 @@ La composition \xAB ${composition} \xBB est retenue parmi celles propos\xE9es ;
3355
3588
  }
3356
3589
 
3357
3590
  // src/server/hosted/composition-routes.ts
3358
- var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z12.string().min(1).max(80) });
3591
+ var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z13.string().min(1).max(80) });
3359
3592
  async function registerCompositionRoutes(app, deps) {
3360
3593
  const route = app.withTypeProvider();
3361
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}}