diff --git a/app.js b/app.js index 4b5540b..6ca4d98 100644 --- a/app.js +++ b/app.js @@ -1,18 +1,12 @@ 'use strict'; -// ============================================================ -// Config -// ============================================================ - const express = require('express'); -const fetch = require('node-fetch'); const path = require('path'); const fs = require('fs'); const PORT = 8195; const HOST = '0.0.0.0'; -// Config file stored alongside app.js const CONFIG_FILE = path.join(__dirname, 'config.json'); function loadConfig() { @@ -27,22 +21,15 @@ function saveConfig(cfg) { fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2)); } -// ============================================================ -// App -// ============================================================ - const app = express(); app.use(express.json()); app.use(express.static(__dirname)); -// --- Settings --- +// ── Config ────────────────────────────────────────────────────── app.get('/api/config', (req, res) => { const cfg = loadConfig(); - res.json({ - hasApiKey: !!cfg.apiKey, - baseUrl: cfg.baseUrl || 'https://api.minimax.io', - }); + res.json({ hasApiKey: !!cfg.apiKey, baseUrl: cfg.baseUrl || 'https://api.minimaxi.com' }); }); app.post('/api/config', (req, res) => { @@ -50,66 +37,135 @@ app.post('/api/config', (req, res) => { if (typeof apiKey !== 'string' || typeof baseUrl !== 'string') { return res.status(400).json({ error: '参数格式错误' }); } - const cfg = { apiKey: apiKey.trim(), baseUrl: baseUrl.trim() || 'https://api.minimax.io' }; + const cfg = { apiKey: apiKey.trim(), baseUrl: baseUrl.trim() || 'https://api.minimaxi.com' }; saveConfig(cfg); res.json({ ok: true }); }); -// --- Image generation --- +// ── Image Generation ───────────────────────────────────────────── app.post('/api/generate', async (req, res) => { - const { prompt, aspect_ratio, model } = req.body; + const { + model, prompt, style, aspect_ratio, + width, height, response_format, seed, + n, prompt_optimizer, aigc_watermark, + } = req.body; + // Validation if (!prompt || typeof prompt !== 'string' || !prompt.trim()) { - return res.status(400).json({ error: '请输入图片描述内容' }); + return res.status(400).json({ error: '请输入图片描述' }); + } + if (model !== 'image-01' && model !== 'image-01-live') { + return res.status(400).json({ error: 'model 参数无效' }); } const cfg = loadConfig(); if (!cfg.apiKey) { - return res.status(400).json({ error: '未配置 API Key,请先在设置中填写 MiniMax API Key。' }); + return res.status(400).json({ error: '未配置 API Key,请先在设置中填写。' }); } - const baseUrl = cfg.baseUrl || 'https://api.minimax.io'; - const endpoint = `${baseUrl}/v1/image_generation`; - + // Build payload — only include optional fields that are set const payload = { - model: model || 'image-01', - prompt: prompt.trim(), - aspect_ratio: aspect_ratio || '1:1', - response_format: 'base64', + model, + prompt: prompt.trim(), + response_format: response_format || 'url', }; + if (aspect_ratio) payload.aspect_ratio = aspect_ratio; + if (width && height) { payload.width = width; payload.height = height; } + if (seed) payload.seed = seed; + if (n) payload.n = Math.min(Math.max(Number(n), 1), 9); + if (prompt_optimizer) payload.prompt_optimizer = true; + if (aigc_watermark) payload.aigc_watermark = true; + + // style only for image-01-live + if (model === 'image-01-live' && style) { + const { style_type, style_weight } = style; + if (style_type) { + payload.style = { style_type }; + if (style_weight != null) payload.style.style_weight = style_weight; + } + } + + const baseUrl = cfg.baseUrl || 'https://api.minimaxi.com'; + const endpoint = `${baseUrl}/v1/image_generation`; + + let response; try { - const response = await fetch(endpoint, { - method: 'POST', + response = await fetch(endpoint, { + method: 'POST', headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); - - const data = await response.json(); - - if (!response.ok) { - const msg = data.error?.message || data.error || `HTTP ${response.status}`; - return res.status(response.status).json({ error: msg }); - } - - // Return base64 images - const images = data.data?.image_base64 || []; - res.json({ images }); } catch (err) { - console.error('[generate] error:', err.message); - res.status(500).json({ error: '无法连接 MiniMax API:' + err.message }); + console.error('[generate] network error:', err.message); + return res.status(502).json({ error: '无法连接 MiniMax API:' + err.message }); + } + + const data = await response.json(); + + if (!response.ok) { + const code = data.base_resp?.status_code; + const msg = data.base_resp?.status_msg || data.error || `HTTP ${response.status}`; + const friendly = friendlyError(code, msg); + return res.status(response.status).json({ error: friendly }); + } + + // Return images + task id + metadata + res.json({ + images: data.data?.image_urls || data.data?.image_base64 || [], + id: data.id, + success_count: data.metadata?.success_count ?? (Array.isArray(data.data?.image_urls || data.data?.image_base64) ? 1 : 0), + failed_count: data.metadata?.failed_count ?? 0, + }); +}); + +// ── Task Status (future-proofing) ─────────────────────────────── + +app.get('/api/task/:id', async (req, res) => { + const cfg = loadConfig(); + if (!cfg.apiKey) { + return res.status(400).json({ error: '未配置 API Key。' }); + } + const baseUrl = cfg.baseUrl || 'https://api.minimaxi.com'; + try { + const response = await fetch(`${baseUrl}/v1/image_generation/${req.params.id}`, { + headers: { 'Authorization': `Bearer ${cfg.apiKey}` }, + }); + const data = await response.json(); + if (!response.ok) { + return res.status(response.status).json({ error: data.base_resp?.status_msg || '请求失败' }); + } + res.json(data); + } catch (err) { + res.status(502).json({ error: err.message }); } }); -// --- Serve index.html at root --- +// ── SPA fallback ─────────────────────────────────────────────── + app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); }); app.listen(PORT, HOST, () => { - console.log(`Image Generator running at http://${HOST === '0.0.0.0' ? '10.0.10.110' : HOST}:${PORT}`); + console.log(`图片生成器运行于 http://${HOST === '0.0.0.0' ? '10.0.10.110' : HOST}:${PORT}`); }); + +// ── Helpers ───────────────────────────────────────────────────── + +function friendlyError(code, msg) { + const map = { + 1002: '触发限流,请稍后再试', + 1004: '账号鉴权失败,请检查 API Key 是否正确', + 1008: '账号余额不足', + 1026: '图片描述涉及敏感内容,请修改后重试', + 2013: '参数异常,请检查输入是否合规', + 2049: '无效的 API Key', + }; + if (code && map[code]) return map[code]; + return msg; +} diff --git a/index.html b/index.html index e73bac1..2df5bb7 100644 --- a/index.html +++ b/index.html @@ -34,14 +34,28 @@ 未配置 API Key,请先在 中填写。 -
+ + +
+ + +
+
+ +
@@ -50,64 +64,122 @@
-
-
- - + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + + +
+
+ + +
+ + +
+
- -
+ + +
+ + +
+ +
+ + × + +
+

同时设置宽高时优先于比例

+
+ + + + + +
+ + +
+ +
+
+ + + + - + - -
+ @@ -128,12 +200,11 @@ -

请从 platform.minimaxi.com 获取 API Key

+

请从 platform.minimaxi.com 获取

@@ -143,7 +214,7 @@
@@ -151,75 +222,115 @@ + + diff --git a/style.css b/style.css index 7737d24..288c9ad 100644 --- a/style.css +++ b/style.css @@ -1,12 +1,6 @@ -/* ============================================================ - Reset & Base - ============================================================ */ +/* ── Reset & Variables ──────────────────────────────────────── */ -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } :root { --bg: #0f0f13; @@ -38,26 +32,22 @@ button { cursor: pointer; font-family: inherit; } a { color: var(--accent2); text-decoration: none; } a:hover { text-decoration: underline; } -/* ============================================================ - Layout - ============================================================ */ +/* ── Layout ──────────────────────────────────────────────────── */ #app { - max-width: 640px; + max-width: 860px; margin: 0 auto; - padding: 0 20px 60px; + padding: 0 24px 80px; min-height: 100vh; } -/* ============================================================ - Header - ============================================================ */ +/* ── Header ──────────────────────────────────────────────────── */ header { display: flex; align-items: center; justify-content: space-between; - padding: 22px 0 28px; + padding: 22px 0 24px; } .logo { @@ -69,30 +59,23 @@ header { color: var(--text); letter-spacing: -0.3px; } +.logo svg { color: var(--accent2); } -.logo svg { - color: var(--accent2); - flex-shrink: 0; -} - -/* ============================================================ - Alert banner - ============================================================ */ +/* ── Alert ───────────────────────────────────────────────────── */ .alert-banner { display: flex; align-items: center; gap: 8px; padding: 10px 14px; - background: rgba(124, 106, 245, 0.1); - border: 1px solid rgba(124, 106, 245, 0.25); + background: rgba(124,106,245,0.1); + border: 1px solid rgba(124,106,245,0.25); border-radius: var(--radius2); color: var(--text2); font-size: 13px; margin-bottom: 20px; animation: fadeSlideIn 0.4s var(--ease); } - .alert-banner svg { color: var(--accent2); flex-shrink: 0; } .link-btn { @@ -105,9 +88,53 @@ header { cursor: pointer; } -/* ============================================================ - Form - ============================================================ */ +/* ── Model Tabs ──────────────────────────────────────────────── */ + +.model-tabs { + display: flex; + gap: 8px; + margin-bottom: 20px; +} + +.model-tab { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius2); + padding: 10px 14px; + color: var(--text2); + transition: all 0.2s; +} + +.model-tab:hover { + border-color: rgba(124,106,245,0.3); + color: var(--text); +} + +.model-tab.active { + background: rgba(124,106,245,0.12); + border-color: rgba(124,106,245,0.5); + color: var(--text); +} + +.tab-name { + font-size: 13px; + font-weight: 600; + font-family: 'Courier New', monospace; +} + +.tab-desc { + font-size: 11px; + color: var(--text3); +} + +.model-tab.active .tab-desc { color: var(--accent2); } + +/* ── Prompt ──────────────────────────────────────────────────── */ .prompt-row { position: relative; @@ -115,11 +142,9 @@ header { border: 1px solid var(--border); border-radius: var(--radius); transition: border-color 0.2s; + margin-bottom: 16px; } - -.prompt-row:focus-within { - border-color: rgba(124, 106, 245, 0.4); -} +.prompt-row:focus-within { border-color: rgba(124,106,245,0.4); } textarea { width: 100%; @@ -130,11 +155,11 @@ textarea { font-family: inherit; font-size: 15px; line-height: 1.6; - padding: 14px 16px 10px; - resize: none; + padding: 14px 16px 8px; + resize: vertical; border-radius: var(--radius); + min-height: 100px; } - textarea::placeholder { color: var(--text3); } .prompt-footer { @@ -142,199 +167,337 @@ textarea::placeholder { color: var(--text3); } justify-content: flex-end; padding: 0 14px 10px; } +.char-count { font-size: 11px; color: var(--text3); font-variant-numeric: tabular-nums; } -.char-count { - font-size: 11px; - color: var(--text3); - font-variant-numeric: tabular-nums; +/* ── Options Grid ────────────────────────────────────────────── */ + +.options-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 16px; } -/* ============================================================ - Controls row - ============================================================ */ - -.controls { +.options-col { display: flex; - align-items: flex-end; + flex-direction: column; gap: 12px; - margin-top: 14px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; } -.control-group { +.opt-group { display: flex; flex-direction: column; gap: 6px; - flex: 1; } -.control-group label { +.opt-label { font-size: 12px; - color: var(--text2); font-weight: 500; - padding-left: 2px; + color: var(--text2); } +.opt-hint { + font-weight: 400; + color: var(--text3); + font-size: 11px; +} + +.opt-note { + font-size: 11px; + color: var(--text3); + font-style: italic; +} + +/* ── Select ─────────────────────────────────────────────────── */ + select { - background: var(--surface); + background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius2); color: var(--text); font-family: inherit; font-size: 14px; - padding: 9px 12px; + padding: 8px 30px 8px 10px; outline: none; cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238a8a9a' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; - background-position: right 10px center; - padding-right: 30px; + background-position: right 8px center; + transition: border-color 0.2s; + width: 100%; +} +select:focus { border-color: rgba(124,106,245,0.4); } + +/* ── Number / Text inputs ────────────────────────────────────── */ + +input[type="number"], +input[type="text"] { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius2); + color: var(--text); + font-family: inherit; + font-size: 14px; + padding: 8px 10px; + outline: none; + width: 100%; transition: border-color 0.2s; } +input:focus { border-color: rgba(124,106,245,0.4); } +input::placeholder { color: var(--text3); } -select:focus { border-color: rgba(124, 106, 245, 0.4); } +/* Hide number spinners */ +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; } +input[type="number"] { -moz-appearance: textfield; } -/* ============================================================ - Generate button - ============================================================ */ +/* ── Stepper ─────────────────────────────────────────────────── */ + +.stepper { + display: flex; + align-items: center; + gap: 0; + width: 100%; +} + +.stepper-btn { + background: var(--surface2); + border: 1px solid var(--border); + color: var(--text2); + width: 36px; + height: 36px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, color 0.15s; + flex-shrink: 0; + border-radius: var(--radius2); +} +.stepper-btn:first-child { border-radius: var(--radius2) 0 0 var(--radius2); } +.stepper-btn:last-child { border-radius: 0 var(--radius2) var(--radius2) 0; } +.stepper-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); } + +.stepper input { + border-radius: 0; + border-left: none; + border-right: none; + text-align: center; + width: 52px; + flex-shrink: 0; + -moz-appearance: textfield; +} + +/* ── Dimension row ───────────────────────────────────────────── */ + +.dim-row { + display: flex; + align-items: center; + gap: 8px; +} +.dim-row input { flex: 1; } +.dim-x { color: var(--text3); font-size: 14px; flex-shrink: 0; } + +/* ── Weight slider ───────────────────────────────────────────── */ + +.weight-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 4px; + background: var(--surface2); + border-radius: 2px; + outline: none; + border: none; + padding: 0; + cursor: pointer; +} +.weight-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--accent); + cursor: pointer; + transition: transform 0.15s; +} +.weight-slider::-webkit-slider-thumb:hover { transform: scale(1.2); } + +/* ── Toggle switches ─────────────────────────────────────────── */ + +.toggle-group { + display: flex; + flex-direction: column; + gap: 10px; +} + +.toggle-row { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; +} + +.toggle-row input { display: none; } + +.toggle-track { + width: 36px; + height: 20px; + background: var(--surface2); + border-radius: 10px; + position: relative; + transition: background 0.2s; + flex-shrink: 0; + border: 1px solid var(--border); +} + +.toggle-thumb { + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--text3); + transition: transform 0.2s, background 0.2s; +} + +.toggle-row input:checked + .toggle-track { + background: rgba(124,106,245,0.3); + border-color: rgba(124,106,245,0.5); +} +.toggle-row input:checked + .toggle-track .toggle-thumb { + transform: translateX(16px); + background: var(--accent); +} + +.toggle-label { font-size: 13px; color: var(--text2); } + +/* ── Generate Button ─────────────────────────────────────────── */ .generate-btn { display: flex; align-items: center; + justify-content: center; gap: 8px; background: var(--accent); color: #fff; border: none; border-radius: var(--radius2); - padding: 10px 20px; - font-size: 14px; + padding: 12px 24px; + font-size: 15px; font-weight: 600; - height: 42px; + width: 100%; + height: 48px; transition: background 0.2s, transform 0.15s var(--ease), opacity 0.2s; - position: relative; - overflow: hidden; - flex-shrink: 0; -} - -.generate-btn:hover:not(:disabled) { - background: var(--accent2); - transform: translateY(-1px); -} - -.generate-btn:active:not(:disabled) { - transform: translateY(0); -} - -.generate-btn:disabled { - opacity: 0.7; - cursor: not-allowed; -} - -.generate-btn .spinner { - display: none; - animation: spin 0.9s linear infinite; + margin-bottom: 16px; } +.generate-btn:hover:not(:disabled) { background: var(--accent2); transform: translateY(-1px); } +.generate-btn:active:not(:disabled) { transform: translateY(0); } +.generate-btn:disabled { opacity: 0.65; cursor: not-allowed; } +.generate-btn .spinner { display: none; animation: spin 0.9s linear infinite; } .generate-btn.loading .btn-icon, -.generate-btn.loading .btn-text { - display: none; -} +.generate-btn.loading .btn-text { display: none; } +.generate-btn.loading .spinner { display: inline; } -.generate-btn.loading .spinner { - display: inline; -} - -/* ============================================================ - Error - ============================================================ */ +/* ── Error ────────────────────────────────────────────────────── */ .error-msg { display: flex; align-items: center; gap: 8px; - margin-top: 14px; + margin-bottom: 16px; padding: 10px 14px; - background: rgba(245, 108, 108, 0.1); - border: 1px solid rgba(245, 108, 108, 0.2); + background: rgba(245,108,108,0.1); + border: 1px solid rgba(245,108,108,0.2); border-radius: var(--radius2); color: var(--danger); font-size: 13px; animation: fadeSlideIn 0.3s var(--ease); } -/* ============================================================ - Result area - ============================================================ */ +/* ── Result Meta ──────────────────────────────────────────────── */ -.result-area { - margin-top: 28px; - animation: fadeSlideIn 0.5s var(--ease); -} - -.result-header { +.result-meta { display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 12px; -} - -#resultLabel { + flex-wrap: wrap; + gap: 16px; + padding: 10px 14px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius2); font-size: 12px; - font-weight: 600; color: var(--text2); - text-transform: uppercase; - letter-spacing: 0.8px; + margin-bottom: 14px; } -.result-actions { +.result-meta code { + font-family: 'Courier New', monospace; + color: var(--accent2); + font-size: 11px; +} + +/* ── Image Grid ──────────────────────────────────────────────── */ + +.image-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} + +.image-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + animation: fadeSlideIn 0.4s var(--ease); +} + +.image-card img { + display: block; + width: 100%; + height: auto; + min-height: 180px; + object-fit: cover; + background: var(--surface2); +} + +.image-actions { display: flex; gap: 8px; + padding: 10px; + border-top: 1px solid var(--border); } .action-btn { + flex: 1; display: flex; align-items: center; - gap: 6px; - background: var(--surface); + justify-content: center; + gap: 5px; + background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius2); color: var(--text2); font-size: 12px; - padding: 6px 12px; - transition: background 0.2s, color 0.2s, border-color 0.2s; + padding: 6px 10px; + transition: background 0.15s, color 0.15s, border-color 0.15s; + cursor: pointer; + font-family: inherit; } - .action-btn:hover { - background: var(--surface2); + background: rgba(124,106,245,0.12); color: var(--text); - border-color: rgba(255,255,255,0.12); + border-color: rgba(124,106,245,0.3); } -.image-wrap { - border-radius: var(--radius); - overflow: hidden; - background: var(--surface); - border: 1px solid var(--border); -} - -.image-wrap img { - display: block; - width: 100%; - height: auto; -} - -@keyframes popIn { - 0% { opacity: 0; transform: scale(0.96); } - 100% { opacity: 1; transform: scale(1); } -} - -.pop-in { - animation: popIn 0.4s var(--ease); -} - -/* ============================================================ - Icon button - ============================================================ */ +/* ── Icon button ─────────────────────────────────────────────── */ .icon-btn { display: flex; @@ -349,15 +512,9 @@ select:focus { border-color: rgba(124, 106, 245, 0.4); } transition: background 0.15s, color 0.15s; flex-shrink: 0; } +.icon-btn:hover { background: var(--surface); color: var(--text); } -.icon-btn:hover { - background: var(--surface); - color: var(--text); -} - -/* ============================================================ - Modal - ============================================================ */ +/* ── Modal ───────────────────────────────────────────────────── */ .modal-overlay { position: fixed; @@ -373,11 +530,7 @@ select:focus { border-color: rgba(124, 106, 245, 0.4); } transition: opacity 0.25s; z-index: 100; } - -.modal-overlay.open { - opacity: 1; - pointer-events: all; -} +.modal-overlay.open { opacity: 1; pointer-events: all; } .modal { background: var(--surface); @@ -388,10 +541,7 @@ select:focus { border-color: rgba(124, 106, 245, 0.4); } transform: translateY(12px) scale(0.97); transition: transform 0.3s var(--ease); } - -.modal-overlay.open .modal { - transform: translateY(0) scale(1); -} +.modal-overlay.open .modal { transform: translateY(0) scale(1); } .modal-header { display: flex; @@ -399,12 +549,7 @@ select:focus { border-color: rgba(124, 106, 245, 0.4); } justify-content: space-between; padding: 18px 20px 0; } - -.modal-header h2 { - font-size: 16px; - font-weight: 600; - color: var(--text); -} +.modal-header h2 { font-size: 16px; font-weight: 600; } .modal-body { padding: 18px 20px; @@ -413,47 +558,13 @@ select:focus { border-color: rgba(124, 106, 245, 0.4); } gap: 18px; } -.field { - display: flex; - flex-direction: column; - gap: 7px; -} +.field { display: flex; flex-direction: column; gap: 7px; } +.field label { font-size: 13px; font-weight: 500; color: var(--text2); } -.field label { - font-size: 13px; - font-weight: 500; - color: var(--text2); -} +.input-row { display: flex; gap: 8px; } +.input-row input { flex: 1; } -.input-row { - display: flex; - gap: 8px; -} - -.input-row input { - flex: 1; -} - -input[type="text"], -input[type="password"] { - background: var(--bg); - border: 1px solid var(--border); - border-radius: var(--radius2); - color: var(--text); - font-family: inherit; - font-size: 14px; - padding: 9px 12px; - outline: none; - width: 100%; - transition: border-color 0.2s; -} - -input:focus { border-color: rgba(124, 106, 245, 0.4); } - -.field-hint { - font-size: 11px; - color: var(--text3); -} +.field-hint { font-size: 11px; color: var(--text3); } .modal-footer { display: flex; @@ -470,13 +581,10 @@ input:focus { border-color: rgba(124, 106, 245, 0.4); } font-family: inherit; font-size: 14px; padding: 8px 18px; + cursor: pointer; transition: background 0.15s, color 0.15s; } - -.btn-secondary:hover { - background: rgba(255,255,255,0.06); - color: var(--text); -} +.btn-secondary:hover { background: rgba(255,255,255,0.06); color: var(--text); } .btn-primary { background: var(--accent); @@ -487,32 +595,24 @@ input:focus { border-color: rgba(124, 106, 245, 0.4); } font-size: 14px; font-weight: 600; padding: 8px 18px; + cursor: pointer; transition: background 0.15s; } - .btn-primary:hover { background: var(--accent2); } -/* ============================================================ - Animations - ============================================================ */ +/* ── Animations ──────────────────────────────────────────────── */ @keyframes fadeSlideIn { - from { opacity: 0; transform: translateY(6px); } + from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } -@keyframes spin { - to { transform: rotate(360deg); } -} +@keyframes spin { to { transform: rotate(360deg); } } -/* ============================================================ - Responsive - ============================================================ */ +/* ── Responsive ──────────────────────────────────────────────── */ -@media (max-width: 480px) { - .controls { flex-direction: column; } - .control-group { width: 100%; } - .generate-btn { width: 100%; justify-content: center; } - .result-actions { gap: 6px; } - .action-btn { padding: 6px 10px; } +@media (max-width: 600px) { + #app { padding: 0 16px 60px; } + .options-grid { grid-template-columns: 1fr; } + .image-grid { grid-template-columns: 1fr; } }