buddy-workbench 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-workbench",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
package/server/config.js CHANGED
@@ -10,6 +10,7 @@ export const paths = {
10
10
  jiraFilters: join(root, 'data', 'jira-filters.json'),
11
11
  todos: join(root, 'data', 'todos.json'),
12
12
  staticPages: join(root, 'data', 'static-pages.json'),
13
+ errors: join(root, 'data', 'errors.json'),
13
14
  shutdownLog: join(root, 'data', 'shutdown.log'),
14
15
  clipboardDir: join(root, 'data', 'clipboard'),
15
16
  plugins: join(root, 'plugins'),
@@ -0,0 +1,44 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { paths } from '../config.js';
5
+
6
+ export function listErrors() {
7
+ try {
8
+ return existsSync(paths.errors) ? JSON.parse(readFileSync(paths.errors, 'utf8')) : [];
9
+ } catch {
10
+ return [];
11
+ }
12
+ }
13
+
14
+ export function saveErrors(errors) {
15
+ mkdirSync(dirname(paths.errors), { recursive: true });
16
+ const trimmed = Array.isArray(errors) ? errors.slice(-100) : [];
17
+ writeFileSync(paths.errors, JSON.stringify(trimmed, null, 2));
18
+ }
19
+
20
+ export function addErrorRecord({ source = 'System', message = '', details = '', level = 'error' }) {
21
+ if (!message && !details) return null;
22
+ const current = listErrors();
23
+ const record = {
24
+ id: crypto.randomUUID(),
25
+ timestamp: new Date().toISOString(),
26
+ source: String(source || 'System').trim(),
27
+ level: ['fatal', 'warning', 'error'].includes(level) ? level : 'error',
28
+ message: String(message || '').trim() || 'Unspecified Error',
29
+ details: String(details || '').trim()
30
+ };
31
+ current.push(record);
32
+ saveErrors(current);
33
+ return record;
34
+ }
35
+
36
+ export function clearAllErrors() {
37
+ saveErrors([]);
38
+ }
39
+
40
+ export function deleteErrorRecord(id) {
41
+ const current = listErrors();
42
+ const filtered = current.filter((item) => item.id !== id);
43
+ saveErrors(filtered);
44
+ }
@@ -0,0 +1,31 @@
1
+ import { Router } from 'express';
2
+ import { addErrorRecord, clearAllErrors, deleteErrorRecord, listErrors } from '../repositories/errors.js';
3
+
4
+ const router = Router();
5
+
6
+ router.get('/', (_req, res) => {
7
+ const errors = listErrors();
8
+ // Return newest first
9
+ res.json([...errors].reverse());
10
+ });
11
+
12
+ router.post('/', (req, res) => {
13
+ const { source, message, details, level } = req.body || {};
14
+ if (!message && !details) {
15
+ return res.status(400).json({ error: 'Message or details is required.' });
16
+ }
17
+ const record = addErrorRecord({ source, message, details, level });
18
+ res.status(201).json(record);
19
+ });
20
+
21
+ router.delete('/', (_req, res) => {
22
+ clearAllErrors();
23
+ res.status(204).end();
24
+ });
25
+
26
+ router.delete('/:id', (req, res) => {
27
+ deleteErrorRecord(req.params.id);
28
+ res.status(204).end();
29
+ });
30
+
31
+ export default router;
@@ -130,7 +130,7 @@ router.get('/:id/issues', async (req, res) => {
130
130
  const response = await httpClient.get(url, { headers });
131
131
  if (response.status !== 200) {
132
132
  const errMsg = response.data?.errorMessages?.[0] || response.data?.message || `Jira API returned status ${response.status}`;
133
- return res.status(response.status).json({ error: errMsg });
133
+ return res.status(response.status).json({ error: errMsg, targetUrl: url });
134
134
  }
135
135
 
136
136
  const rawIssues = response.data?.issues || [];
@@ -153,7 +153,8 @@ router.get('/:id/issues', async (req, res) => {
153
153
 
154
154
  res.json({ filter, issues });
155
155
  } catch (error) {
156
- res.status(500).json({ error: error.message || 'Failed to fetch Jira issues.' });
156
+ const targetUrl = `https://${jiraHost}/rest/api/2/search?jql=filter%3D${encodeURIComponent(filter.filterId)}&maxResults=200&fields=summary,priority,duedate,status`;
157
+ res.status(500).json({ error: error.message || 'Failed to fetch Jira issues.', targetUrl });
157
158
  }
158
159
  });
159
160
 
@@ -213,9 +213,9 @@ router.post('/check', async (req, res) => {
213
213
  const prRes = await httpClient.get(prUrl, { headers });
214
214
  if (prRes.status < 200 || prRes.status >= 300) {
215
215
  if (prRes.status === 401) {
216
- return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Access Token in Settings.' });
216
+ return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Access Token in Settings.', targetUrl: prUrl });
217
217
  }
218
- return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText || prRes.status}` });
218
+ return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText || prRes.status}`, targetUrl: prUrl });
219
219
  }
220
220
  const prInfo = prRes.data;
221
221
 
@@ -223,7 +223,7 @@ router.post('/check', async (req, res) => {
223
223
  const changesUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/changes?limit=1000`;
224
224
  const changesRes = await httpClient.get(changesUrl, { headers });
225
225
  if (changesRes.status < 200 || changesRes.status >= 300) {
226
- return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText || changesRes.status}` });
226
+ return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText || changesRes.status}`, targetUrl: changesUrl });
227
227
  }
228
228
  const changesData = changesRes.data;
229
229
 
@@ -329,14 +329,14 @@ router.get('/my-prs', async (req, res) => {
329
329
  const headers = { 'Accept': 'application/json' };
330
330
  if (token) headers['Authorization'] = `Bearer ${token}`;
331
331
 
332
+ const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
332
333
  try {
333
- const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
334
334
  const response = await httpClient.get(url, { headers });
335
335
  if (response.status < 200 || response.status >= 300) {
336
336
  if (response.status === 401) {
337
- return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
337
+ return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.', targetUrl: url });
338
338
  }
339
- return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}` });
339
+ return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}`, targetUrl: url });
340
340
  }
341
341
  const data = response.data;
342
342
  const values = data.values || [];
@@ -350,7 +350,7 @@ router.get('/my-prs', async (req, res) => {
350
350
 
351
351
  res.json({ values: enrichPrList(sorted, host) });
352
352
  } catch (error) {
353
- res.status(500).json({ error: error.message });
353
+ res.status(500).json({ error: error.message, targetUrl: url });
354
354
  }
355
355
  });
356
356
 
@@ -363,14 +363,14 @@ router.get('/review-prs', async (req, res) => {
363
363
  const headers = { 'Accept': 'application/json' };
364
364
  if (token) headers['Authorization'] = `Bearer ${token}`;
365
365
 
366
+ const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
366
367
  try {
367
- const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
368
368
  const response = await httpClient.get(url, { headers });
369
369
  if (response.status < 200 || response.status >= 300) {
370
370
  if (response.status === 401) {
371
- return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
371
+ return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.', targetUrl: url });
372
372
  }
373
- return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}` });
373
+ return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}`, targetUrl: url });
374
374
  }
375
375
  const data = response.data;
376
376
  const values = data.values || [];
@@ -379,7 +379,7 @@ router.get('/review-prs', async (req, res) => {
379
379
 
380
380
  res.json({ values: enrichPrList(sorted, host) });
381
381
  } catch (error) {
382
- res.status(500).json({ error: error.message });
382
+ res.status(500).json({ error: error.message, targetUrl: url });
383
383
  }
384
384
  });
385
385
 
@@ -423,14 +423,15 @@ router.post('/comment', async (req, res) => {
423
423
  if (response.status < 200 || response.status >= 300) {
424
424
  const errJson = response.data || {};
425
425
  return res.status(response.status).json({
426
- error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText || response.status}`
426
+ error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText || response.status}`,
427
+ targetUrl: url
427
428
  });
428
429
  }
429
430
 
430
431
  const data = response.data;
431
432
  res.json({ success: true, data });
432
433
  } catch (error) {
433
- res.status(500).json({ error: error.message });
434
+ res.status(500).json({ error: error.message, targetUrl: url });
434
435
  }
435
436
  });
436
437
 
@@ -8,8 +8,8 @@ router.get('/', (_req, res) => {
8
8
  res.json(listStaticPages());
9
9
  });
10
10
 
11
- // Update static pages
12
- router.post('/', (req, res) => {
11
+ // Save all static pages
12
+ router.put('/', (req, res) => {
13
13
  if (!Array.isArray(req.body)) {
14
14
  return res.status(400).json({ error: 'Expected an array of static pages.' });
15
15
  }
@@ -17,4 +17,66 @@ router.post('/', (req, res) => {
17
17
  res.json(listStaticPages());
18
18
  });
19
19
 
20
+ // Create single static page
21
+ router.post('/', (req, res) => {
22
+ const { name, url } = req.body || {};
23
+ if (!name || typeof name !== 'string' || !name.trim()) {
24
+ return res.status(400).json({ error: 'Page name is required.' });
25
+ }
26
+ if (!url || typeof url !== 'string' || !url.trim()) {
27
+ return res.status(400).json({ error: 'URL is required.' });
28
+ }
29
+
30
+ const pages = listStaticPages();
31
+ const newPage = {
32
+ id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
33
+ name: name.trim(),
34
+ url: url.trim(),
35
+ icon: 'GlobalOutlined'
36
+ };
37
+ pages.push(newPage);
38
+ saveStaticPages(pages);
39
+ res.status(201).json(newPage);
40
+ });
41
+
42
+ // Update single static page
43
+ router.put('/:id', (req, res) => {
44
+ const { id } = req.params;
45
+ const { name, url } = req.body || {};
46
+ const pages = listStaticPages();
47
+ const index = pages.findIndex((p) => String(p.id) === String(id));
48
+ if (index === -1) {
49
+ return res.status(404).json({ error: 'Static page not found.' });
50
+ }
51
+
52
+ if (name !== undefined) {
53
+ if (typeof name !== 'string' || !name.trim()) {
54
+ return res.status(400).json({ error: 'Page name cannot be empty.' });
55
+ }
56
+ pages[index].name = name.trim();
57
+ }
58
+
59
+ if (url !== undefined) {
60
+ if (typeof url !== 'string' || !url.trim()) {
61
+ return res.status(400).json({ error: 'URL cannot be empty.' });
62
+ }
63
+ pages[index].url = url.trim();
64
+ }
65
+
66
+ saveStaticPages(pages);
67
+ res.json(pages[index]);
68
+ });
69
+
70
+ // Delete static page
71
+ router.delete('/:id', (req, res) => {
72
+ const { id } = req.params;
73
+ const pages = listStaticPages();
74
+ const filtered = pages.filter((p) => String(p.id) !== String(id));
75
+ if (filtered.length === pages.length) {
76
+ return res.status(404).json({ error: 'Static page not found.' });
77
+ }
78
+ saveStaticPages(filtered);
79
+ res.status(204).end();
80
+ });
81
+
20
82
  export default router;
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { promisify } from 'node:util';
5
5
  import { execFile } from 'node:child_process';
6
6
  import { listPortHistory, savePortHistory } from '../repositories/port-history.js';
7
+ import { addErrorRecord } from '../repositories/errors.js';
7
8
 
8
9
  const running = new Map();
9
10
  const logs = new Map();
@@ -217,6 +218,13 @@ export function runScript(launcher, script) {
217
218
  child.on('exit', (code) => {
218
219
  appendLog(key, code === 0 ? 'output' : 'error', `\nProcess exited with code ${code}.\n`);
219
220
  const item = logs.get(key);
221
+ if (code !== null && code !== 0) {
222
+ addErrorRecord({
223
+ source: `Launcher: ${launcher.alias}`,
224
+ message: `Script "${script.name}" exited with code ${code}`,
225
+ details: item?.error || item?.output || `Command: ${script.command}`
226
+ });
227
+ }
220
228
  if (item) item.errorCount = 0;
221
229
  running.delete(key);
222
230
  retireGroup(child.pid);
package/server.js CHANGED
@@ -16,11 +16,43 @@ import prReviewRoutes from './server/routes/pr-review.js';
16
16
  import jiraFiltersRoutes from './server/routes/jira-filters.js';
17
17
  import todoRoutes from './server/routes/todos.js';
18
18
  import staticPagesRoutes from './server/routes/static-pages.js';
19
+ import errorRoutes from './server/routes/errors.js';
20
+ import { addErrorRecord } from './server/repositories/errors.js';
19
21
  import { startClipboardCapture } from './server/services/clipboard-history.js';
20
22
 
21
23
  const port = Number(process.env.PORT || 3100);
22
24
  const app = express();
23
25
  app.use(express.json());
26
+
27
+ // Intercept error responses (HTTP status >= 400) from any API route
28
+ app.use((req, res, next) => {
29
+ const originalJson = res.json;
30
+ res.json = function (body) {
31
+ if (res.statusCode >= 400 && body && body.error && !req.path.startsWith('/api/errors')) {
32
+ const host = req.get('host') || req.headers.host || `localhost:${port}`;
33
+ const fullUrl = `${req.protocol}://${host}${req.originalUrl || req.url}`;
34
+
35
+ const detailsLines = [];
36
+ if (body.targetUrl) {
37
+ detailsLines.push(`Target URL: ${body.targetUrl}`);
38
+ }
39
+ detailsLines.push(`Endpoint: ${req.method} ${fullUrl}`);
40
+ detailsLines.push(`HTTP Status: ${res.statusCode}`);
41
+ if (typeof body.error === 'string' && body.error !== `HTTP ${res.statusCode} Error`) {
42
+ detailsLines.push(`Error: ${body.error}`);
43
+ }
44
+
45
+ addErrorRecord({
46
+ source: `Backend API (${req.method} ${req.path})`,
47
+ message: typeof body.error === 'string' ? body.error : `HTTP ${res.statusCode} Error`,
48
+ details: detailsLines.join('\n')
49
+ });
50
+ }
51
+ return originalJson.call(this, body);
52
+ };
53
+ next();
54
+ });
55
+
24
56
  app.use(express.static(paths.ui));
25
57
  app.use('/plugins', express.static(paths.plugins));
26
58
  app.use('/api/launchers', launcherRoutes);
@@ -33,6 +65,16 @@ app.use('/api/pr-review', prReviewRoutes);
33
65
  app.use('/api/jira-filters', jiraFiltersRoutes);
34
66
  app.use('/api/todos', todoRoutes);
35
67
  app.use('/api/static-pages', staticPagesRoutes);
68
+ app.use('/api/errors', errorRoutes);
69
+
70
+ app.use((err, req, res, _next) => {
71
+ addErrorRecord({
72
+ source: `Backend API (${req.method} ${req.path})`,
73
+ message: err.message || 'Internal Server Error',
74
+ details: err.stack || String(err)
75
+ });
76
+ res.status(500).json({ error: err.message || 'Internal Server Error' });
77
+ });
36
78
 
37
79
  await freePort(port);
38
80
  const server = app.listen(port, () => { console.log(`Buddy Workbench: http://localhost:${port} (pid ${process.pid})`); shutdownTrace(`server started (pid=${process.pid})`); });
@@ -57,6 +99,14 @@ async function shutdown(exitCode = 0) {
57
99
  process.once('SIGINT', () => { shutdownTrace('received SIGINT'); void shutdown(); });
58
100
  process.once('SIGTERM', () => { shutdownTrace('received SIGTERM'); void shutdown(); });
59
101
  process.once('SIGHUP', () => { shutdownTrace('received SIGHUP'); void shutdown(); });
60
- process.once('uncaughtException', (error) => { console.error(error); void shutdown(1); });
61
- process.once('unhandledRejection', (error) => { console.error(error); void shutdown(1); });
102
+ process.once('uncaughtException', (error) => {
103
+ addErrorRecord({ source: 'Server UncaughtException', message: error.message || 'Uncaught Server Exception', details: error.stack || String(error) });
104
+ console.error(error);
105
+ void shutdown(1);
106
+ });
107
+ process.once('unhandledRejection', (error) => {
108
+ addErrorRecord({ source: 'Server UnhandledRejection', message: error?.message || String(error || 'Unhandled Promise Rejection'), details: error?.stack || String(error) });
109
+ console.error(error);
110
+ void shutdown(1);
111
+ });
62
112
  process.once('exit', () => stopAllScripts(true));
@@ -1 +1 @@
1
- html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,PingFang SC,sans-serif;color:#172033;background:#f7f8fc}html,body{height:100vh;overflow:hidden}body{margin:0;background:#f7f8fc}.workbench{height:100vh;overflow:hidden;background:#f7f8fc}.sidebar{position:sticky!important;top:0;height:100vh;background:#171c2b!important;padding:28px 14px}.sidebar .ant-menu-title-content{-webkit-user-select:none;user-select:none}.brand{padding:0 12px 30px;color:#fff;font-size:21px;font-weight:750;letter-spacing:-.4px}.brand span{color:#9589ff}.sidebar .ant-menu{background:transparent;border-inline-end:0}.sidebar-bottom{position:absolute;right:14px;bottom:28px;left:14px}.sidebar-footer{padding:14px 12px 0;color:#727b94;font-size:12px}.page{height:100vh;overflow-y:auto;padding:24px clamp(20px,3.5vw,40px)}.page-header{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:30px}.page-header .ant-typography{margin:0}.launcher-grid{display:grid;gap:7px}.launcher-card{cursor:pointer;border-color:#e3e6ef}.launcher-card .ant-card-body{padding:10px 14px}.service-card{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:46px}.service-card .ant-typography{display:block;margin:0}.service-card h4.ant-typography{margin:0 0 1px;font-size:15px;line-height:20px}.service-info{min-width:0;flex:1}.service-info>.ant-typography{font-size:12px;line-height:16px}.service-action{display:flex;flex-direction:column;align-items:flex-end;gap:3px;white-space:nowrap}.service-action .ant-badge{font-size:11px;line-height:14px}.launcher-table .ant-table-tbody>tr{cursor:pointer}.start-script-icon{color:#8c8c8c;font-size:12px}.script-add{margin-bottom:10px}.service-config{margin-bottom:10px;margin-top:10px}.service-config .ant-collapse-content-box{padding-bottom:0!important}.service-config .ant-form-item{margin-bottom:14px}.clipboard-picker{min-width:190px}.squoosh-page{position:relative;min-height:calc(100vh - 48px);margin:-24px clamp(-20px,-3.5vw,-40px);padding:24px clamp(20px,3.5vw,40px);color:#172033}.squoosh-page.is-file-dragging{box-shadow:inset 0 0 0 3px #7667e8}.squoosh-page.is-file-dragging:after{position:absolute;z-index:20;top:12px;right:12px;bottom:12px;left:12px;display:grid;place-items:center;border:2px dashed #7667e8;border-radius:12px;background:#f8f7ffc7;color:#5b4cc4;content:"Drop image to compress";font-size:20px;font-weight:700;pointer-events:none}.squoosh-header{display:flex;align-items:center;justify-content:space-between;gap:20px;padding-bottom:20px;border-bottom:1px solid #e4e7ef}.squoosh-header .ant-typography{margin:0;color:inherit}.squoosh-header>div>.ant-typography:first-child,.settings-heading>.ant-typography:first-child{color:#6d5ce7;font-size:11px;font-weight:750;letter-spacing:.12em}.squoosh-header h2.ant-typography{margin-top:4px;font-size:25px}.squoosh-studio{display:grid;grid-template-columns:minmax(0,1fr) 380px;margin-top:16px;border:1px solid #e1e5ed;border-radius:10px;overflow:hidden;background:#fff;box-shadow:0 12px 32px #1f2a4412}.squoosh-preview-area{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));min-width:0;background:#fbfcfe}.preview-pane{display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:570px}.preview-pane+.preview-pane{border-left:1px solid #e1e5ed}.preview-pane>header{display:flex;justify-content:space-between;align-items:center;gap:12px;min-height:58px;padding:0 16px;border-bottom:1px solid #e1e5ed;background:#fff}.preview-pane>header>div{display:grid;gap:1px;min-width:0}.preview-pane>header .ant-typography{color:#27344c;font-weight:650}.preview-pane>header span{overflow:hidden;color:#7d8799;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.preview-canvas{position:relative;display:grid;min-width:0;min-height:0;place-items:center;padding:22px;background-color:#f3f5f9;background-image:linear-gradient(45deg,#e9edf4 25%,transparent 25%),linear-gradient(-45deg,#e9edf4 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#e9edf4 75%),linear-gradient(-45deg,transparent 75%,#e9edf4 75%);background-position:0 0,0 12px,12px -12px,-12px 0;background-size:24px 24px}.image-dropzone{width:100%;max-width:420px}.image-dropzone.ant-upload-wrapper .ant-upload-drag{border-color:#cfd6e4;background:#ffffffe0}.image-dropzone.ant-upload-wrapper .ant-upload-drag:hover{border-color:#7667e8}.image-dropzone .ant-upload{padding:76px 18px!important}.image-dropzone .ant-upload-drag-icon .anticon{color:#6d5ce7}.image-dropzone .ant-upload-text{color:#27344c!important}.image-dropzone .ant-upload-hint{color:#7d8799!important}.image-panel{display:flex;align-items:center;justify-content:center;width:100%;height:100%;min-width:0;min-height:0}.image-panel img{display:block;width:auto;max-width:100%;max-height:min(65vh,650px);object-fit:contain;border-radius:5px;box-shadow:0 10px 30px #00000052}.image-panel-meta{display:grid;gap:3px;min-width:0;text-align:center}.image-panel-meta .ant-typography{min-width:0;color:#46516a}.squoosh-settings{overflow-y:auto;padding:22px 20px;background:#fff}.settings-heading{margin-bottom:21px;padding-bottom:17px;border-bottom:1px solid #e7eaf0}.settings-heading .ant-typography{margin:0;color:#27344c}.settings-heading h4.ant-typography{margin-top:5px}.settings-heading .ant-typography-secondary{margin-top:4px;color:#7d8799;font-size:13px}.option-row,.option-switch,.option-select{display:flex;justify-content:space-between;align-items:center;gap:16px}.option-row{margin-bottom:4px}.option-row .ant-typography,.option-switch .ant-typography,.option-select .ant-typography{color:#27344c}.option-row .ant-typography-secondary,.option-switch .ant-typography-secondary,.option-select .ant-typography-secondary{color:#7d8799;font-size:12px}.option-switch{padding:15px 0;border-top:1px solid #e7eaf0}.option-select{padding:14px 0;border-top:1px solid #e7eaf0}.option-select .ant-select,.option-select .ant-input-number{flex:0 0 170px;width:170px}.advanced-options{margin:6px -12px 16px}.advanced-options .ant-collapse-header,.advanced-options .ant-collapse-content{color:#46516a!important}.advanced-options .ant-collapse-content{background:transparent}.advanced-options .ant-collapse-content-box{padding-top:0!important}.image-loading{display:grid;min-height:330px;place-items:center}.image-loading .ant-spin-text{color:#46516a}.preview-pane>header button.ant-btn.ant-btn-color-primary{min-width:116px;border-color:#5f50d8!important;background:#6657dc!important;color:#fff!important;font-weight:650;text-shadow:none}.preview-pane>header button.ant-btn.ant-btn-color-primary>span,.preview-pane>header button.ant-btn.ant-btn-color-primary .anticon{color:#fff!important}.preview-pane>header button.ant-btn.ant-btn-color-primary:hover{border-color:#5143c2!important;background:#5143c2!important;color:#fff!important}.compression-saving{position:absolute;bottom:24px;left:50%;z-index:1;margin:0;padding:9px 12px;border-radius:4px;background:#fff7e6;color:#ad6800;font-weight:600;text-align:center;transform:translate(-50%);white-space:nowrap}.compression-saving.is-saving{background:#f6ffed;color:#389e0d}.clipboard-tabs{margin:-12px 0 16px}.clipboard-tabs .ant-tabs-nav{margin-bottom:0}.clipboard-list{display:grid;gap:8px}.clipboard-item .ant-card-body{position:relative;display:block;padding:12px 14px}.clipboard-item .ant-card-body>div{width:100%;min-width:0}.clipboard-actions{position:absolute;top:8px;right:8px;display:flex}.clipboard-item pre{width:100%;max-height:160px;overflow:auto;margin:5px 0 0;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.clipboard-original{display:flex;gap:12px;margin-top:8px;font-size:12px}.port-manual-query{margin:-12px 0 16px}.port-query-result{margin-top:12px}.group-task-list{display:grid;gap:8px}.group-task-header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:16px}.group-task-empty{margin:16px 0}.group-task-card{display:flex;align-items:center;justify-content:space-between;gap:16px}.group-task-card>div:first-child{display:grid;gap:2px}.group-task-card .ant-space{flex-wrap:nowrap}.group-task-card .ant-btn{width:72px}.group-task-selector{display:grid;gap:8px;width:100%}.group-task-selector .ant-card-body{padding:10px 14px}.group-task-projects{display:grid;gap:8px}.settings-page{width:100%;max-width:none}.settings-page>.ant-typography{margin:0}.settings-page-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.settings-page-header .ant-typography{margin:0}.settings-page-header>.ant-btn{min-width:96px}.settings-card{width:100%;margin-top:20px;border-radius:14px;border:1px solid #e4e8f1;box-shadow:0 2px 12px #1f2a440d}.settings-card h4.ant-typography{margin:0}.settings-icon-wrap{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex-shrink:0;border-radius:10px;background:linear-gradient(135deg,#ece9fc,#ddd6fe)}.settings-icon{color:#6d5ce7;font-size:18px}.settings-title{display:flex;align-items:center;gap:14px;margin-bottom:24px;padding-bottom:20px;border-bottom:1px solid #eef0f6}.settings-title .ant-typography{margin:0}.settings-title .ant-typography-secondary{font-size:13px;margin-top:2px}.token-settings-form{display:grid;gap:0}.token-row{display:grid;grid-template-columns:200px 1fr;align-items:center;gap:20px;padding:16px 18px;margin:0 -6px;border-radius:10px;border:1px solid transparent;transition:background .18s,border-color .18s}.token-row:hover{background:#f8f9fc;border-color:#eef0f6}.token-row+.token-row{margin-top:2px}.token-row.is-configured{background:#fafbfe}.token-row-info{display:grid;gap:2px;min-width:0}.token-row-name-line{display:flex;align-items:center;gap:8px}.token-row-name{font-size:14px;white-space:nowrap}.token-row-hint{font-size:12px;line-height:1.4}.token-row-tag.ant-tag{margin:0;font-size:11px;line-height:18px;padding-inline:6px;border-radius:4px}.token-row .ant-form-item{margin:0;min-width:0}.token-row .ant-input,.token-row .ant-input-password{border-radius:8px}.script-header{display:flex;align-items:center;gap:16px}.script-header .ant-divider{flex:1;min-width:0;margin:16px 0}.script-header>.ant-btn{flex-shrink:0}.script-editor{margin-bottom:10px}.script-fields{display:grid;grid-template-columns:1fr 1.5fr;gap:12px}.script-fields .ant-form-item{margin-bottom:12px}.log-output{min-height:360px;max-height:58vh;overflow:auto;margin:0;padding:12px 0;border-radius:4px;background:#111827;color:#d1fae5;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}.log-line{min-height:19px}.log-line:hover{background:#ffffff0a}.log-line code{padding:0 15px;overflow-wrap:anywhere;color:inherit;white-space:pre-wrap;font:inherit}.log-link{color:#7dd3fc;text-decoration:underline;text-underline-offset:2px}.log-link:hover{color:#bae6fd}.ansi-black{color:#475569}.ansi-red,.log-error{color:#fb7185}.ansi-green,.log-success{color:#86efac}.ansi-yellow,.log-warning{color:#fcd34d}.ansi-blue{color:#93c5fd}.ansi-magenta{color:#f0abfc}.ansi-cyan{color:#67e8f9}.ansi-gray{color:#94a3b8}.ansi-white{color:#f8fafc}@media(max-width:1100px){.squoosh-studio{grid-template-columns:1fr}.squoosh-settings{border-top:1px solid #e1e5ed}.preview-pane{min-height:440px}}@media(max-width:650px){.sidebar{width:64px!important;min-width:64px!important;padding:20px 8px}.brand{padding:0 10px 25px;font-size:0}.brand span{font-size:20px}.sidebar .ant-menu-title-content,.sidebar-footer{display:none}.sidebar-bottom{right:8px;bottom:20px;left:8px}.page{padding:32px 20px}.service-card,.page-header{align-items:flex-start;flex-direction:column}.script-fields{grid-template-columns:1fr}.token-row{grid-template-columns:1fr;gap:10px;padding:14px 12px}.squoosh-page{min-height:calc(100vh - 64px);margin:-12px -20px -32px;padding:18px 20px 28px}.squoosh-header{align-items:flex-start;flex-direction:column}.squoosh-preview-area{grid-template-columns:1fr}.preview-pane{min-height:360px}.preview-pane+.preview-pane{border-top:1px solid #e1e5ed;border-left:0}.preview-canvas{min-height:300px}}.standalone-log-container{display:flex;flex-direction:column;height:100vh;background:#111827;overflow:hidden}.standalone-log-header{display:flex;justify-content:space-between;align-items:center;padding:12px 24px;background:#1f2937;border-bottom:1px solid #374151;color:#f3f4f6;flex-shrink:0}.standalone-log-body{flex:1;min-height:0;display:flex;flex-direction:column;background:#111827}.standalone-log-body .log-output{flex:1;min-height:0;max-height:none!important;border-radius:0!important;margin:0!important;padding:16px 0}.page.is-static-page{padding:0!important;overflow:hidden}.static-page-container{width:100%;height:100%;overflow:hidden;background:#fff}.static-page-iframe{width:100%;height:100%;border:none;display:block}
1
+ html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,PingFang SC,sans-serif;color:#172033;background:#f7f8fc}html,body{height:100vh;overflow:hidden}body{margin:0;background:#f7f8fc}.workbench{height:100vh;overflow:hidden;background:#f7f8fc}.sidebar{position:sticky!important;top:0;height:100vh;background:#171c2b!important;padding:28px 14px}.sidebar .ant-menu-title-content{-webkit-user-select:none;user-select:none}.brand{padding:0 12px 30px;color:#fff;font-size:21px;font-weight:750;letter-spacing:-.4px}.brand span{color:#9589ff}.sidebar .ant-menu{background:transparent;border-inline-end:0}.sidebar-bottom{position:absolute;right:14px;bottom:28px;left:14px}.sidebar-footer{padding:14px 12px 0;color:#727b94;font-size:12px}.page{height:100vh;overflow-y:auto;padding:24px clamp(20px,3.5vw,40px)}.page-header{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:30px}.page-header .ant-typography{margin:0}.launcher-grid{display:grid;gap:7px}.launcher-card{cursor:pointer;border-color:#e3e6ef}.launcher-card .ant-card-body{padding:10px 14px}.service-card{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:46px}.service-card .ant-typography{display:block;margin:0}.service-card h4.ant-typography{margin:0 0 1px;font-size:15px;line-height:20px}.service-info{min-width:0;flex:1}.service-info>.ant-typography{font-size:12px;line-height:16px}.service-action{display:flex;flex-direction:column;align-items:flex-end;gap:3px;white-space:nowrap}.service-action .ant-badge{font-size:11px;line-height:14px}.launcher-table .ant-table-tbody>tr{cursor:pointer}.start-script-icon{color:#8c8c8c;font-size:12px}.script-add{margin-bottom:10px}.service-config{margin-bottom:10px;margin-top:10px}.service-config .ant-collapse-content-box{padding-bottom:0!important}.service-config .ant-form-item{margin-bottom:14px}.clipboard-picker{min-width:190px}.squoosh-page{position:relative;min-height:calc(100vh - 48px);margin:-24px clamp(-20px,-3.5vw,-40px);padding:24px clamp(20px,3.5vw,40px);color:#172033}.squoosh-page.is-file-dragging{box-shadow:inset 0 0 0 3px #7667e8}.squoosh-page.is-file-dragging:after{position:absolute;z-index:20;top:12px;right:12px;bottom:12px;left:12px;display:grid;place-items:center;border:2px dashed #7667e8;border-radius:12px;background:#f8f7ffc7;color:#5b4cc4;content:"Drop image to compress";font-size:20px;font-weight:700;pointer-events:none}.squoosh-header{display:flex;align-items:center;justify-content:space-between;gap:20px;padding-bottom:20px;border-bottom:1px solid #e4e7ef}.squoosh-header .ant-typography{margin:0;color:inherit}.squoosh-header>div>.ant-typography:first-child,.settings-heading>.ant-typography:first-child{color:#6d5ce7;font-size:11px;font-weight:750;letter-spacing:.12em}.squoosh-header h2.ant-typography{margin-top:4px;font-size:25px}.squoosh-studio{display:grid;grid-template-columns:minmax(0,1fr) 380px;margin-top:16px;border:1px solid #e1e5ed;border-radius:10px;overflow:hidden;background:#fff;box-shadow:0 12px 32px #1f2a4412}.squoosh-preview-area{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));min-width:0;background:#fbfcfe}.preview-pane{display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:570px}.preview-pane+.preview-pane{border-left:1px solid #e1e5ed}.preview-pane>header{display:flex;justify-content:space-between;align-items:center;gap:12px;min-height:58px;padding:0 16px;border-bottom:1px solid #e1e5ed;background:#fff}.preview-pane>header>div{display:grid;gap:1px;min-width:0}.preview-pane>header .ant-typography{color:#27344c;font-weight:650}.preview-pane>header span{overflow:hidden;color:#7d8799;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.preview-canvas{position:relative;display:grid;min-width:0;min-height:0;place-items:center;padding:22px;background-color:#f3f5f9;background-image:linear-gradient(45deg,#e9edf4 25%,transparent 25%),linear-gradient(-45deg,#e9edf4 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#e9edf4 75%),linear-gradient(-45deg,transparent 75%,#e9edf4 75%);background-position:0 0,0 12px,12px -12px,-12px 0;background-size:24px 24px}.image-dropzone{width:100%;max-width:420px}.image-dropzone.ant-upload-wrapper .ant-upload-drag{border-color:#cfd6e4;background:#ffffffe0}.image-dropzone.ant-upload-wrapper .ant-upload-drag:hover{border-color:#7667e8}.image-dropzone .ant-upload{padding:76px 18px!important}.image-dropzone .ant-upload-drag-icon .anticon{color:#6d5ce7}.image-dropzone .ant-upload-text{color:#27344c!important}.image-dropzone .ant-upload-hint{color:#7d8799!important}.image-panel{display:flex;align-items:center;justify-content:center;width:100%;height:100%;min-width:0;min-height:0}.image-panel img{display:block;width:auto;max-width:100%;max-height:min(65vh,650px);object-fit:contain;border-radius:5px;box-shadow:0 10px 30px #00000052}.image-panel-meta{display:grid;gap:3px;min-width:0;text-align:center}.image-panel-meta .ant-typography{min-width:0;color:#46516a}.squoosh-settings{overflow-y:auto;padding:22px 20px;background:#fff}.settings-heading{margin-bottom:21px;padding-bottom:17px;border-bottom:1px solid #e7eaf0}.settings-heading .ant-typography{margin:0;color:#27344c}.settings-heading h4.ant-typography{margin-top:5px}.settings-heading .ant-typography-secondary{margin-top:4px;color:#7d8799;font-size:13px}.option-row,.option-switch,.option-select{display:flex;justify-content:space-between;align-items:center;gap:16px}.option-row{margin-bottom:4px}.option-row .ant-typography,.option-switch .ant-typography,.option-select .ant-typography{color:#27344c}.option-row .ant-typography-secondary,.option-switch .ant-typography-secondary,.option-select .ant-typography-secondary{color:#7d8799;font-size:12px}.option-switch{padding:15px 0;border-top:1px solid #e7eaf0}.option-select{padding:14px 0;border-top:1px solid #e7eaf0}.option-select .ant-select,.option-select .ant-input-number{flex:0 0 170px;width:170px}.advanced-options{margin:6px -12px 16px}.advanced-options .ant-collapse-header,.advanced-options .ant-collapse-content{color:#46516a!important}.advanced-options .ant-collapse-content{background:transparent}.advanced-options .ant-collapse-content-box{padding-top:0!important}.image-loading{display:grid;min-height:330px;place-items:center}.image-loading .ant-spin-text{color:#46516a}.preview-pane>header button.ant-btn.ant-btn-color-primary{min-width:116px;border-color:#5f50d8!important;background:#6657dc!important;color:#fff!important;font-weight:650;text-shadow:none}.preview-pane>header button.ant-btn.ant-btn-color-primary>span,.preview-pane>header button.ant-btn.ant-btn-color-primary .anticon{color:#fff!important}.preview-pane>header button.ant-btn.ant-btn-color-primary:hover{border-color:#5143c2!important;background:#5143c2!important;color:#fff!important}.compression-saving{position:absolute;bottom:24px;left:50%;z-index:1;margin:0;padding:9px 12px;border-radius:4px;background:#fff7e6;color:#ad6800;font-weight:600;text-align:center;transform:translate(-50%);white-space:nowrap}.compression-saving.is-saving{background:#f6ffed;color:#389e0d}.clipboard-tabs{margin:-12px 0 16px}.clipboard-tabs .ant-tabs-nav{margin-bottom:0}.clipboard-list{display:grid;gap:8px}.clipboard-item .ant-card-body{position:relative;display:block;padding:12px 14px}.clipboard-item .ant-card-body>div{width:100%;min-width:0}.clipboard-actions{position:absolute;top:8px;right:8px;display:flex}.clipboard-item pre{width:100%;max-height:160px;overflow:auto;margin:5px 0 0;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.clipboard-original{display:flex;gap:12px;margin-top:8px;font-size:12px}.port-manual-query{margin:-12px 0 16px}.port-query-result{margin-top:12px}.group-task-list{display:grid;gap:8px}.group-task-header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:16px}.group-task-empty{margin:16px 0}.group-task-card{display:flex;align-items:center;justify-content:space-between;gap:16px}.group-task-card>div:first-child{display:grid;gap:2px}.group-task-card .ant-space{flex-wrap:nowrap}.group-task-card .ant-btn{width:72px}.group-task-selector{display:grid;gap:8px;width:100%}.group-task-selector .ant-card-body{padding:10px 14px}.group-task-projects{display:grid;gap:8px}.settings-page{width:100%;max-width:none}.settings-page>.ant-typography{margin:0}.settings-page-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.settings-page-header .ant-typography{margin:0}.settings-page-header>.ant-btn{min-width:96px}.settings-card{width:100%;margin-top:20px;border-radius:14px;border:1px solid #e4e8f1;box-shadow:0 2px 12px #1f2a440d}.settings-card h4.ant-typography{margin:0}.settings-icon-wrap{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex-shrink:0;border-radius:10px;background:linear-gradient(135deg,#ece9fc,#ddd6fe)}.settings-icon{color:#6d5ce7;font-size:18px}.settings-title{display:flex;align-items:center;gap:14px;margin-bottom:24px;padding-bottom:20px;border-bottom:1px solid #eef0f6}.settings-title .ant-typography{margin:0}.settings-title .ant-typography-secondary{font-size:13px;margin-top:2px}.token-settings-form{display:grid;gap:0}.token-row{display:grid;grid-template-columns:200px 1fr;align-items:center;gap:20px;padding:16px 18px;margin:0 -6px;border-radius:10px;border:1px solid transparent;transition:background .18s,border-color .18s}.token-row:hover{background:#f8f9fc;border-color:#eef0f6}.token-row+.token-row{margin-top:2px}.token-row.is-configured{background:#fafbfe}.token-row-info{display:grid;gap:2px;min-width:0}.token-row-name-line{display:flex;align-items:center;gap:8px}.token-row-name{font-size:14px;white-space:nowrap}.token-row-hint{font-size:12px;line-height:1.4}.token-row-tag.ant-tag{margin:0;font-size:11px;line-height:18px;padding-inline:6px;border-radius:4px}.token-row .ant-form-item{margin:0;min-width:0}.token-row .ant-input,.token-row .ant-input-password{border-radius:8px}.script-header{display:flex;align-items:center;gap:16px}.script-header .ant-divider{flex:1;min-width:0;margin:16px 0}.script-header>.ant-btn{flex-shrink:0}.script-editor{margin-bottom:10px}.script-fields{display:grid;grid-template-columns:1fr 1.5fr;gap:12px}.script-fields .ant-form-item{margin-bottom:12px}.log-output{min-height:360px;max-height:58vh;overflow:auto;margin:0;padding:12px 0;border-radius:4px;background:#111827;color:#d1fae5;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}.log-line{min-height:19px}.log-line:hover{background:#ffffff0a}.log-line code{padding:0 15px;overflow-wrap:anywhere;color:inherit;white-space:pre-wrap;font:inherit}.log-link{color:#7dd3fc;text-decoration:underline;text-underline-offset:2px}.log-link:hover{color:#bae6fd}.ansi-black{color:#475569}.ansi-red,.log-error{color:#fb7185}.ansi-green,.log-success{color:#86efac}.ansi-yellow,.log-warning{color:#fcd34d}.ansi-blue{color:#93c5fd}.ansi-magenta{color:#f0abfc}.ansi-cyan{color:#67e8f9}.ansi-gray{color:#94a3b8}.ansi-white{color:#f8fafc}@media(max-width:1100px){.squoosh-studio{grid-template-columns:1fr}.squoosh-settings{border-top:1px solid #e1e5ed}.preview-pane{min-height:440px}}@media(max-width:650px){.sidebar{width:64px!important;min-width:64px!important;padding:20px 8px}.brand{padding:0 10px 25px;font-size:0}.brand span{font-size:20px}.sidebar .ant-menu-title-content,.sidebar-footer{display:none}.sidebar-bottom{right:8px;bottom:20px;left:8px}.page{padding:32px 20px}.service-card,.page-header{align-items:flex-start;flex-direction:column}.script-fields{grid-template-columns:1fr}.token-row{grid-template-columns:1fr;gap:10px;padding:14px 12px}.squoosh-page{min-height:calc(100vh - 64px);margin:-12px -20px -32px;padding:18px 20px 28px}.squoosh-header{align-items:flex-start;flex-direction:column}.squoosh-preview-area{grid-template-columns:1fr}.preview-pane{min-height:360px}.preview-pane+.preview-pane{border-top:1px solid #e1e5ed;border-left:0}.preview-canvas{min-height:300px}}.standalone-log-container{display:flex;flex-direction:column;height:100vh;background:#111827;overflow:hidden}.standalone-log-header{display:flex;justify-content:space-between;align-items:center;padding:12px 24px;background:#1f2937;border-bottom:1px solid #374151;color:#f3f4f6;flex-shrink:0}.standalone-log-body{flex:1;min-height:0;display:flex;flex-direction:column;background:#111827}.standalone-log-body .log-output{flex:1;min-height:0;max-height:none!important;border-radius:0!important;margin:0!important;padding:16px 0}.page.is-static-page{padding:0!important;overflow:hidden}.static-page-container{width:100%;height:100%;overflow:hidden;background:#fff}.static-page-iframe{width:100%;height:100%;border:none;display:block}.sidebar-bottom-inner{display:flex;align-items:center;gap:4px}.sidebar-bottom-inner .ant-menu{flex:1;min-width:0}.sidebar-error-btn{color:#a6adb4!important;border:none!important;background:transparent!important;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;flex-shrink:0;transition:all .2s}.sidebar-error-btn:hover{color:#ff4d4f!important;background:#ff4d4f26!important}.sidebar-error-btn.is-active{color:#fff!important;background:#ff4d4f!important}