Really-amin commited on
Commit
20320b7
·
verified ·
1 Parent(s): d70685c

Upload PromptForge v1.0 — Structured prompt generator for Google AI Studio

Browse files
Files changed (4) hide show
  1. frontend/client.js +18 -327
  2. frontend/index.html +173 -61
  3. frontend/style.css +197 -282
  4. frontend/style.css.bak +284 -0
frontend/client.js CHANGED
@@ -1,332 +1,23 @@
1
  /**
2
- * PromptForge — client.js
3
- * Handles all API communication and UI state transitions.
4
  */
5
-
6
- const API_BASE = ""; // Same origin; change to e.g. "http://localhost:8000" for dev
7
- let currentPromptId = null; // Tracks the active manifest
8
-
9
- // ── DOM refs ──────────────────────────────────────────────────────
10
  const $ = id => document.getElementById(id);
11
- const steps = {
12
- input: $("step-input"),
13
- manifest: $("step-manifest"),
14
- finalized: $("step-finalized"),
15
- refine: $("step-refine"),
16
- };
17
-
18
- // ── Utility ───────────────────────────────────────────────────────
19
- function show(el) { el.classList.remove("hidden"); }
20
- function hide(el) { el.classList.add("hidden"); }
21
- function showStep(name) {
22
- Object.values(steps).forEach(hide);
23
- show(steps[name]);
24
- }
25
-
26
- function toast(msg, type = "info") {
27
- const t = $("toast");
28
- t.textContent = msg;
29
- t.className = `toast ${type}`;
30
- show(t);
31
- setTimeout(() => hide(t), 3500);
32
- }
33
-
34
- function setLoading(btn, loading) {
35
- btn.disabled = loading;
36
- btn.dataset.originalText ??= btn.textContent;
37
- btn.textContent = loading ? "⏳ Working…" : btn.dataset.originalText;
38
- }
39
-
40
- async function apiFetch(path, method = "GET", body = null) {
41
- const opts = {
42
- method,
43
- headers: { "Content-Type": "application/json" },
44
- };
45
- if (body) opts.body = JSON.stringify(body);
46
- const resp = await fetch(API_BASE + path, opts);
47
- if (!resp.ok) {
48
- const err = await resp.json().catch(() => ({ detail: resp.statusText }));
49
- throw new Error(err.detail || "Unknown error");
50
- }
51
- return resp.json();
52
- }
53
-
54
- // ── Step 1: Generate ──────────────────────────────────────────────
55
- $("btn-generate").addEventListener("click", async () => {
56
- const instruction = $("instruction").value.trim();
57
- if (!instruction || instruction.length < 5) {
58
- toast("Please enter a meaningful instruction (min 5 characters).", "error");
59
- return;
60
- }
61
-
62
- const provider = $("provider").value;
63
- const apiKey = $("api-key").value.trim();
64
- const extraCtx = $("extra-context").value.trim();
65
- const enhance = provider !== "none" && !!apiKey;
66
-
67
- const btn = $("btn-generate");
68
- setLoading(btn, true);
69
- try {
70
- const data = await apiFetch("/api/generate", "POST", {
71
- instruction,
72
- output_format: "both",
73
- provider,
74
- api_key: apiKey || null,
75
- enhance,
76
- extra_context: extraCtx || null,
77
- });
78
-
79
- currentPromptId = data.prompt_id;
80
- renderManifest(data.manifest);
81
- show(steps.manifest);
82
- steps.manifest.scrollIntoView({ behavior: "smooth" });
83
- toast("✅ Manifest generated — review and approve.", "success");
84
- } catch (e) {
85
- toast(`Error: ${e.message}`, "error");
86
- } finally {
87
- setLoading(btn, false);
88
- }
89
- });
90
-
91
- // ── Provider selector shows/hides API key field ───────────────────
92
- $("provider").addEventListener("change", () => {
93
- const group = $("api-key-group");
94
- group.style.display = $("provider").value !== "none" ? "flex" : "none";
95
- });
96
-
97
- // ── Render manifest into editable grid ───────────────────────────
98
- function renderManifest(manifest) {
99
- const sp = manifest.structured_prompt;
100
- const grid = $("manifest-grid");
101
- grid.innerHTML = "";
102
-
103
- const fields = [
104
- { key: "role", label: "Role", value: sp.role, full: false },
105
- { key: "style", label: "Style & Tone", value: sp.style, full: false },
106
- { key: "task", label: "Task", value: sp.task, full: true },
107
- { key: "input_format", label: "Input Format", value: sp.input_format, full: false },
108
- { key: "output_format", label: "Output Format", value: sp.output_format, full: false },
109
- { key: "constraints", label: "Constraints", value: sp.constraints.join("\n"), full: true },
110
- { key: "safety", label: "Safety", value: sp.safety.join("\n"), full: true },
111
- ];
112
-
113
- fields.forEach(f => {
114
- const div = document.createElement("div");
115
- div.className = `manifest-field${f.full ? " full" : ""}`;
116
- div.innerHTML = `<label>${f.label}</label>
117
- <textarea id="field-${f.key}" rows="${f.full ? 3 : 2}">${escapeHtml(f.value)}</textarea>`;
118
- grid.appendChild(div);
119
- });
120
-
121
- $("manifest-json").textContent = JSON.stringify(manifest, null, 2);
122
- }
123
-
124
- // ── Step 2/3: Approve ─────────────────────────────────────────────
125
- $("btn-approve").addEventListener("click", async () => {
126
- if (!currentPromptId) return;
127
-
128
- // Collect edited field values
129
- const edits = {};
130
- ["role", "style", "task", "input_format", "output_format"].forEach(key => {
131
- const el = $(`field-${key}`);
132
- if (el) edits[key] = el.value.trim();
133
- });
134
- const cEl = $("field-constraints");
135
- if (cEl) edits.constraints = cEl.value.trim().split("\n").filter(Boolean);
136
- const sEl = $("field-safety");
137
- if (sEl) edits.safety = sEl.value.trim().split("\n").filter(Boolean);
138
-
139
- const btn = $("btn-approve");
140
- setLoading(btn, true);
141
- try {
142
- const data = await apiFetch("/api/approve", "POST", {
143
- prompt_id: currentPromptId,
144
- edits,
145
- });
146
-
147
- renderFinalizedPrompt(data.finalized_prompt);
148
- hide(steps.manifest);
149
- show(steps.finalized);
150
- steps.finalized.scrollIntoView({ behavior: "smooth" });
151
- toast("✅ Prompt approved and finalized!", "success");
152
- } catch (e) {
153
- toast(`Approval failed: ${e.message}`, "error");
154
- } finally {
155
- setLoading(btn, false);
156
- }
157
- });
158
-
159
- // ── Render finalized prompt ────────────────────────────────────────
160
- function renderFinalizedPrompt(sp) {
161
- $("finalized-text").textContent = sp.raw_prompt_text;
162
- $("finalized-json").textContent = JSON.stringify(sp, null, 2);
163
- }
164
-
165
- // ── Tabs ──────────────────────────────────────────────────────────
166
- document.querySelectorAll(".tab").forEach(tab => {
167
- tab.addEventListener("click", () => {
168
- const target = tab.dataset.tab;
169
- document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
170
- tab.classList.add("active");
171
- document.querySelectorAll(".tab-panel").forEach(p => hide(p));
172
- show($(`tab-${target}`));
173
- });
174
- });
175
-
176
- // ── Copy buttons ──────────────────────────────────────────────────
177
- document.querySelectorAll(".btn-copy").forEach(btn => {
178
- btn.addEventListener("click", () => {
179
- const pre = $(btn.dataset.target);
180
- if (!pre) return;
181
- navigator.clipboard.writeText(pre.textContent).then(() => {
182
- const orig = btn.textContent;
183
- btn.textContent = "✓ Copied!";
184
- setTimeout(() => (btn.textContent = orig), 1800);
185
- });
186
- });
187
  });
188
-
189
- // ── Export ────────────────────────────────────────────────────────
190
- async function exportPrompt(format) {
191
- if (!currentPromptId) return;
192
- try {
193
- const data = await apiFetch("/api/export", "POST", {
194
- prompt_id: currentPromptId,
195
- export_format: format,
196
- });
197
- const content = typeof data.data === "string" ? data.data : JSON.stringify(data.data, null, 2);
198
- const ext = format === "text" ? "txt" : "json";
199
- downloadFile(`prompt_${currentPromptId.slice(0,8)}.${ext}`, content);
200
- toast(`⬇ Exported as .${ext}`, "success");
201
- } catch (e) {
202
- toast(`Export failed: ${e.message}`, "error");
203
- }
204
- }
205
-
206
- $("btn-export-json").addEventListener("click", () => exportPrompt("json"));
207
- $("btn-export-txt").addEventListener("click", () => exportPrompt("text"));
208
-
209
- function downloadFile(filename, content) {
210
- const a = document.createElement("a");
211
- a.href = URL.createObjectURL(new Blob([content], { type: "text/plain" }));
212
- a.download = filename;
213
- a.click();
214
- URL.revokeObjectURL(a.href);
215
- }
216
-
217
- // ── Refine ────────────────────────────────────────────────────────
218
- $("btn-refine").addEventListener("click", () => {
219
- show(steps.refine);
220
- steps.refine.scrollIntoView({ behavior: "smooth" });
221
- });
222
- $("btn-cancel-refine").addEventListener("click", () => hide(steps.refine));
223
-
224
- $("btn-submit-refine").addEventListener("click", async () => {
225
- if (!currentPromptId) return;
226
- const feedback = $("feedback").value.trim();
227
- if (!feedback) { toast("Please enter feedback.", "error"); return; }
228
-
229
- const btn = $("btn-submit-refine");
230
- setLoading(btn, true);
231
- try {
232
- const data = await apiFetch("/api/refine", "POST", {
233
- prompt_id: currentPromptId,
234
- feedback,
235
- provider: $("provider").value,
236
- api_key: $("api-key").value.trim() || null,
237
- });
238
-
239
- currentPromptId = data.prompt_id;
240
- renderManifest(data.manifest);
241
- hide(steps.finalized);
242
- hide(steps.refine);
243
- show(steps.manifest);
244
- $("feedback").value = "";
245
- steps.manifest.scrollIntoView({ behavior: "smooth" });
246
- toast(`🔁 Refined to v${data.manifest.version} — please re-approve.`, "success");
247
- } catch (e) {
248
- toast(`Refine failed: ${e.message}`, "error");
249
- } finally {
250
- setLoading(btn, false);
251
- }
252
  });
253
-
254
- // ── Reset / New ───────────────────────────────────────────────────
255
- function resetAll() {
256
- currentPromptId = null;
257
- $("instruction").value = "";
258
- $("extra-context").value = "";
259
- $("feedback").value = "";
260
- Object.values(steps).forEach(hide);
261
- show(steps.input);
262
- window.scrollTo({ top: 0, behavior: "smooth" });
263
- }
264
-
265
- $("btn-reset").addEventListener("click", resetAll);
266
- $("btn-new").addEventListener("click", resetAll);
267
-
268
- // ── History ────────────────────────────────────────────────────────
269
- $("btn-load-history").addEventListener("click", loadHistory);
270
-
271
- async function loadHistory() {
272
- try {
273
- const data = await apiFetch("/api/history");
274
- const tbody = $("history-body");
275
-
276
- if (data.total === 0) {
277
- tbody.innerHTML = `<tr><td colspan="6" class="muted center">No prompts generated yet.</td></tr>`;
278
- return;
279
- }
280
-
281
- tbody.innerHTML = data.entries.map(e => `
282
- <tr>
283
- <td><code>${e.prompt_id.slice(0, 8)}…</code></td>
284
- <td>${escapeHtml(e.instruction.slice(0, 60))}${e.instruction.length > 60 ? "…" : ""}</td>
285
- <td>v${e.version}</td>
286
- <td><span class="status-badge status-${e.status}">${e.status}</span></td>
287
- <td>${new Date(e.created_at).toLocaleString()}</td>
288
- <td>
289
- <button class="btn-sm btn-secondary" onclick="loadPromptById('${e.prompt_id}')">Load</button>
290
- <button class="btn-sm btn-secondary" onclick="deletePrompt('${e.prompt_id}')">🗑</button>
291
- </td>
292
- </tr>`).join("");
293
- } catch (e) {
294
- toast(`Could not load history: ${e.message}`, "error");
295
- }
296
- }
297
-
298
- window.loadPromptById = async function(id) {
299
- try {
300
- const manifest = await apiFetch(`/api/history/${id}`);
301
- currentPromptId = manifest.prompt_id;
302
- renderManifest(manifest);
303
- show(steps.manifest);
304
- steps.manifest.scrollIntoView({ behavior: "smooth" });
305
- toast("Manifest loaded from history.", "success");
306
- } catch (e) {
307
- toast(`Load failed: ${e.message}`, "error");
308
- }
309
- };
310
-
311
- window.deletePrompt = async function(id) {
312
- if (!confirm("Delete this prompt from history?")) return;
313
- try {
314
- await apiFetch(`/api/history/${id}`, "DELETE");
315
- toast("Prompt deleted.", "success");
316
- loadHistory();
317
- } catch (e) {
318
- toast(`Delete failed: ${e.message}`, "error");
319
- }
320
- };
321
-
322
- // ── Helpers ────────────────────────────────────────────────────────
323
- function escapeHtml(str) {
324
- return String(str)
325
- .replace(/&/g, "&amp;")
326
- .replace(/</g, "&lt;")
327
- .replace(/>/g, "&gt;")
328
- .replace(/"/g, "&quot;");
329
- }
330
-
331
- // ── Init ──────────────────────────────────────────────────────────
332
- loadHistory();
 
1
  /**
2
+ * PromptForge — client.js v2.0
3
+ * Custom cursor, toast system, step progress, full API flow.
4
  */
5
+ const API = "";
6
+ let currentPromptId = null;
 
 
 
7
  const $ = id => document.getElementById(id);
8
+ const show = el => el?.classList.remove("hidden");
9
+ const hide = el => el?.classList.add("hidden");
10
+
11
+ /* Custom Cursor */
12
+ const dot = $("cursor-dot"), ring = $("cursor-ring");
13
+ let mx=0,my=0,rx=0,ry=0;
14
+ document.addEventListener("mousemove", e => {
15
+ mx=e.clientX; my=e.clientY;
16
+ dot.style.left=mx+"px"; dot.style.top=my+"px";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  });
18
+ (function animRing(){ rx+=(mx-rx)*.14; ry+=(my-ry)*.14;
19
+ ring.style.left=rx+"px"; ring.style.top=ry+"px"; requestAnimationFrame(animRing); })();
20
+ document.querySelectorAll("button,a,select,textarea,input,summary").forEach(el=>{
21
+ el.addEventListener("mouseenter",()=>{ dot.style.width="14px";dot.style.height="14px";dot.style.background="var(--magenta)";ring.style.width="54px";ring.style.height="54px";ring.style.borderColor="rgba(247,37,133,.45)"; });
22
+ el.addEventListener("mouseleave",()=>{ dot.style.width="8px";dot.style.height="8px";dot.style.background="var(--cyan)";ring.style.width="36px";ring.style.height="36px";ring.style.borderColor="rgba(0,245,212,.45)"; });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/index.html CHANGED
@@ -1,54 +1,129 @@
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.0" />
6
- <title>PromptForge — Google AI Studio Prompt Generator</title>
7
- <link rel="stylesheet" href="/static/style.css" />
 
8
  </head>
9
  <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  <header>
11
  <div class="header-inner">
12
- <div class="logo">⚙️ PromptForge</div>
13
- <span class="tagline">Structured prompts for Google AI Studio</span>
 
 
 
 
 
 
 
 
 
 
14
  </div>
15
  </header>
16
 
17
  <main>
18
- <!-- ── Step 0: Input ─────────────────────────────────────────── -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  <section id="step-input" class="card">
20
- <h2><span class="step-badge">STEP 1</span> Enter Your Instruction</h2>
 
 
 
21
 
22
- <label for="instruction">Raw instruction / task description</label>
23
- <textarea id="instruction" rows="5"
24
- placeholder="e.g. Generate a TypeScript React component with TailwindCSS and unit tests."></textarea>
 
25
 
26
- <label for="extra-context">Additional context (optional)</label>
27
- <textarea id="extra-context" rows="2"
28
- placeholder="e.g. The component should be accessible and support dark mode."></textarea>
 
 
 
 
 
 
 
 
29
 
30
- <div class="row-two">
31
- <div class="field-group">
32
- <label for="provider">AI Enhancement Provider</label>
33
  <select id="provider">
34
- <option value="none">None (local only)</option>
35
- <option value="google">Google Gemini</option>
36
- <option value="huggingface">Hugging Face</option>
37
  </select>
 
38
  </div>
39
- <div class="field-group" id="api-key-group" style="display:none;">
40
- <label for="api-key">API Key <span class="muted">(never stored)</span></label>
41
- <input id="api-key" type="password" placeholder="Paste your API key here..." />
 
42
  </div>
43
  </div>
44
 
45
- <button id="btn-generate" class="btn-primary">⚡ Generate Prompt Manifest</button>
 
 
46
  </section>
47
 
48
- <!-- ── Step 1: Manifest preview ──────────────────────────────── -->
49
- <section id="step-manifest" class="card hidden">
50
- <h2><span class="step-badge">STEP 2</span> Review Manifest</h2>
51
- <p class="muted">Review the generated manifest. Edit any field below, then approve.</p>
 
 
 
 
52
 
53
  <div id="manifest-grid" class="manifest-grid"></div>
54
 
@@ -59,71 +134,108 @@
59
 
60
  <div class="action-row">
61
  <button id="btn-approve" class="btn-primary">✅ Approve &amp; Finalize</button>
62
- <button id="btn-reset" class="btn-secondary">↩ Start Over</button>
63
  </div>
64
  </section>
65
 
66
- <!-- ── Step 2: Finalized prompt ──────────────────────────────── -->
67
- <section id="step-finalized" class="card hidden">
68
- <h2><span class="step-badge">STEP 3</span> Finalized Prompt</h2>
69
- <p class="muted">Your structured prompt is ready. Copy it directly into Google AI Studio.</p>
 
 
 
 
70
 
71
  <div class="tab-bar">
72
  <button class="tab active" data-tab="text">📄 Plain Text</button>
73
- <button class="tab" data-tab="json">{ } JSON</button>
74
  </div>
75
 
76
  <div id="tab-text" class="tab-panel">
77
  <pre id="finalized-text"></pre>
78
- <button class="btn-copy" data-target="finalized-text">📋 Copy</button>
79
  </div>
80
  <div id="tab-json" class="tab-panel hidden">
81
  <pre id="finalized-json"></pre>
82
- <button class="btn-copy" data-target="finalized-json">📋 Copy</button>
83
  </div>
84
 
 
 
85
  <div class="action-row">
86
  <button id="btn-export-json" class="btn-secondary">⬇ Export JSON</button>
87
- <button id="btn-export-txt" class="btn-secondary">⬇ Export Text</button>
88
- <button id="btn-refine" class="btn-secondary">🔁 Refine with Feedback</button>
89
- <button id="btn-new" class="btn-primary">➕ New Prompt</button>
90
  </div>
91
  </section>
92
 
93
- <!-- ── Refine panel ───────────────────────────────────────────── -->
94
- <section id="step-refine" class="card hidden">
95
- <h2><span class="step-badge">STEP 5</span> Refine Prompt</h2>
96
- <label for="feedback">Your feedback / change requests</label>
97
- <textarea id="feedback" rows="3"
98
- placeholder="e.g. Add accessibility constraints. Use React hooks only."></textarea>
 
 
 
 
 
 
 
 
99
  <div class="action-row">
100
  <button id="btn-submit-refine" class="btn-primary">🔁 Submit Refinement</button>
101
  <button id="btn-cancel-refine" class="btn-secondary">Cancel</button>
102
  </div>
103
  </section>
104
 
105
- <!-- ── History ───────────────────────────────────────────────── -->
106
  <section id="section-history" class="card">
107
- <h2>📜 Prompt History</h2>
108
- <button id="btn-load-history" class="btn-secondary btn-sm">Refresh History</button>
109
- <table id="history-table">
110
- <thead>
111
- <tr><th>ID</th><th>Instruction</th><th>Version</th><th>Status</th><th>Date</th><th></th></tr>
112
- </thead>
113
- <tbody id="history-body">
114
- <tr><td colspan="6" class="muted center">Click "Refresh History" to load.</td></tr>
115
- </tbody>
116
- </table>
 
 
 
 
 
 
 
 
 
 
 
 
117
  </section>
118
- </main>
119
 
120
- <!-- Toast notification -->
121
- <div id="toast" class="toast hidden"></div>
122
 
 
123
  <footer>
124
- <p>PromptForge v1.0 · <a href="/docs" target="_blank">API Docs</a> · Built with FastAPI + Vanilla JS</p>
 
 
 
 
 
 
 
125
  </footer>
126
 
127
- <script src="/static/client.js"></script>
 
 
 
 
 
128
  </body>
129
  </html>
 
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.0"/>
6
+ <title>PromptForge — Neural Prompt Generator</title>
7
+ <link rel="stylesheet" href="/static/style.css"/>
8
+ <meta name="description" content="Transform raw instructions into structured, ready-to-use prompts for Google AI Studio."/>
9
  </head>
10
  <body>
11
+
12
+ <!-- ── Custom Cursor ─────────────────────────────────────────────── -->
13
+ <div id="cursor-dot"></div>
14
+ <div id="cursor-ring"></div>
15
+
16
+ <!-- ── Background Layers ─────────────────────────────────────────── -->
17
+ <div class="bg-mesh"></div>
18
+ <div class="bg-grid"></div>
19
+ <div class="orb orb-1"></div>
20
+ <div class="orb orb-2"></div>
21
+ <div class="orb orb-3"></div>
22
+
23
+ <div id="app">
24
+
25
+ <!-- ── Header ──────────────────────────────────────────────────── -->
26
  <header>
27
  <div class="header-inner">
28
+ <div class="logo-group">
29
+ <div class="logo-icon">⚙️</div>
30
+ <span class="logo-text">PromptForge</span>
31
+ <span class="logo-tag">v2.0</span>
32
+ </div>
33
+ <div class="header-meta">
34
+ <div class="status-pill">
35
+ <div class="status-dot"></div>
36
+ <span>ONLINE</span>
37
+ </div>
38
+ <a class="nav-link" href="/docs" target="_blank">⚡ API Docs</a>
39
+ </div>
40
  </div>
41
  </header>
42
 
43
  <main>
44
+
45
+ <!-- ── Step Progress ─────────────────────────────────────────── -->
46
+ <div class="step-progress">
47
+ <div class="prog-step active" id="pstep-1">
48
+ <div class="prog-node">1</div>
49
+ <div class="prog-label">Input</div>
50
+ </div>
51
+ <div class="prog-line" id="pline-1"></div>
52
+ <div class="prog-step" id="pstep-2">
53
+ <div class="prog-node">2</div>
54
+ <div class="prog-label">Review</div>
55
+ </div>
56
+ <div class="prog-line" id="pline-2"></div>
57
+ <div class="prog-step" id="pstep-3">
58
+ <div class="prog-node">3</div>
59
+ <div class="prog-label">Finalize</div>
60
+ </div>
61
+ <div class="prog-line" id="pline-3"></div>
62
+ <div class="prog-step" id="pstep-4">
63
+ <div class="prog-node">4</div>
64
+ <div class="prog-label">Export</div>
65
+ </div>
66
+ <div class="prog-line" id="pline-4"></div>
67
+ <div class="prog-step" id="pstep-5">
68
+ <div class="prog-node">5</div>
69
+ <div class="prog-label">Refine</div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- ── STEP 1: Input ─────────────────────────────────────────── -->
74
  <section id="step-input" class="card">
75
+ <div class="card-header">
76
+ <h2>Enter Your Instruction</h2>
77
+ <span class="step-badge">STEP 01</span>
78
+ </div>
79
 
80
+ <div class="info-banner">
81
+ <span class="info-icon">💡</span>
82
+ <span>Describe any task in plain English. PromptForge will transform it into a fully structured prompt ready for Google AI Studio.</span>
83
+ </div>
84
 
85
+ <div class="field">
86
+ <label><span class="lbl-dot"></span> Instruction</label>
87
+ <textarea id="instruction" rows="5"
88
+ placeholder="e.g. Generate a TypeScript React component with TailwindCSS and unit tests."></textarea>
89
+ </div>
90
+
91
+ <div class="field">
92
+ <label><span class="lbl-dot"></span> Additional Context <span class="lbl-opt">optional</span></label>
93
+ <textarea id="extra-context" rows="2"
94
+ placeholder="e.g. Support dark mode, must be accessible (WCAG AA), use React hooks only."></textarea>
95
+ </div>
96
 
97
+ <div class="input-grid">
98
+ <div class="field">
99
+ <label><span class="lbl-dot"></span> AI Enhancement</label>
100
  <select id="provider">
101
+ <option value="none"> Local Engine (no API)</option>
102
+ <option value="google">🌐 Google Gemini</option>
103
+ <option value="huggingface">🤗 Hugging Face</option>
104
  </select>
105
+ <div class="field-note">Optionally enhance via AI after generation.</div>
106
  </div>
107
+ <div class="field hidden" id="api-key-group">
108
+ <label><span class="lbl-dot"></span> API Key <span class="lbl-opt">never stored</span></label>
109
+ <input type="password" id="api-key" placeholder="Paste your API key here"/>
110
+ <div class="field-note">Transmitted securely, never logged.</div>
111
  </div>
112
  </div>
113
 
114
+ <div class="action-row">
115
+ <button id="btn-generate" class="btn-primary">⚡ Generate Prompt Manifest</button>
116
+ </div>
117
  </section>
118
 
119
+ <!-- ── STEP 2: Manifest Review ──────────────────────────────── -->
120
+ <section id="step-manifest" class="card hidden fade-in">
121
+ <div class="card-header">
122
+ <h2>Review &amp; Edit Manifest</h2>
123
+ <span class="step-badge">STEP 02</span>
124
+ </div>
125
+
126
+ <p class="muted">Every field is editable. Tweak anything before approving — the final prompt will regenerate automatically.</p>
127
 
128
  <div id="manifest-grid" class="manifest-grid"></div>
129
 
 
134
 
135
  <div class="action-row">
136
  <button id="btn-approve" class="btn-primary">✅ Approve &amp; Finalize</button>
137
+ <button id="btn-reset" class="btn-secondary">↩ Start Over</button>
138
  </div>
139
  </section>
140
 
141
+ <!-- ── STEP 3: Finalized Prompt ──────────────────────────────── -->
142
+ <section id="step-finalized" class="card hidden fade-in">
143
+ <div class="card-header">
144
+ <h2>Finalized Prompt</h2>
145
+ <span class="step-badge">STEP 03</span>
146
+ </div>
147
+
148
+ <p class="muted">Your structured prompt is ready. Copy it directly into Google AI Studio or export it below.</p>
149
 
150
  <div class="tab-bar">
151
  <button class="tab active" data-tab="text">📄 Plain Text</button>
152
+ <button class="tab" data-tab="json">{ } JSON</button>
153
  </div>
154
 
155
  <div id="tab-text" class="tab-panel">
156
  <pre id="finalized-text"></pre>
157
+ <button class="btn-copy" data-target="finalized-text">📋 Copy to Clipboard</button>
158
  </div>
159
  <div id="tab-json" class="tab-panel hidden">
160
  <pre id="finalized-json"></pre>
161
+ <button class="btn-copy" data-target="finalized-json">📋 Copy to Clipboard</button>
162
  </div>
163
 
164
+ <div class="divider"></div>
165
+
166
  <div class="action-row">
167
  <button id="btn-export-json" class="btn-secondary">⬇ Export JSON</button>
168
+ <button id="btn-export-txt" class="btn-secondary">⬇ Export Text</button>
169
+ <button id="btn-refine" class="btn-secondary">🔁 Refine with Feedback</button>
170
+ <button id="btn-new" class="btn-primary">➕ New Prompt</button>
171
  </div>
172
  </section>
173
 
174
+ <!-- ── STEP 5: Refine ────────────────────────────────────────── -->
175
+ <section id="step-refine" class="card hidden fade-in">
176
+ <div class="card-header">
177
+ <h2>Refine Prompt</h2>
178
+ <span class="step-badge">STEP 05</span>
179
+ </div>
180
+ <p class="muted">Describe what to change. PromptForge will generate a new version (v+1) of the manifest for re-approval.</p>
181
+
182
+ <div class="field">
183
+ <label><span class="lbl-dot"></span> Your Feedback</label>
184
+ <textarea id="feedback" rows="3"
185
+ placeholder="e.g. Add ARIA labels, keyboard navigation, and a dark-mode variant prop."></textarea>
186
+ </div>
187
+
188
  <div class="action-row">
189
  <button id="btn-submit-refine" class="btn-primary">🔁 Submit Refinement</button>
190
  <button id="btn-cancel-refine" class="btn-secondary">Cancel</button>
191
  </div>
192
  </section>
193
 
194
+ <!-- ── History ───────────────────────────────────────────────── -->
195
  <section id="section-history" class="card">
196
+ <div class="card-header">
197
+ <h2>Prompt History</h2>
198
+ <button id="btn-load-history" class="btn-secondary btn-sm">↺ Refresh</button>
199
+ </div>
200
+
201
+ <div class="table-wrap">
202
+ <table id="history-table">
203
+ <thead>
204
+ <tr>
205
+ <th>ID</th>
206
+ <th>Instruction</th>
207
+ <th>Ver</th>
208
+ <th>Status</th>
209
+ <th>Date</th>
210
+ <th>Actions</th>
211
+ </tr>
212
+ </thead>
213
+ <tbody id="history-body">
214
+ <tr><td class="empty-msg" colspan="6">Click ↺ Refresh to load history.</td></tr>
215
+ </tbody>
216
+ </table>
217
+ </div>
218
  </section>
 
219
 
220
+ </main>
 
221
 
222
+ <!-- ── Footer ──────────────────────────────────────────────────── -->
223
  <footer>
224
+ <div class="footer-inner">
225
+ <span class="footer-copy">© 2025 PromptForge — Neural Prompt Generator</span>
226
+ <div class="footer-links">
227
+ <a href="/docs" target="_blank">API Docs</a>
228
+ <a href="/redoc" target="_blank">ReDoc</a>
229
+ <a href="/health" target="_blank">Health</a>
230
+ </div>
231
+ </div>
232
  </footer>
233
 
234
+ </div><!-- #app -->
235
+
236
+ <!-- ── Toast container ─────────────────────────────────────────── -->
237
+ <div id="toast-container"></div>
238
+
239
+ <script src="/static/client.js"></script>
240
  </body>
241
  </html>
frontend/style.css CHANGED
@@ -1,284 +1,199 @@
1
- /* PromptForge — style.css */
 
 
 
2
 
3
- :root {
4
- --bg: #0f1117;
5
- --surface: #1a1d27;
6
- --surface2: #22263a;
7
- --border: #2e3350;
8
- --accent: #5b8df6;
9
- --accent-hover: #3d6fe0;
10
- --success: #34d399;
11
- --warning: #fbbf24;
12
- --danger: #f87171;
13
- --text: #e4e7f0;
14
- --text-muted: #7c84a3;
15
- --radius: 10px;
16
- --font: 'Inter', system-ui, sans-serif;
17
- --mono: 'Fira Code', 'Cascadia Code', monospace;
18
- }
19
-
20
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
21
-
22
- body {
23
- background: var(--bg);
24
- color: var(--text);
25
- font-family: var(--font);
26
- font-size: 15px;
27
- line-height: 1.6;
28
- min-height: 100vh;
29
- }
30
-
31
- /* ── Header ──────────────────────────────────────────────────── */
32
- header {
33
- background: linear-gradient(135deg, #1e2336 0%, #151824 100%);
34
- border-bottom: 1px solid var(--border);
35
- padding: 14px 24px;
36
- position: sticky;
37
- top: 0;
38
- z-index: 100;
39
- backdrop-filter: blur(6px);
40
- }
41
- .header-inner { display: flex; align-items: center; gap: 16px; max-width: 960px; margin: 0 auto; }
42
- .logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.3px; }
43
- .tagline { color: var(--text-muted); font-size: 0.85rem; }
44
-
45
- /* ── Main layout ──────────────────────────────────────────────── */
46
- main {
47
- max-width: 960px;
48
- margin: 32px auto;
49
- padding: 0 20px;
50
- display: flex;
51
- flex-direction: column;
52
- gap: 24px;
53
- }
54
-
55
- /* ── Cards ──────────────────────────────────────────────────────── */
56
- .card {
57
- background: var(--surface);
58
- border: 1px solid var(--border);
59
- border-radius: var(--radius);
60
- padding: 28px 30px;
61
- }
62
- .card h2 {
63
- font-size: 1.05rem;
64
- font-weight: 600;
65
- margin-bottom: 18px;
66
- display: flex;
67
- align-items: center;
68
- gap: 10px;
69
- }
70
- .step-badge {
71
- background: var(--accent);
72
- color: #fff;
73
- font-size: 0.65rem;
74
- font-weight: 700;
75
- letter-spacing: 0.5px;
76
- padding: 3px 8px;
77
- border-radius: 20px;
78
- }
79
-
80
- /* ── Form elements ─────────────────────────────────────────────── */
81
- label {
82
- display: block;
83
- margin-bottom: 6px;
84
- font-size: 0.875rem;
85
- color: var(--text-muted);
86
- font-weight: 500;
87
- }
88
- textarea, input[type="password"], select {
89
- width: 100%;
90
- background: var(--surface2);
91
- border: 1px solid var(--border);
92
- border-radius: 7px;
93
- color: var(--text);
94
- font-family: var(--font);
95
- font-size: 0.9rem;
96
- padding: 10px 14px;
97
- resize: vertical;
98
- transition: border-color 0.2s;
99
- margin-bottom: 16px;
100
- }
101
- textarea:focus, input:focus, select:focus {
102
- outline: none;
103
- border-color: var(--accent);
104
- }
105
- select option { background: var(--surface2); }
106
-
107
- .row-two {
108
- display: grid;
109
- grid-template-columns: 1fr 1fr;
110
- gap: 16px;
111
- }
112
- .field-group { display: flex; flex-direction: column; }
113
- .field-group label { margin-bottom: 6px; }
114
- .field-group textarea,
115
- .field-group input,
116
- .field-group select { margin-bottom: 0; }
117
-
118
- /* ── Buttons ────────────────────────────────────────────────────── */
119
- .btn-primary, .btn-secondary {
120
- display: inline-flex;
121
- align-items: center;
122
- gap: 6px;
123
- border: none;
124
- border-radius: 7px;
125
- cursor: pointer;
126
- font-family: var(--font);
127
- font-size: 0.9rem;
128
- font-weight: 600;
129
- padding: 10px 20px;
130
- transition: background 0.2s, transform 0.1s;
131
- }
132
- .btn-primary {
133
- background: var(--accent);
134
- color: #fff;
135
- }
136
- .btn-primary:hover { background: var(--accent-hover); transform: translateY(-1px); }
137
- .btn-primary:active { transform: translateY(0); }
138
-
139
- .btn-secondary {
140
- background: var(--surface2);
141
- color: var(--text);
142
- border: 1px solid var(--border);
143
- }
144
- .btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
145
 
146
- .btn-sm { font-size: 0.8rem; padding: 7px 14px; }
147
-
148
- .btn-copy {
149
- background: var(--surface2);
150
- border: 1px solid var(--border);
151
- border-radius: 6px;
152
- color: var(--text-muted);
153
- cursor: pointer;
154
- font-size: 0.8rem;
155
- margin-top: 8px;
156
- padding: 6px 14px;
157
- transition: color 0.2s;
158
- }
159
- .btn-copy:hover { color: var(--success); border-color: var(--success); }
160
-
161
- .action-row {
162
- display: flex;
163
- flex-wrap: wrap;
164
- gap: 10px;
165
- margin-top: 20px;
166
- }
167
-
168
- /* ── Manifest grid ──────────────────────────────────────────────── */
169
- .manifest-grid {
170
- display: grid;
171
- grid-template-columns: 1fr 1fr;
172
- gap: 16px;
173
- margin-bottom: 20px;
174
- }
175
- .manifest-field { display: flex; flex-direction: column; }
176
- .manifest-field.full { grid-column: 1 / -1; }
177
- .manifest-field label { font-size: 0.78rem; color: var(--accent); text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 4px; }
178
- .manifest-field textarea,
179
- .manifest-field input {
180
- margin-bottom: 0;
181
- font-size: 0.85rem;
182
- min-height: 70px;
183
- }
184
-
185
- /* ── Tabs ───────────────────────────────────────────────────────── */
186
- .tab-bar { display: flex; gap: 4px; margin-bottom: 14px; }
187
- .tab {
188
- background: none;
189
- border: 1px solid var(--border);
190
- border-radius: 6px;
191
- color: var(--text-muted);
192
- cursor: pointer;
193
- font-family: var(--font);
194
- font-size: 0.85rem;
195
- padding: 7px 16px;
196
- transition: all 0.2s;
197
- }
198
- .tab.active { background: var(--accent); border-color: var(--accent); color: #fff; }
199
-
200
- /* ── Pre / code blocks ──────────────────────────────────────────── */
201
- pre {
202
- background: #0a0c14;
203
- border: 1px solid var(--border);
204
- border-radius: 8px;
205
- color: #c9d1d9;
206
- font-family: var(--mono);
207
- font-size: 0.82rem;
208
- line-height: 1.5;
209
- overflow-x: auto;
210
- padding: 16px 18px;
211
- white-space: pre-wrap;
212
- word-break: break-word;
213
- }
214
-
215
- details summary {
216
- cursor: pointer;
217
- color: var(--text-muted);
218
- font-size: 0.85rem;
219
- margin-bottom: 10px;
220
- user-select: none;
221
- }
222
- details[open] summary { color: var(--text); }
223
-
224
- /* ── History table ──────────────────────────────────────────────── */
225
- table { width: 100%; border-collapse: collapse; margin-top: 14px; font-size: 0.85rem; }
226
- th { color: var(--text-muted); font-weight: 600; padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--border); }
227
- td { padding: 9px 10px; border-bottom: 1px solid #1e2236; vertical-align: middle; }
228
- tr:hover td { background: var(--surface2); }
229
- td.center { text-align: center; }
230
-
231
- .status-badge {
232
- border-radius: 20px;
233
- font-size: 0.72rem;
234
- font-weight: 700;
235
- letter-spacing: 0.3px;
236
- padding: 3px 9px;
237
- text-transform: uppercase;
238
- }
239
- .status-pending { background: #2d2a1a; color: var(--warning); }
240
- .status-approved { background: #162820; color: var(--success); }
241
- .status-exported { background: #1a1f36; color: var(--accent); }
242
-
243
- /* ── Utility ────────────────────────────────────────────────────── */
244
- .hidden { display: none !important; }
245
- .muted { color: var(--text-muted); font-size: 0.875rem; margin-bottom: 12px; }
246
-
247
- /* ── Toast ──────────────────────────────────────────────────────── */
248
- .toast {
249
- position: fixed;
250
- bottom: 28px;
251
- right: 28px;
252
- background: var(--surface2);
253
- border: 1px solid var(--border);
254
- border-radius: 9px;
255
- box-shadow: 0 4px 24px rgba(0,0,0,0.4);
256
- color: var(--text);
257
- font-size: 0.875rem;
258
- font-weight: 500;
259
- max-width: 340px;
260
- padding: 13px 18px;
261
- z-index: 999;
262
- animation: slideIn 0.25s ease;
263
- }
264
- .toast.success { border-color: var(--success); color: var(--success); }
265
- .toast.error { border-color: var(--danger); color: var(--danger); }
266
- @keyframes slideIn { from { opacity:0; transform: translateY(10px); } to { opacity:1; transform: translateY(0); } }
267
-
268
- /* ── Footer ──────────────────────────────────────────────────────── */
269
- footer {
270
- text-align: center;
271
- color: var(--text-muted);
272
- font-size: 0.8rem;
273
- padding: 24px 0 32px;
274
- }
275
- footer a { color: var(--accent); text-decoration: none; }
276
- footer a:hover { text-decoration: underline; }
277
-
278
- /* ── Responsive ──────────────────────────────────────────────────── */
279
- @media (max-width: 640px) {
280
- .row-two, .manifest-grid { grid-template-columns: 1fr; }
281
- .manifest-field.full { grid-column: 1; }
282
- .card { padding: 20px 16px; }
283
- main { margin: 16px auto; }
284
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ═══════════════════════════════════════════════════════════════
2
+ PROMPTFORGE — Neural Forge UI v2.0
3
+ Aesthetic: Deep Space Cyberpunk + Glassmorphism
4
+ ═══════════════════════════════════════════════════════════════ */
5
 
6
+ @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,400&family=Outfit:wght@300;400;500;600&display=swap');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ :root {
9
+ --void:#020408; --deep:#060c18;
10
+ --surface-1:rgba(8,15,32,.85); --surface-2:rgba(12,22,48,.75); --surface-3:rgba(16,28,58,.6);
11
+ --cyan:#00f5d4; --cyan-dim:rgba(0,245,212,.12); --cyan-glow:rgba(0,245,212,.4);
12
+ --magenta:#f72585; --magenta-dim:rgba(247,37,133,.12);
13
+ --violet:#7209b7; --violet-dim:rgba(114,9,183,.18); --gold:#ffd60a;
14
+ --text:#e8edf8; --text-soft:#a8b4cc; --text-muted:#5a6a8a; --text-dim:#3a4a6a;
15
+ --border:rgba(0,245,212,.1); --border-hot:rgba(0,245,212,.32);
16
+ --font-display:'Syne',sans-serif; --font-body:'Outfit',sans-serif; --font-mono:'DM Mono',monospace;
17
+ --radius-sm:8px; --radius-md:14px; --radius-lg:20px;
18
+ --shadow-card:0 0 0 1px rgba(0,245,212,.07),0 24px 64px rgba(0,0,0,.65),0 4px 16px rgba(0,0,0,.4);
19
+ --shadow-hover:0 0 0 1px rgba(0,245,212,.22),0 32px 80px rgba(0,0,0,.7),0 0 40px rgba(0,245,212,.05);
20
+ --glow-cyan:0 0 24px rgba(0,245,212,.55),0 0 60px rgba(0,245,212,.2);
21
+ }
22
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
23
+ html{scroll-behavior:smooth;scrollbar-width:thin;scrollbar-color:var(--cyan-dim) transparent}
24
+ ::-webkit-scrollbar{width:4px}::-webkit-scrollbar-thumb{background:var(--cyan-dim);border-radius:4px}
25
+ body{background:var(--void);color:var(--text);font-family:var(--font-body);font-size:15px;line-height:1.65;min-height:100vh;overflow-x:hidden;cursor:none}
26
+ @media(max-width:680px){body{cursor:auto}button{cursor:pointer!important}}
27
+
28
+ /* Cursor */
29
+ #cursor-dot{position:fixed;width:8px;height:8px;background:var(--cyan);border-radius:50%;pointer-events:none;z-index:9999;transform:translate(-50%,-50%);transition:width .2s,height .2s,background .2s;box-shadow:var(--glow-cyan)}
30
+ #cursor-ring{position:fixed;width:36px;height:36px;border:1px solid rgba(0,245,212,.45);border-radius:50%;pointer-events:none;z-index:9998;transform:translate(-50%,-50%);transition:all .1s ease}
31
+ @media(max-width:680px){#cursor-dot,#cursor-ring{display:none}}
32
+
33
+ /* Backgrounds */
34
+ .bg-mesh{position:fixed;inset:0;z-index:0;pointer-events:none;background:radial-gradient(ellipse 80% 50% at 15% 15%,rgba(0,245,212,.065) 0%,transparent 60%),radial-gradient(ellipse 60% 40% at 85% 85%,rgba(247,37,133,.05) 0%,transparent 60%),radial-gradient(ellipse 50% 60% at 60% 5%,rgba(114,9,183,.04) 0%,transparent 55%);animation:meshAnim 22s ease-in-out infinite alternate}
35
+ @keyframes meshAnim{0%{filter:hue-rotate(0deg) brightness(1)}50%{filter:hue-rotate(18deg) brightness(1.08)}100%{filter:hue-rotate(-8deg) brightness(.96)}}
36
+ .bg-grid{position:fixed;inset:0;z-index:0;pointer-events:none;background-image:linear-gradient(rgba(0,245,212,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(0,245,212,.025) 1px,transparent 1px);background-size:60px 60px;mask-image:radial-gradient(ellipse 100% 80% at 50% 0%,black 30%,transparent 75%)}
37
+ .orb{position:fixed;border-radius:50%;filter:blur(110px);pointer-events:none;z-index:0;animation:orbFloat 28s ease-in-out infinite}
38
+ .orb-1{width:700px;height:700px;background:radial-gradient(circle,rgba(0,245,212,.055) 0%,transparent 70%);top:-250px;left:-250px;animation-duration:32s}
39
+ .orb-2{width:500px;height:500px;background:radial-gradient(circle,rgba(247,37,133,.045) 0%,transparent 70%);bottom:-150px;right:-150px;animation-duration:24s;animation-delay:-12s}
40
+ .orb-3{width:400px;height:400px;background:radial-gradient(circle,rgba(114,9,183,.038) 0%,transparent 70%);top:45%;left:50%;animation-duration:36s;animation-delay:-20s}
41
+ @keyframes orbFloat{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(70px,-50px) scale(1.12)}66%{transform:translate(-50px,70px) scale(.9)}}
42
+
43
+ /* Layout */
44
+ #app{position:relative;z-index:1;min-height:100vh;display:flex;flex-direction:column}
45
+ main{max-width:900px;width:100%;margin:0 auto;padding:20px 20px 80px;display:flex;flex-direction:column;gap:20px;flex:1}
46
+
47
+ /* Header */
48
+ header{position:sticky;top:0;z-index:100;backdrop-filter:blur(28px) saturate(180%);-webkit-backdrop-filter:blur(28px) saturate(180%);background:rgba(2,4,8,.88);border-bottom:1px solid rgba(0,245,212,.07)}
49
+ .header-inner{max-width:900px;margin:0 auto;padding:13px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px}
50
+ .logo-group{display:flex;align-items:center;gap:12px}
51
+ .logo-icon{width:40px;height:40px;flex-shrink:0;background:linear-gradient(135deg,var(--cyan),var(--violet));border-radius:11px;display:grid;place-items:center;font-size:18px;box-shadow:0 0 22px rgba(0,245,212,.28);animation:logoPulse 4s ease-in-out infinite}
52
+ @keyframes logoPulse{0%,100%{box-shadow:0 0 22px rgba(0,245,212,.28)}50%{box-shadow:0 0 40px rgba(0,245,212,.5),0 0 80px rgba(0,245,212,.1)}}
53
+ .logo-text{font-family:var(--font-display);font-size:1.25rem;font-weight:800;letter-spacing:-.3px;background:linear-gradient(90deg,var(--cyan),#b8f0e6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
54
+ .logo-tag{font-family:var(--font-mono);font-size:.6rem;color:var(--cyan);background:var(--cyan-dim);border:1px solid rgba(0,245,212,.18);padding:2px 8px;border-radius:20px;letter-spacing:1px}
55
+ .header-meta{display:flex;align-items:center;gap:14px}
56
+ .status-pill{display:flex;align-items:center;gap:7px;font-family:var(--font-mono);font-size:.72rem;color:var(--text-muted);background:rgba(0,245,212,.04);border:1px solid var(--border);padding:5px 12px;border-radius:20px}
57
+ .status-dot{width:6px;height:6px;border-radius:50%;background:var(--cyan);box-shadow:0 0 8px var(--cyan);animation:blink 2.2s ease-in-out infinite}
58
+ @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
59
+ .nav-link{font-family:var(--font-mono);font-size:.72rem;color:var(--text-muted);text-decoration:none;display:flex;align-items:center;gap:5px;padding:5px 12px;border:1px solid var(--border);border-radius:6px;transition:all .2s}
60
+ .nav-link:hover{color:var(--cyan);border-color:var(--border-hot);background:var(--cyan-dim)}
61
+
62
+ /* Step Progress */
63
+ .step-progress{display:flex;align-items:center;justify-content:center;padding:18px 0 2px;gap:0}
64
+ .prog-step{display:flex;flex-direction:column;align-items:center;gap:5px}
65
+ .prog-node{width:30px;height:30px;border-radius:50%;border:1px solid var(--border);background:var(--surface-1);display:grid;place-items:center;font-family:var(--font-mono);font-size:.7rem;color:var(--text-muted);transition:all .4s cubic-bezier(.34,1.56,.64,1);position:relative;z-index:1}
66
+ .prog-step.active .prog-node{border-color:var(--cyan);background:var(--cyan-dim);color:var(--cyan);box-shadow:0 0 18px rgba(0,245,212,.4);transform:scale(1.18)}
67
+ .prog-step.done .prog-node{border-color:var(--cyan);background:var(--cyan);color:var(--void);font-weight:700}
68
+ .prog-label{font-size:.6rem;color:var(--text-dim);font-family:var(--font-mono);letter-spacing:.5px;text-transform:uppercase}
69
+ .prog-step.active .prog-label{color:var(--cyan)}.prog-step.done .prog-label{color:var(--text-muted)}
70
+ .prog-line{width:55px;height:1px;background:var(--border);margin-bottom:20px;position:relative;overflow:hidden}
71
+ .prog-line::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,var(--cyan),var(--magenta));transform:scaleX(0);transform-origin:left;transition:transform .6s ease}
72
+ .prog-line.filled::after{transform:scaleX(1)}
73
+
74
+ /* Cards */
75
+ .card{backdrop-filter:blur(20px) saturate(160%);-webkit-backdrop-filter:blur(20px) saturate(160%);background:var(--surface-1);border:1px solid var(--border);border-radius:var(--radius-lg);padding:30px 34px;box-shadow:var(--shadow-card);position:relative;overflow:hidden;transition:box-shadow .4s,border-color .4s,transform .3s;animation:cardIn .5s cubic-bezier(.22,1,.36,1) both}
76
+ @keyframes cardIn{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}
77
+ .card:hover{box-shadow:var(--shadow-hover);border-color:rgba(0,245,212,.16)}
78
+ .card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(0,245,212,.5),transparent);opacity:.6}
79
+ .card::after{content:'';position:absolute;top:-50px;right:-50px;width:130px;height:130px;border-radius:50%;background:radial-gradient(circle,rgba(0,245,212,.035) 0%,transparent 70%);pointer-events:none}
80
+ .card-header{display:flex;align-items:center;gap:12px;margin-bottom:26px}
81
+ .card-header h2{font-family:var(--font-display);font-size:1rem;font-weight:700;color:var(--text);letter-spacing:-.2px;flex:1}
82
+ .step-badge{font-family:var(--font-mono);font-size:.58rem;font-weight:500;letter-spacing:1.5px;text-transform:uppercase;color:var(--void);background:linear-gradient(90deg,var(--cyan),#00c4a8);padding:3px 10px;border-radius:20px;box-shadow:0 0 12px rgba(0,245,212,.38)}
83
+
84
+ /* Forms */
85
+ .field{margin-bottom:20px}
86
+ label{display:flex;align-items:center;gap:7px;font-family:var(--font-mono);font-size:.7rem;font-weight:500;letter-spacing:1px;text-transform:uppercase;color:var(--text-muted);margin-bottom:7px}
87
+ .lbl-dot{width:4px;height:4px;border-radius:50%;background:var(--cyan);box-shadow:0 0 6px var(--cyan)}
88
+ .lbl-opt{color:var(--text-dim);font-size:.62rem;margin-left:auto;letter-spacing:.5px;text-transform:none}
89
+ textarea,input[type="password"],select{width:100%;background:rgba(4,9,20,.85);border:1px solid rgba(0,245,212,.1);border-radius:var(--radius-md);color:var(--text);font-family:var(--font-body);font-size:.9rem;padding:12px 16px;resize:vertical;outline:none;transition:border-color .3s,box-shadow .3s,background .3s;box-shadow:inset 0 1px 0 rgba(255,255,255,.02),0 1px 4px rgba(0,0,0,.35)}
90
+ textarea:focus,input:focus,select:focus{border-color:var(--cyan);background:rgba(0,245,212,.025);box-shadow:0 0 0 3px rgba(0,245,212,.07),0 0 20px rgba(0,245,212,.08)}
91
+ textarea::placeholder,input::placeholder{color:var(--text-dim);font-style:italic}
92
+ textarea{min-height:110px;line-height:1.7}
93
+ select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='11' height='7' viewBox='0 0 11 7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5.5 6L10 1' stroke='%2300f5d4' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;padding-right:38px}
94
+ select option{background:#06101e}
95
+ .input-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
96
+ .field-note{font-size:.72rem;color:var(--text-dim);margin-top:5px;font-family:var(--font-mono)}
97
+
98
+ /* Buttons */
99
+ button{cursor:none}
100
+ .btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:none;border-radius:var(--radius-md);font-family:var(--font-display);font-size:.9rem;font-weight:700;letter-spacing:.3px;padding:13px 28px;position:relative;overflow:hidden;background:linear-gradient(135deg,var(--cyan) 0%,#00d4b4 40%,#00a88e 70%,var(--violet) 100%);background-size:250% 250%;color:var(--void);animation:gradFlow 5s ease infinite;transition:transform .3s cubic-bezier(.34,1.56,.64,1),box-shadow .3s}
101
+ @keyframes gradFlow{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
102
+ .btn-primary::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,rgba(255,255,255,.2),transparent);opacity:0;transition:opacity .3s}
103
+ .btn-primary::after{content:'';position:absolute;top:0;left:-100%;width:55%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.22),transparent);transform:skewX(-20deg)}
104
+ .btn-primary:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 10px 32px var(--cyan-glow),0 0 60px rgba(0,245,212,.12)}
105
+ .btn-primary:hover::before{opacity:1}
106
+ .btn-primary:hover::after{animation:shimmer .65s ease forwards}
107
+ @keyframes shimmer{to{left:150%}}
108
+ .btn-primary:active{transform:translateY(0) scale(.99)}
109
+ .btn-primary:disabled{opacity:.6;transform:none;cursor:not-allowed;animation:none}
110
+ .btn-secondary{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface-2);color:var(--text-soft);font-family:var(--font-body);font-size:.875rem;font-weight:500;padding:11px 22px;transition:all .25s;backdrop-filter:blur(8px);position:relative;overflow:hidden}
111
+ .btn-secondary::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,var(--cyan-dim),transparent);opacity:0;transition:opacity .3s}
112
+ .btn-secondary:hover{border-color:var(--border-hot);color:var(--cyan);transform:translateY(-1px);box-shadow:0 4px 18px rgba(0,245,212,.1)}
113
+ .btn-secondary:hover::before{opacity:1}
114
+ .btn-danger:hover{border-color:var(--magenta);color:var(--magenta);background:var(--magenta-dim);box-shadow:0 4px 18px rgba(247,37,133,.12)}
115
+ .btn-sm{font-size:.76rem;padding:6px 13px;border-radius:var(--radius-sm)}
116
+ .action-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:22px;align-items:center}
117
+ .btn-copy{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:8px;background:var(--surface-3);color:var(--text-muted);font-family:var(--font-mono);font-size:.73rem;padding:7px 14px;margin-top:10px;transition:all .25s}
118
+ .btn-copy:hover{color:var(--cyan);border-color:var(--border-hot);background:var(--cyan-dim)}
119
+ .btn-copy.copied{color:var(--cyan);border-color:var(--cyan);background:var(--cyan-dim)}
120
+
121
+ /* Manifest */
122
+ .manifest-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:22px}
123
+ .manifest-field{display:flex;flex-direction:column;gap:5px}
124
+ .manifest-field.full{grid-column:1/-1}
125
+ .manifest-field label{font-size:.65rem;letter-spacing:1.5px;color:var(--cyan)}
126
+ .manifest-field textarea,.manifest-field input{font-size:.84rem;min-height:62px;border-radius:var(--radius-sm);padding:10px 13px}
127
+
128
+ /* Details */
129
+ details{border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;transition:border-color .3s}
130
+ details[open]{border-color:rgba(0,245,212,.18)}
131
+ details summary{padding:11px 16px;cursor:none;font-family:var(--font-mono);font-size:.78rem;color:var(--text-muted);background:var(--surface-2);display:flex;align-items:center;gap:8px;list-style:none;user-select:none;transition:all .2s}
132
+ details summary::-webkit-details-marker{display:none}
133
+ details summary::before{content:'▶';font-size:.5rem;color:var(--cyan);transition:transform .3s}
134
+ details[open] summary::before{transform:rotate(90deg)}
135
+ details summary:hover{color:var(--text);background:var(--surface-3)}
136
+
137
+ /* Pre */
138
+ pre{background:rgba(2,5,12,.92);border:1px solid rgba(0,245,212,.06);border-radius:var(--radius-md);color:#a0d0c4;font-family:var(--font-mono);font-size:.79rem;line-height:1.6;overflow:auto;padding:20px;white-space:pre-wrap;word-break:break-word;max-height:420px;position:relative}
139
+ pre::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(0,245,212,.25),transparent)}
140
+
141
+ /* Tabs */
142
+ .tab-bar{display:flex;gap:4px;margin-bottom:16px;background:rgba(4,9,20,.7);padding:4px;border-radius:var(--radius-md);border:1px solid var(--border);width:fit-content}
143
+ .tab{border:none;border-radius:9px;background:none;color:var(--text-muted);font-family:var(--font-mono);font-size:.76rem;letter-spacing:.5px;padding:7px 18px;transition:all .25s}
144
+ .tab.active{background:linear-gradient(135deg,rgba(0,245,212,.18),rgba(0,245,212,.06));color:var(--cyan);border:1px solid rgba(0,245,212,.22);box-shadow:0 0 14px rgba(0,245,212,.14)}
145
+ .tab:not(.active):hover{color:var(--text-soft)}
146
+
147
+ /* Table */
148
+ .table-wrap{margin-top:14px;border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden}
149
+ table{width:100%;border-collapse:collapse;font-size:.82rem}
150
+ thead{background:rgba(0,245,212,.03);border-bottom:1px solid var(--border)}
151
+ th{padding:11px 15px;text-align:left;font-family:var(--font-mono);font-size:.64rem;letter-spacing:1px;text-transform:uppercase;color:var(--text-muted);font-weight:400}
152
+ td{padding:11px 15px;border-bottom:1px solid rgba(0,245,212,.03);vertical-align:middle;color:var(--text-soft);transition:background .2s}
153
+ tr:last-child td{border-bottom:none}
154
+ tbody tr:hover td{background:rgba(0,245,212,.025)}
155
+ td code{font-family:var(--font-mono);font-size:.76rem;color:var(--cyan);background:var(--cyan-dim);padding:2px 8px;border-radius:4px}
156
+ td.empty-msg{padding:36px;text-align:center;color:var(--text-dim);font-style:italic}
157
+
158
+ /* Badges */
159
+ .badge{display:inline-flex;align-items:center;gap:5px;border-radius:20px;font-family:var(--font-mono);font-size:.64rem;letter-spacing:.5px;text-transform:uppercase;padding:3px 10px}
160
+ .badge::before{content:'';width:5px;height:5px;border-radius:50%;flex-shrink:0}
161
+ .badge-pending{background:rgba(255,214,10,.1);color:var(--gold);border:1px solid rgba(255,214,10,.2)}
162
+ .badge-pending::before{background:var(--gold);box-shadow:0 0 5px var(--gold)}
163
+ .badge-approved{background:rgba(0,245,212,.1);color:var(--cyan);border:1px solid rgba(0,245,212,.2)}
164
+ .badge-approved::before{background:var(--cyan);box-shadow:0 0 5px var(--cyan)}
165
+ .badge-exported{background:rgba(247,37,133,.1);color:var(--magenta);border:1px solid rgba(247,37,133,.2)}
166
+ .badge-exported::before{background:var(--magenta);box-shadow:0 0 5px var(--magenta)}
167
+
168
+ /* Info banner */
169
+ .info-banner{display:flex;align-items:flex-start;gap:11px;background:rgba(0,245,212,.04);border:1px solid rgba(0,245,212,.1);border-radius:var(--radius-md);padding:13px 16px;font-size:.83rem;color:var(--text-soft);margin-bottom:20px}
170
+ .info-icon{font-size:15px;flex-shrink:0;margin-top:1px}
171
+ .divider{height:1px;background:linear-gradient(90deg,transparent,var(--border),transparent);margin:22px 0}
172
+
173
+ /* Toasts */
174
+ #toast-container{position:fixed;bottom:26px;right:26px;z-index:9990;display:flex;flex-direction:column;gap:9px;pointer-events:none}
175
+ .toast{pointer-events:all;backdrop-filter:blur(22px);background:rgba(6,12,24,.96);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:0 8px 36px rgba(0,0,0,.55);display:flex;align-items:center;gap:11px;font-size:.84rem;max-width:360px;padding:13px 16px;animation:toastIn .35s cubic-bezier(.22,1,.36,1) both}
176
+ .toast.leaving{animation:toastOut .3s ease forwards}
177
+ .toast-icon{width:28px;height:28px;border-radius:8px;display:grid;place-items:center;font-size:13px;flex-shrink:0}
178
+ .toast.success{border-color:rgba(0,245,212,.22)}.toast.success .toast-icon{background:var(--cyan-dim)}
179
+ .toast.error{border-color:rgba(247,37,133,.22)}.toast.error .toast-icon{background:var(--magenta-dim)}
180
+ @keyframes toastIn{from{opacity:0;transform:translateX(16px) scale(.96)}to{opacity:1;transform:none}}
181
+ @keyframes toastOut{to{opacity:0;transform:translateX(16px) scale(.96);max-height:0;padding:0;overflow:hidden}}
182
+
183
+ /* Footer */
184
+ footer{position:relative;z-index:1;border-top:1px solid rgba(0,245,212,.05);padding:22px 20px}
185
+ .footer-inner{max-width:900px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
186
+ .footer-copy{font-family:var(--font-mono);font-size:.7rem;color:var(--text-dim)}
187
+ .footer-links{display:flex;gap:16px}
188
+ .footer-links a{font-family:var(--font-mono);font-size:.7rem;color:var(--text-muted);text-decoration:none;transition:color .2s}
189
+ .footer-links a:hover{color:var(--cyan)}
190
+
191
+ /* Utility */
192
+ .hidden{display:none!important}
193
+ .muted{color:var(--text-muted);font-size:.84rem;margin-bottom:14px;line-height:1.55}
194
+ .fade-in{animation:fadeIn .45s cubic-bezier(.22,1,.36,1) both}
195
+ @keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
196
+
197
+ /* Responsive */
198
+ @media(max-width:680px){.input-grid,.manifest-grid{grid-template-columns:1fr}.manifest-field.full{grid-column:1}.card{padding:22px 18px}main{padding:14px 14px 60px}.prog-line{width:28px}.footer-inner{flex-direction:column;align-items:center}}
199
+ @media(max-width:420px){.header-meta{display:none}}
frontend/style.css.bak ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* PromptForge — style.css */
2
+
3
+ :root {
4
+ --bg: #0f1117;
5
+ --surface: #1a1d27;
6
+ --surface2: #22263a;
7
+ --border: #2e3350;
8
+ --accent: #5b8df6;
9
+ --accent-hover: #3d6fe0;
10
+ --success: #34d399;
11
+ --warning: #fbbf24;
12
+ --danger: #f87171;
13
+ --text: #e4e7f0;
14
+ --text-muted: #7c84a3;
15
+ --radius: 10px;
16
+ --font: 'Inter', system-ui, sans-serif;
17
+ --mono: 'Fira Code', 'Cascadia Code', monospace;
18
+ }
19
+
20
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
21
+
22
+ body {
23
+ background: var(--bg);
24
+ color: var(--text);
25
+ font-family: var(--font);
26
+ font-size: 15px;
27
+ line-height: 1.6;
28
+ min-height: 100vh;
29
+ }
30
+
31
+ /* ── Header ──────────────────────────────────────────────────── */
32
+ header {
33
+ background: linear-gradient(135deg, #1e2336 0%, #151824 100%);
34
+ border-bottom: 1px solid var(--border);
35
+ padding: 14px 24px;
36
+ position: sticky;
37
+ top: 0;
38
+ z-index: 100;
39
+ backdrop-filter: blur(6px);
40
+ }
41
+ .header-inner { display: flex; align-items: center; gap: 16px; max-width: 960px; margin: 0 auto; }
42
+ .logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.3px; }
43
+ .tagline { color: var(--text-muted); font-size: 0.85rem; }
44
+
45
+ /* ── Main layout ──────────────────────────────────────────────── */
46
+ main {
47
+ max-width: 960px;
48
+ margin: 32px auto;
49
+ padding: 0 20px;
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 24px;
53
+ }
54
+
55
+ /* ── Cards ──────────────────────────────────────────────────────── */
56
+ .card {
57
+ background: var(--surface);
58
+ border: 1px solid var(--border);
59
+ border-radius: var(--radius);
60
+ padding: 28px 30px;
61
+ }
62
+ .card h2 {
63
+ font-size: 1.05rem;
64
+ font-weight: 600;
65
+ margin-bottom: 18px;
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 10px;
69
+ }
70
+ .step-badge {
71
+ background: var(--accent);
72
+ color: #fff;
73
+ font-size: 0.65rem;
74
+ font-weight: 700;
75
+ letter-spacing: 0.5px;
76
+ padding: 3px 8px;
77
+ border-radius: 20px;
78
+ }
79
+
80
+ /* ── Form elements ─────────────────────────────────────────────── */
81
+ label {
82
+ display: block;
83
+ margin-bottom: 6px;
84
+ font-size: 0.875rem;
85
+ color: var(--text-muted);
86
+ font-weight: 500;
87
+ }
88
+ textarea, input[type="password"], select {
89
+ width: 100%;
90
+ background: var(--surface2);
91
+ border: 1px solid var(--border);
92
+ border-radius: 7px;
93
+ color: var(--text);
94
+ font-family: var(--font);
95
+ font-size: 0.9rem;
96
+ padding: 10px 14px;
97
+ resize: vertical;
98
+ transition: border-color 0.2s;
99
+ margin-bottom: 16px;
100
+ }
101
+ textarea:focus, input:focus, select:focus {
102
+ outline: none;
103
+ border-color: var(--accent);
104
+ }
105
+ select option { background: var(--surface2); }
106
+
107
+ .row-two {
108
+ display: grid;
109
+ grid-template-columns: 1fr 1fr;
110
+ gap: 16px;
111
+ }
112
+ .field-group { display: flex; flex-direction: column; }
113
+ .field-group label { margin-bottom: 6px; }
114
+ .field-group textarea,
115
+ .field-group input,
116
+ .field-group select { margin-bottom: 0; }
117
+
118
+ /* ── Buttons ────────────────────────────────────────────────────── */
119
+ .btn-primary, .btn-secondary {
120
+ display: inline-flex;
121
+ align-items: center;
122
+ gap: 6px;
123
+ border: none;
124
+ border-radius: 7px;
125
+ cursor: pointer;
126
+ font-family: var(--font);
127
+ font-size: 0.9rem;
128
+ font-weight: 600;
129
+ padding: 10px 20px;
130
+ transition: background 0.2s, transform 0.1s;
131
+ }
132
+ .btn-primary {
133
+ background: var(--accent);
134
+ color: #fff;
135
+ }
136
+ .btn-primary:hover { background: var(--accent-hover); transform: translateY(-1px); }
137
+ .btn-primary:active { transform: translateY(0); }
138
+
139
+ .btn-secondary {
140
+ background: var(--surface2);
141
+ color: var(--text);
142
+ border: 1px solid var(--border);
143
+ }
144
+ .btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
145
+
146
+ .btn-sm { font-size: 0.8rem; padding: 7px 14px; }
147
+
148
+ .btn-copy {
149
+ background: var(--surface2);
150
+ border: 1px solid var(--border);
151
+ border-radius: 6px;
152
+ color: var(--text-muted);
153
+ cursor: pointer;
154
+ font-size: 0.8rem;
155
+ margin-top: 8px;
156
+ padding: 6px 14px;
157
+ transition: color 0.2s;
158
+ }
159
+ .btn-copy:hover { color: var(--success); border-color: var(--success); }
160
+
161
+ .action-row {
162
+ display: flex;
163
+ flex-wrap: wrap;
164
+ gap: 10px;
165
+ margin-top: 20px;
166
+ }
167
+
168
+ /* ── Manifest grid ──────────────────────────────────────────────── */
169
+ .manifest-grid {
170
+ display: grid;
171
+ grid-template-columns: 1fr 1fr;
172
+ gap: 16px;
173
+ margin-bottom: 20px;
174
+ }
175
+ .manifest-field { display: flex; flex-direction: column; }
176
+ .manifest-field.full { grid-column: 1 / -1; }
177
+ .manifest-field label { font-size: 0.78rem; color: var(--accent); text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 4px; }
178
+ .manifest-field textarea,
179
+ .manifest-field input {
180
+ margin-bottom: 0;
181
+ font-size: 0.85rem;
182
+ min-height: 70px;
183
+ }
184
+
185
+ /* ── Tabs ───────────────────────────────────────────────────────── */
186
+ .tab-bar { display: flex; gap: 4px; margin-bottom: 14px; }
187
+ .tab {
188
+ background: none;
189
+ border: 1px solid var(--border);
190
+ border-radius: 6px;
191
+ color: var(--text-muted);
192
+ cursor: pointer;
193
+ font-family: var(--font);
194
+ font-size: 0.85rem;
195
+ padding: 7px 16px;
196
+ transition: all 0.2s;
197
+ }
198
+ .tab.active { background: var(--accent); border-color: var(--accent); color: #fff; }
199
+
200
+ /* ── Pre / code blocks ──────────────────────────────────────────── */
201
+ pre {
202
+ background: #0a0c14;
203
+ border: 1px solid var(--border);
204
+ border-radius: 8px;
205
+ color: #c9d1d9;
206
+ font-family: var(--mono);
207
+ font-size: 0.82rem;
208
+ line-height: 1.5;
209
+ overflow-x: auto;
210
+ padding: 16px 18px;
211
+ white-space: pre-wrap;
212
+ word-break: break-word;
213
+ }
214
+
215
+ details summary {
216
+ cursor: pointer;
217
+ color: var(--text-muted);
218
+ font-size: 0.85rem;
219
+ margin-bottom: 10px;
220
+ user-select: none;
221
+ }
222
+ details[open] summary { color: var(--text); }
223
+
224
+ /* ── History table ──────────────────────────────────────────────── */
225
+ table { width: 100%; border-collapse: collapse; margin-top: 14px; font-size: 0.85rem; }
226
+ th { color: var(--text-muted); font-weight: 600; padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--border); }
227
+ td { padding: 9px 10px; border-bottom: 1px solid #1e2236; vertical-align: middle; }
228
+ tr:hover td { background: var(--surface2); }
229
+ td.center { text-align: center; }
230
+
231
+ .status-badge {
232
+ border-radius: 20px;
233
+ font-size: 0.72rem;
234
+ font-weight: 700;
235
+ letter-spacing: 0.3px;
236
+ padding: 3px 9px;
237
+ text-transform: uppercase;
238
+ }
239
+ .status-pending { background: #2d2a1a; color: var(--warning); }
240
+ .status-approved { background: #162820; color: var(--success); }
241
+ .status-exported { background: #1a1f36; color: var(--accent); }
242
+
243
+ /* ── Utility ────────────────────────────────────────────────────── */
244
+ .hidden { display: none !important; }
245
+ .muted { color: var(--text-muted); font-size: 0.875rem; margin-bottom: 12px; }
246
+
247
+ /* ── Toast ──────────────────────────────────────────────────────── */
248
+ .toast {
249
+ position: fixed;
250
+ bottom: 28px;
251
+ right: 28px;
252
+ background: var(--surface2);
253
+ border: 1px solid var(--border);
254
+ border-radius: 9px;
255
+ box-shadow: 0 4px 24px rgba(0,0,0,0.4);
256
+ color: var(--text);
257
+ font-size: 0.875rem;
258
+ font-weight: 500;
259
+ max-width: 340px;
260
+ padding: 13px 18px;
261
+ z-index: 999;
262
+ animation: slideIn 0.25s ease;
263
+ }
264
+ .toast.success { border-color: var(--success); color: var(--success); }
265
+ .toast.error { border-color: var(--danger); color: var(--danger); }
266
+ @keyframes slideIn { from { opacity:0; transform: translateY(10px); } to { opacity:1; transform: translateY(0); } }
267
+
268
+ /* ── Footer ──────────────────────────────────────────────────────── */
269
+ footer {
270
+ text-align: center;
271
+ color: var(--text-muted);
272
+ font-size: 0.8rem;
273
+ padding: 24px 0 32px;
274
+ }
275
+ footer a { color: var(--accent); text-decoration: none; }
276
+ footer a:hover { text-decoration: underline; }
277
+
278
+ /* ── Responsive ──────────────────────────────────────────────────── */
279
+ @media (max-width: 640px) {
280
+ .row-two, .manifest-grid { grid-template-columns: 1fr; }
281
+ .manifest-field.full { grid-column: 1; }
282
+ .card { padding: 20px 16px; }
283
+ main { margin: 16px auto; }
284
+ }