@leonarto/spec-embryo 0.1.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.
Files changed (62) hide show
  1. package/README.md +156 -0
  2. package/package.json +48 -0
  3. package/src/backends/base.ts +18 -0
  4. package/src/backends/deterministic.ts +105 -0
  5. package/src/backends/index.ts +26 -0
  6. package/src/backends/prompt.ts +169 -0
  7. package/src/backends/subprocess.ts +198 -0
  8. package/src/cli.ts +111 -0
  9. package/src/commands/agents.ts +16 -0
  10. package/src/commands/current.ts +95 -0
  11. package/src/commands/doctor.ts +12 -0
  12. package/src/commands/handoff.ts +64 -0
  13. package/src/commands/init.ts +101 -0
  14. package/src/commands/reshape.ts +20 -0
  15. package/src/commands/resume.ts +19 -0
  16. package/src/commands/spec.ts +108 -0
  17. package/src/commands/status.ts +98 -0
  18. package/src/commands/task.ts +190 -0
  19. package/src/commands/ui.ts +35 -0
  20. package/src/domain.ts +357 -0
  21. package/src/engine.ts +290 -0
  22. package/src/frontmatter.ts +83 -0
  23. package/src/index.ts +75 -0
  24. package/src/paths.ts +32 -0
  25. package/src/repository.ts +807 -0
  26. package/src/services/adoption.ts +169 -0
  27. package/src/services/agents.ts +191 -0
  28. package/src/services/dashboard.ts +776 -0
  29. package/src/services/details.ts +453 -0
  30. package/src/services/doctor.ts +452 -0
  31. package/src/services/layout.ts +420 -0
  32. package/src/services/spec-answer-evaluation.ts +103 -0
  33. package/src/services/spec-import.ts +217 -0
  34. package/src/services/spec-questions.ts +343 -0
  35. package/src/services/ui.ts +34 -0
  36. package/src/storage.ts +57 -0
  37. package/src/templates.ts +270 -0
  38. package/tsconfig.json +17 -0
  39. package/web/package.json +24 -0
  40. package/web/src/app.css +83 -0
  41. package/web/src/app.d.ts +6 -0
  42. package/web/src/app.html +11 -0
  43. package/web/src/lib/components/AnalysisFilters.svelte +293 -0
  44. package/web/src/lib/components/DocumentBody.svelte +100 -0
  45. package/web/src/lib/components/MultiSelectDropdown.svelte +280 -0
  46. package/web/src/lib/components/SelectDropdown.svelte +265 -0
  47. package/web/src/lib/server/project-root.ts +34 -0
  48. package/web/src/lib/task-board.ts +20 -0
  49. package/web/src/routes/+layout.server.ts +57 -0
  50. package/web/src/routes/+layout.svelte +421 -0
  51. package/web/src/routes/+layout.ts +1 -0
  52. package/web/src/routes/+page.svelte +530 -0
  53. package/web/src/routes/specs/+page.svelte +416 -0
  54. package/web/src/routes/specs/[specId]/+page.server.ts +81 -0
  55. package/web/src/routes/specs/[specId]/+page.svelte +675 -0
  56. package/web/src/routes/tasks/+page.svelte +341 -0
  57. package/web/src/routes/tasks/[taskId]/+page.server.ts +12 -0
  58. package/web/src/routes/tasks/[taskId]/+page.svelte +431 -0
  59. package/web/src/routes/timeline/+page.svelte +1093 -0
  60. package/web/svelte.config.js +10 -0
  61. package/web/tsconfig.json +9 -0
  62. package/web/vite.config.ts +11 -0
@@ -0,0 +1,270 @@
1
+ import { basename } from "node:path";
2
+ import { serializeTomlFrontmatter } from "./frontmatter.ts";
3
+ import { buildManagedPaths } from "./services/layout.ts";
4
+
5
+ export function createConfigToml(projectName: string, memoryDir = "docs/spm"): string {
6
+ const managed = buildManagedPaths(memoryDir);
7
+ return `
8
+ tool_version = "0.1.0"
9
+ project_name = ${JSON.stringify(projectName)}
10
+ memory_dir = ${JSON.stringify(managed.memoryDir)}
11
+ state_file = ${JSON.stringify(managed.stateFile)}
12
+ handoffs_dir = ${JSON.stringify(managed.handoffsDir)}
13
+ specs_dir = ${JSON.stringify(managed.specsDir)}
14
+ tasks_dir = ${JSON.stringify(managed.tasksDir)}
15
+ skills_dir = ${JSON.stringify(managed.skillsDir)}
16
+
17
+ [defaults]
18
+ resume_backend = "deterministic"
19
+
20
+ [backends.deterministic]
21
+ enabled = true
22
+
23
+ [backends.prompt]
24
+ enabled = true
25
+
26
+ [backends.codex]
27
+ enabled = false
28
+ command = "codex"
29
+ args = ["exec", "{prompt_text_file}"]
30
+
31
+ [backends.claude]
32
+ enabled = false
33
+ command = "claude"
34
+ args = ["-p", "{prompt}"]
35
+ `.trimStart();
36
+ }
37
+
38
+ export function createCurrentStateTemplate(updatedAt: string): string {
39
+ return serializeTomlFrontmatter(
40
+ {
41
+ doc_kind: "current-state",
42
+ id: "CURRENT",
43
+ title: "Current Project State",
44
+ summary: "Building the first usable local-first Spec Embryo foundation.",
45
+ focus: "Ship the first usable Spec Embryo workflow around init, status, resume, handoff, spec list, and task list.",
46
+ active_spec_ids: ["SPEC-001"],
47
+ active_task_ids: ["TASK-001", "TASK-002"],
48
+ blocked_task_ids: [],
49
+ next_action_hints: [
50
+ "Verify init/status/resume flows against the seed repo files.",
51
+ "Keep the file-backed storage layer stable before adding server mode.",
52
+ ],
53
+ updated_at: updatedAt,
54
+ tags: ["v1", "seed"],
55
+ },
56
+ `# Current State
57
+
58
+ This file is the explicit checkpoint for interrupted work. It should answer:
59
+
60
+ - what the project is trying to do right now
61
+ - which specs and tasks are active
62
+ - what should happen next
63
+ - what is blocked
64
+ - where a future human or agent should resume
65
+
66
+ ## Narrative
67
+
68
+ The immediate goal is to establish a strong deterministic core. The repo itself is the durable memory system, so the first slice prioritizes file formats, linking, validation, and resume ergonomics before any local server or remote backend exists.
69
+
70
+ ## Notes
71
+
72
+ - Prompt-return mode is first-class to avoid unnecessary nested agent spawning.
73
+ - Subprocess agent backends are adapters, not the center of the product.
74
+ `,
75
+ );
76
+ }
77
+
78
+ export function createSeedSpecTemplate(updatedAt: string): string {
79
+ return serializeTomlFrontmatter(
80
+ {
81
+ doc_kind: "spec",
82
+ id: "SPEC-001",
83
+ title: "Establish the deterministic local-first core",
84
+ status: "active",
85
+ summary: "Define the file-backed, spec-first architecture and ship the first usable CLI workflow.",
86
+ task_ids: ["TASK-001", "TASK-002", "TASK-003"],
87
+ tags: ["v1", "foundation", "cli"],
88
+ owner: "human-ai",
89
+ updated_at: updatedAt,
90
+ },
91
+ `# Problem
92
+
93
+ AI-assisted software work loses continuity when the current state only lives in chat history or an external task tracker.
94
+
95
+ # Goals
96
+
97
+ - Keep repo memory local and readable.
98
+ - Make specs central and tasks explicitly linked.
99
+ - Support deterministic status and resume flows without any LLM.
100
+ - Leave room for prompt-return and subprocess backends later.
101
+
102
+ # Non-Goals For V1
103
+
104
+ - No web UI yet.
105
+ - No database dependency.
106
+ - No mandatory hosted service.
107
+ `,
108
+ );
109
+ }
110
+
111
+ export function createSeedTaskTemplates(updatedAt: string): Array<{ name: string; content: string }> {
112
+ return [
113
+ {
114
+ name: "TASK-001-bootstrap-cli.md",
115
+ content: serializeTomlFrontmatter(
116
+ {
117
+ doc_kind: "task",
118
+ id: "TASK-001",
119
+ title: "Implement the Bun CLI and file-backed repository layer",
120
+ status: "in_progress",
121
+ summary: "Create the runnable CLI entrypoint, document parser, storage helpers, and core commands.",
122
+ spec_ids: ["SPEC-001"],
123
+ depends_on: [],
124
+ blocked_by: [],
125
+ priority: 1,
126
+ owner_role: "builder",
127
+ created_at: updatedAt,
128
+ tags: ["v1", "cli"],
129
+ updated_at: updatedAt,
130
+ },
131
+ `# Scope
132
+
133
+ Ship the initial commands and the document model so the project is already useful with local files alone.
134
+
135
+ # Done Looks Like
136
+
137
+ - \`init\`, \`status\`, \`resume\`, \`handoff\`, \`spec list\`, and \`task list\` exist
138
+ - repo memory files are created and validated deterministically
139
+ - the CLI can help a human or agent resume after interruption
140
+ `,
141
+ ),
142
+ },
143
+ {
144
+ name: "TASK-002-deterministic-resume.md",
145
+ content: serializeTomlFrontmatter(
146
+ {
147
+ doc_kind: "task",
148
+ id: "TASK-002",
149
+ title: "Derive deterministic status, resume context, and next actions",
150
+ status: "todo",
151
+ summary: "Build the engine that links specs, tasks, current state, git data, blockers, and critical path.",
152
+ spec_ids: ["SPEC-001"],
153
+ depends_on: ["TASK-001"],
154
+ blocked_by: [],
155
+ priority: 1,
156
+ owner_role: "planner",
157
+ created_at: updatedAt,
158
+ tags: ["v1", "engine"],
159
+ updated_at: updatedAt,
160
+ },
161
+ `# Scope
162
+
163
+ Use explicit rules to compute what matters now, what is blocked, and what should happen next.
164
+
165
+ # Notes
166
+
167
+ Critical path can be simple in v1: longest unfinished dependency chain by declared task links.
168
+ `,
169
+ ),
170
+ },
171
+ {
172
+ name: "TASK-003-prompt-return-mode.md",
173
+ content: serializeTomlFrontmatter(
174
+ {
175
+ doc_kind: "task",
176
+ id: "TASK-003",
177
+ title: "Add prompt-return mode and subprocess backend adapters",
178
+ status: "todo",
179
+ summary: "Return structured context and prompts when an AI is already calling the tool, with future-ready subprocess adapters.",
180
+ spec_ids: ["SPEC-001"],
181
+ depends_on: ["TASK-002"],
182
+ blocked_by: [],
183
+ priority: 2,
184
+ owner_role: "orchestrator",
185
+ created_at: updatedAt,
186
+ tags: ["v1", "backends"],
187
+ updated_at: updatedAt,
188
+ },
189
+ `# Scope
190
+
191
+ Prompt-return should be first-class. Spawning another AI should be opt-in rather than assumed.
192
+
193
+ # Notes
194
+
195
+ Adapters should keep backend-specific behavior behind a shared interface.
196
+ `,
197
+ ),
198
+ },
199
+ ];
200
+ }
201
+
202
+ export function createSkillsReadme(): string {
203
+ return `# Managed Skills
204
+
205
+ This folder is part of the managed Spec Embryo memory surface for the project.
206
+
207
+ Possible future contents:
208
+
209
+ - role definitions for planner / builder / reviewer agents
210
+ - prompt snippets or subtask recipes
211
+ - deterministic capability manifests
212
+ - skill docs another agent can inspect before continuing work
213
+ `;
214
+ }
215
+
216
+ export function createTemplateFiles(): Array<{ name: string; content: string }> {
217
+ const placeholderTimestamp = "2026-04-02T00:00:00Z";
218
+ return [
219
+ {
220
+ name: "spec-template.md",
221
+ content: createSeedSpecTemplate(placeholderTimestamp),
222
+ },
223
+ {
224
+ name: "task-template.md",
225
+ content: createSeedTaskTemplates(placeholderTimestamp)[0]!.content,
226
+ },
227
+ {
228
+ name: "handoff-template.md",
229
+ content: serializeTomlFrontmatter(
230
+ {
231
+ doc_kind: "handoff",
232
+ id: "HANDOFF-PLACEHOLDER",
233
+ title: "Checkpoint title",
234
+ summary: "Short handoff summary.",
235
+ handoff_kind: "checkpoint",
236
+ created_at: placeholderTimestamp,
237
+ active_task_ids: ["TASK-001"],
238
+ related_spec_ids: ["SPEC-001"],
239
+ author: "spec-embryo",
240
+ updated_at: placeholderTimestamp,
241
+ tags: ["handoff"],
242
+ },
243
+ `# Summary
244
+
245
+ Describe where the project was left.
246
+
247
+ ## What Changed
248
+
249
+ - Itemize meaningful changes.
250
+
251
+ ## Next Steps
252
+
253
+ - List the clearest next moves.
254
+
255
+ ## Blockers
256
+
257
+ - Note anything that needs a decision or unblocking.
258
+ `,
259
+ ),
260
+ },
261
+ {
262
+ name: "current-state-template.md",
263
+ content: createCurrentStateTemplate(placeholderTimestamp),
264
+ },
265
+ ];
266
+ }
267
+
268
+ export function inferProjectName(rootDir: string): string {
269
+ return basename(rootDir);
270
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022"],
6
+ "moduleResolution": "bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "verbatimModuleSyntax": true,
9
+ "noEmit": true,
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "noUncheckedIndexedAccess": true,
14
+ "noImplicitOverride": true,
15
+ "forceConsistentCasingInFileNames": true
16
+ }
17
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "spec-embryo-web",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "svelte-kit sync && vite dev",
7
+ "build": "svelte-kit sync && vite build",
8
+ "preview": "vite preview",
9
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
10
+ },
11
+ "devDependencies": {
12
+ "@sveltejs/adapter-auto": "7.0.1",
13
+ "@sveltejs/kit": "2.55.0",
14
+ "@sveltejs/vite-plugin-svelte": "7.0.0",
15
+ "@types/node": "25.5.0",
16
+ "svelte": "5.55.1",
17
+ "svelte-check": "4.4.6",
18
+ "typescript": "6.0.2",
19
+ "vite": "8.0.3"
20
+ },
21
+ "dependencies": {
22
+ "@lucide/svelte": "^1.7.0"
23
+ }
24
+ }
@@ -0,0 +1,83 @@
1
+ :root {
2
+ --bg: #f1ebdf;
3
+ --bg-deep: #e5dac7;
4
+ --panel: rgba(255, 252, 246, 0.84);
5
+ --panel-strong: rgba(255, 253, 249, 0.95);
6
+ --panel-muted: rgba(248, 242, 232, 0.82);
7
+ --ink: #181612;
8
+ --muted: #5a5143;
9
+ --muted-soft: #766b5d;
10
+ --line: rgba(24, 22, 18, 0.12);
11
+ --line-strong: rgba(24, 22, 18, 0.22);
12
+ --accent: #0f8d60;
13
+ --accent-soft: rgba(15, 141, 96, 0.12);
14
+ --warning: #8f5421;
15
+ --warning-soft: rgba(143, 84, 33, 0.14);
16
+ --danger: #972d2d;
17
+ --danger-soft: rgba(151, 45, 45, 0.12);
18
+ --shadow: 0 28px 80px rgba(37, 29, 13, 0.12);
19
+ --shadow-soft: 0 10px 28px rgba(34, 26, 12, 0.08);
20
+ --display-font: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
21
+ --body-font: "Avenir Next", "Helvetica Neue", "Segoe UI", sans-serif;
22
+ --radius-lg: 1.15rem;
23
+ --radius-xl: 1.55rem;
24
+ }
25
+
26
+ *,
27
+ *::before,
28
+ *::after {
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ html,
33
+ body {
34
+ min-height: 100%;
35
+ }
36
+
37
+ body {
38
+ margin: 0;
39
+ color: var(--ink);
40
+ background:
41
+ radial-gradient(circle at top left, rgba(15, 141, 96, 0.12), transparent 24rem),
42
+ radial-gradient(circle at top right, rgba(151, 45, 45, 0.08), transparent 22rem),
43
+ linear-gradient(180deg, #f7f1e6 0%, var(--bg) 48%, var(--bg-deep) 100%);
44
+ font-family: var(--body-font);
45
+ text-rendering: optimizeLegibility;
46
+ }
47
+
48
+ body::before {
49
+ content: "";
50
+ position: fixed;
51
+ inset: 0;
52
+ pointer-events: none;
53
+ background-image:
54
+ linear-gradient(rgba(26, 24, 21, 0.03) 1px, transparent 1px),
55
+ linear-gradient(90deg, rgba(26, 24, 21, 0.03) 1px, transparent 1px);
56
+ background-size: 24px 24px;
57
+ mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.85), transparent);
58
+ }
59
+
60
+ body::after {
61
+ content: "";
62
+ position: fixed;
63
+ inset: 0;
64
+ pointer-events: none;
65
+ background:
66
+ linear-gradient(140deg, rgba(255, 255, 255, 0.18), transparent 32%),
67
+ radial-gradient(circle at 80% 18%, rgba(15, 141, 96, 0.08), transparent 18rem);
68
+ }
69
+
70
+ a {
71
+ color: inherit;
72
+ }
73
+
74
+ button,
75
+ input,
76
+ textarea,
77
+ select {
78
+ font: inherit;
79
+ }
80
+
81
+ ::selection {
82
+ background: rgba(15, 141, 96, 0.18);
83
+ }
@@ -0,0 +1,6 @@
1
+ // See https://svelte.dev/docs/kit/types#app.d.ts
2
+ declare global {
3
+ namespace App {}
4
+ }
5
+
6
+ export {};
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ %sveltekit.head%
7
+ </head>
8
+ <body data-sveltekit-preload-data="hover">
9
+ <div style="display: contents">%sveltekit.body%</div>
10
+ </body>
11
+ </html>
@@ -0,0 +1,293 @@
1
+ <script lang="ts">
2
+ import { goto } from "$app/navigation";
3
+ import { page } from "$app/state";
4
+ import { Filter, Layers3, RotateCcw, SlidersHorizontal } from "@lucide/svelte";
5
+ import type { Snippet } from "svelte";
6
+ import type { DashboardData, DashboardTimeWindow } from "../../../../src/services/dashboard.ts";
7
+ import MultiSelectDropdown from "./MultiSelectDropdown.svelte";
8
+ import SelectDropdown from "./SelectDropdown.svelte";
9
+
10
+ let {
11
+ dashboard,
12
+ title = "Analysis filters",
13
+ detail = "Slice the analytical views by spec set and lifecycle period.",
14
+ eyebrow = "Analytical slice",
15
+ showTimeWindow = true,
16
+ resultCount = dashboard.analysis.filteredTaskCount,
17
+ resultLabel = "tasks in the current analytical slice",
18
+ hasActiveFilters = dashboard.analysis.hasActiveFilters,
19
+ inactiveDetail = "Without analytical filters, archived tasks stay hidden from active-flow views.",
20
+ activeDetail = "Archived tasks are included while filters are active.",
21
+ clearHref = "",
22
+ extraField,
23
+ } = $props<{
24
+ dashboard: DashboardData;
25
+ title?: string;
26
+ detail?: string;
27
+ eyebrow?: string;
28
+ showTimeWindow?: boolean;
29
+ resultCount?: number;
30
+ resultLabel?: string;
31
+ hasActiveFilters?: boolean;
32
+ inactiveDetail?: string;
33
+ activeDetail?: string;
34
+ clearHref?: string;
35
+ extraField?: Snippet;
36
+ }>();
37
+
38
+ const timeWindows: Array<{ value: DashboardTimeWindow; label: string; description: string }> = [
39
+ { value: "7d", label: "Last 7 days", description: "Only recently completed or cancelled work." },
40
+ { value: "30d", label: "Last 30 days", description: "A broader recent delivery slice." },
41
+ { value: "90d", label: "Last 90 days", description: "A quarterly view of lifecycle history." },
42
+ ];
43
+
44
+ const specOptions = $derived(
45
+ dashboard.analysis.availableSpecs.map((spec: DashboardData["analysis"]["availableSpecs"][number]) => ({
46
+ value: spec.id,
47
+ label: spec.id,
48
+ description: spec.title,
49
+ })),
50
+ );
51
+
52
+ const currentPath = $derived(page.url.pathname);
53
+
54
+ async function applyFilters(event: Event): Promise<void> {
55
+ const form = event.currentTarget as HTMLFormElement;
56
+ const formData = new FormData(form);
57
+ const searchParams = new URLSearchParams();
58
+
59
+ for (const [key, value] of formData.entries()) {
60
+ if (typeof value !== "string" || value.length === 0) {
61
+ continue;
62
+ }
63
+
64
+ searchParams.append(key, value);
65
+ }
66
+
67
+ const search = searchParams.toString();
68
+ await goto(search.length > 0 ? `${currentPath}?${search}` : currentPath, {
69
+ keepFocus: true,
70
+ noScroll: true,
71
+ replaceState: true,
72
+ });
73
+ }
74
+ </script>
75
+
76
+ <section class="filters-shell">
77
+ <div class="filters-copy">
78
+ <div class="copy-head">
79
+ <span class="icon-pill">
80
+ <Filter size={15} strokeWidth={1.9} />
81
+ </span>
82
+ <div>
83
+ <p class="eyebrow">{eyebrow}</p>
84
+ <h4>{title}</h4>
85
+ </div>
86
+ </div>
87
+ <p>{detail}</p>
88
+ </div>
89
+
90
+ <form class="filters-form" method="GET" action={currentPath} onchange={applyFilters}>
91
+ <div class="field-grid">
92
+ <fieldset class="field-card">
93
+ <legend>
94
+ <Layers3 size={14} strokeWidth={1.9} />
95
+ Specs
96
+ </legend>
97
+ <MultiSelectDropdown
98
+ name="spec"
99
+ options={specOptions}
100
+ selectedValues={dashboard.analysis.selectedSpecIds}
101
+ buttonLabel="Choose specs"
102
+ emptyLabel="All specs"
103
+ />
104
+ </fieldset>
105
+
106
+ {#if showTimeWindow}
107
+ <fieldset class="field-card field-card-select">
108
+ <legend>
109
+ <SlidersHorizontal size={14} strokeWidth={1.9} />
110
+ Time window
111
+ </legend>
112
+ <SelectDropdown
113
+ name="window"
114
+ options={timeWindows}
115
+ selectedValue={dashboard.analysis.timeWindow ?? ""}
116
+ buttonLabel="Choose window"
117
+ emptyLabel="All lifecycle history"
118
+ />
119
+ </fieldset>
120
+ {/if}
121
+
122
+ {@render extraField?.()}
123
+ </div>
124
+
125
+ <div class="filters-footer">
126
+ <div class="result-copy">
127
+ <strong>{resultCount}</strong>
128
+ <span>{resultLabel}</span>
129
+ {#if hasActiveFilters}
130
+ <small>{activeDetail}</small>
131
+ {:else}
132
+ <small>{inactiveDetail}</small>
133
+ {/if}
134
+ </div>
135
+
136
+ <div class="button-row">
137
+ <a class="ghost-button" href={clearHref || currentPath}>
138
+ <RotateCcw size={14} strokeWidth={1.9} />
139
+ Clear
140
+ </a>
141
+ </div>
142
+ </div>
143
+ </form>
144
+ </section>
145
+
146
+ <style>
147
+ .filters-shell {
148
+ display: grid;
149
+ gap: 0.85rem;
150
+ margin-bottom: 1rem;
151
+ padding: 0.9rem;
152
+ border-radius: var(--radius-xl);
153
+ border: 1px solid var(--line);
154
+ background: linear-gradient(180deg, rgba(255, 252, 246, 0.94), rgba(247, 241, 232, 0.88));
155
+ box-shadow: var(--shadow);
156
+ }
157
+
158
+ .filters-copy,
159
+ .filters-form,
160
+ .field-grid {
161
+ display: grid;
162
+ gap: 0.7rem;
163
+ }
164
+
165
+ .copy-head,
166
+ .filters-footer,
167
+ .button-row,
168
+ legend {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 0.5rem;
172
+ }
173
+
174
+ .filters-footer {
175
+ justify-content: space-between;
176
+ gap: 0.85rem;
177
+ flex-wrap: wrap;
178
+ }
179
+
180
+ .copy-head {
181
+ align-items: start;
182
+ }
183
+
184
+ .icon-pill {
185
+ display: inline-flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ width: 2.05rem;
189
+ height: 2.05rem;
190
+ border-radius: 0.85rem;
191
+ border: 1px solid var(--line);
192
+ background: rgba(255, 255, 255, 0.84);
193
+ color: var(--muted-soft);
194
+ flex-shrink: 0;
195
+ }
196
+
197
+ .eyebrow {
198
+ margin: 0 0 0.22rem;
199
+ text-transform: uppercase;
200
+ letter-spacing: 0.14em;
201
+ font-size: 0.68rem;
202
+ color: var(--muted);
203
+ }
204
+
205
+ h4,
206
+ p,
207
+ fieldset,
208
+ legend {
209
+ margin: 0;
210
+ }
211
+
212
+ h4 {
213
+ font-family: var(--display-font);
214
+ font-size: 1.02rem;
215
+ }
216
+
217
+ p,
218
+ .result-copy span,
219
+ .result-copy small {
220
+ color: var(--muted);
221
+ line-height: 1.45;
222
+ }
223
+
224
+ .field-grid {
225
+ grid-template-columns: minmax(0, 1.65fr) repeat(2, minmax(13rem, 0.9fr));
226
+ }
227
+
228
+ .field-grid:has(> :only-child) {
229
+ grid-template-columns: 1fr;
230
+ }
231
+
232
+ .field-card {
233
+ display: grid;
234
+ gap: 0.55rem;
235
+ min-width: 0;
236
+ padding: 0.72rem;
237
+ border-radius: var(--radius-lg);
238
+ border: 1px solid var(--line);
239
+ background: rgba(255, 255, 255, 0.72);
240
+ box-shadow: var(--shadow-soft);
241
+ }
242
+
243
+ .field-card-select {
244
+ align-content: start;
245
+ }
246
+
247
+ legend,
248
+ legend {
249
+ padding: 0;
250
+ color: var(--muted-soft);
251
+ font-size: 0.78rem;
252
+ font-weight: 600;
253
+ }
254
+
255
+ .ghost-button {
256
+ border-radius: 999px;
257
+ border: 1px solid var(--line-strong);
258
+ background: rgba(255, 255, 255, 0.9);
259
+ color: var(--ink);
260
+ box-shadow: var(--shadow-soft);
261
+ }
262
+
263
+ .result-copy {
264
+ display: grid;
265
+ gap: 0.08rem;
266
+ }
267
+
268
+ .result-copy strong {
269
+ font-family: var(--display-font);
270
+ font-size: 1.08rem;
271
+ line-height: 1;
272
+ }
273
+
274
+ .button-row {
275
+ flex-wrap: wrap;
276
+ justify-content: end;
277
+ }
278
+
279
+ .ghost-button {
280
+ display: inline-flex;
281
+ align-items: center;
282
+ gap: 0.42rem;
283
+ padding: 0.62rem 0.88rem;
284
+ text-decoration: none;
285
+ cursor: pointer;
286
+ }
287
+
288
+ @media (max-width: 980px) {
289
+ .field-grid {
290
+ grid-template-columns: 1fr;
291
+ }
292
+ }
293
+ </style>