artifacty 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,8 +12,9 @@ Every feature follows these architecture rules:
12
12
  - Treat artifact content as untrusted.
13
13
  - Add no runtime dependency unless the section explicitly permits it.
14
14
 
15
- Status recorded on 2026-09-04: all sections below were implemented, and
16
- `STORE_VERSION` was `8`. The original planning text follows.
15
+ Status recorded on 2026-09-04: the original sections 1–21 were implemented, and
16
+ `STORE_VERSION` was `8`. Their original planning text follows. Section 22 is a
17
+ new proposal deferred to the next version; it is not a current capability.
17
18
 
18
19
  Store schema changes increase `STORE_VERSION` (originally `4`). They use the
19
20
  existing `ensureColumn` / `CREATE TABLE IF NOT EXISTS` migration path. Older
@@ -42,6 +43,7 @@ stores upgrade on first access.
42
43
  19. [OpenAPI Specification](#19-openapi-specification)
43
44
  20. [MCP Protocol Refresh](#20-mcp-protocol-refresh)
44
45
  21. [Delivery Plan](#21-delivery-plan)
46
+ 22. [Optional Artifact Encryption at Rest — Planned](#22-optional-artifact-encryption-at-rest--planned)
45
47
 
46
48
  ---
47
49
 
@@ -987,3 +989,30 @@ If the artifact envelope changes, use a `v2` schema document. Increase
987
989
  check.
988
990
  - `/raw` always returns the stored bytes unchanged.
989
991
  - No generated MCP config hard-codes `http://127.0.0.1:8787`.
992
+
993
+
994
+ ---
995
+
996
+ ## 22. Optional Artifact Encryption at Rest — Planned
997
+
998
+ **Status (2026-10-14): deferred to the next version. Design and implementation
999
+ are pending.** This is a substantial storage change, not a current feature.
1000
+ Existing SMTP configuration encryption does not encrypt artifact data.
1001
+
1002
+ An administrator should be able to opt in or turn encryption off. Each change
1003
+ must migrate the existing store between encrypted and plaintext states; a
1004
+ setting that affects only future writes is insufficient. The design must cover
1005
+ artifact/version blobs and bundle assets, metadata, and search indexes, including
1006
+ any retained plaintext copies or derived data. Cipher and storage choices remain
1007
+ open until the full design is reviewed.
1008
+
1009
+ Before migration, create and verify a recovery backup and check available disk
1010
+ space. Hold an exclusive write lock across HTTP, CLI, MCP, and background writers.
1011
+ Verify file references, hashes, and database/index integrity before committing
1012
+ the new state. Define how failures and interrupted migrations recover without
1013
+ leaving a mixed or silently unreadable store.
1014
+
1015
+ Keep encryption keys and recovery material separately secured. The design must
1016
+ specify key backup, restore to a new home, key-loss behavior, and whether disabling
1017
+ encryption leaves any recoverable plaintext or ciphertext copies. Review backup,
1018
+ retention, and recovery behavior before implementing the administrator control.
package/docs/smtp.md ADDED
@@ -0,0 +1,25 @@
1
+ # SMTP and password reset
2
+
3
+ English | [한국어](ko/smtp.md)
4
+
5
+ ## Configure email
6
+
7
+ As an administrator, open **Administration → Email / SMTP** (`/admin/email`). Enter the SMTP host, port, sender address, and TLS mode. Enable delivery when the configuration is ready. Use your own mail service settings; this guide supplies no provider credentials.
8
+
9
+ TLS or STARTTLS is required. Artifacty validates the server certificate and does not fall back to an insecure connection. If the username is empty, SMTP authentication is disabled and the stored password is cleared. With a username, an empty password field keeps the existing password.
10
+
11
+ ## Reset a user's password
12
+
13
+ At `/admin/users`, an administrator can reset a user's password. Artifacty generates a temporary password, requires a password change at the next login, and revokes the user's sessions. Existing API tokens are retained.
14
+
15
+ If SMTP delivery is enabled, Artifacty sends the temporary password to the user's email address. SMTP acceptance does not guarantee inbox delivery. A failed or uncertain send is not retried automatically; the message may already have been accepted. Use the temporary password shown once in the administrator result for manual delivery. Do not repeat the reset solely to retry email.
16
+
17
+ ## Protect the encryption key
18
+
19
+ Artifacty encrypts the complete SMTP configuration in SQLite with AES-256-GCM. The separate key is `ARTIFACTY_HOME/secrets/smtp.key`. On POSIX systems, the directory requires mode `700` and the key file mode `600`.
20
+
21
+ Full JSON backups carry the encrypted configuration, but not its key. Upgrade snapshots also exclude the key. A restore into the same home leaves the existing key untouched. When moving or restoring into a new home, back up and transfer the original key separately with its private permissions.
22
+
23
+ If the key is lost, the encrypted configuration cannot be recovered. Restore the original key before changing settings. Do not generate a replacement key to try to decrypt the old configuration. Keep the key backup private and separate from shared backup bundles.
24
+
25
+ See [upgrade snapshots and recovery](upgrade-backups.md) for store restore procedures.
@@ -29,6 +29,22 @@ The command uses POSIX Linux shell syntax. After a successful update, it puts `~
29
29
 
30
30
  The `latest` tag must resolve to version 0.14.0 or newer to include the updater.
31
31
 
32
+ ### Update from the browser
33
+
34
+ Sign in with an administrator browser session. Open **Administration → Server update** (`/admin/update`). API tokens do not replace that session for these routes.
35
+
36
+ 1. Select **Check for updates**. This checks the latest npm release without installing or restarting the service.
37
+ 2. If browser updates are supported and a newer version is available, review the displayed target version.
38
+ 3. Select the confirmation checkbox for backup and restart.
39
+ 4. Select **Back up and update**.
40
+ 5. Keep the page open while the server restarts. Read the update result after reconnection.
41
+
42
+ Confirmation expires after ten minutes and belongs to the browser session that checked the version. If it expires or the installed version changes, check again. Form submissions require the exact same origin as the server. No password re-entry is required.
43
+
44
+ Browser installation requires this server to be the non-root Linux systemd user service. Other installations can check the latest release, but cannot start the update from this page. The worker runs in an independent systemd transient service, so stopping the Artifacty server does not stop the update worker. Status is stored outside the artifact store under `~/.local/share/artifacty/update-jobs`.
45
+
46
+ Opening the page or reading status does not contact npm or install a package. Status checks can inspect systemd to identify a stopped worker. If dispatch is uncertain, the job remains reserved. An interrupted worker or incomplete recovery requires manual inspection before another update; it is not reported as success. Keep the retained snapshot and releases until recovery is verified.
47
+
32
48
  ### Automatic installation limits
33
49
 
34
50
  Automatic installation and restart support Linux with the systemd user service `com.artifacty.server`. Run the updater as the service's user. The unit must use the simple `ExecStart` form produced by Artifacty's installer and must explicitly pass `--home` for the selected store. Unsupported command expansion or quoting stops the update before service shutdown. Automatic updates reject root execution. Do not run the updater as root to replace a root-owned global package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -29,6 +29,7 @@
29
29
  "mcp": "node src/mcp-server.js",
30
30
  "test": "node --test",
31
31
  "lint": "node scripts/lint.mjs",
32
+ "build:ui": "node scripts/build-ui.mjs",
32
33
  "smoke": "bash scripts/smoke.sh",
33
34
  "release:check": "npm run lint && npm test && npm run smoke"
34
35
  },
@@ -38,15 +39,18 @@
38
39
  "docs/assets/artifacty.png",
39
40
  "docs/central-team-deployment-design.md",
40
41
  "docs/integrations.md",
42
+ "docs/language-packs.md",
41
43
  "docs/mcp-public-api.md",
42
44
  "docs/network-sharing.md",
43
45
  "docs/release-checklist.md",
44
46
  "docs/roadmap-design.md",
45
47
  "docs/sarif-csv-artifact-plan.md",
48
+ "docs/smtp.md",
46
49
  "docs/threat-model.md",
47
50
  "docs/upgrade-backups.md",
48
51
  "docs/writing-style.md",
49
52
  "scripts/lint.mjs",
53
+ "scripts/build-ui.mjs",
50
54
  "scripts/smoke.sh",
51
55
  "README.md",
52
56
  "LICENSE",
@@ -76,5 +80,10 @@
76
80
  "@babel/standalone": "^8.0.4",
77
81
  "react": "^18.3.1",
78
82
  "react-dom": "^18.3.1"
83
+ },
84
+ "devDependencies": {
85
+ "@tabler/icons": "^3.49.0",
86
+ "bootstrap": "^5.3.8",
87
+ "sass": "^1.105.1"
79
88
  }
80
89
  }
@@ -0,0 +1,16 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import * as sass from 'sass';
3
+ const root = new URL('../', import.meta.url);
4
+ const result = sass.compile(new URL('src/styles/ui-controls.scss', root).pathname, {
5
+ loadPaths: [new URL('node_modules', root).pathname], style: 'compressed',
6
+ quietDeps: true, silenceDeprecations: ['import', 'global-builtin', 'color-functions', 'if-function'],
7
+ });
8
+ await writeFile(new URL('src/client/ui-controls.css', root), '/*! Bootstrap v5.3.8 | MIT | https://getbootstrap.com/ | Generated by npm run build:ui */\n' + result.css + '\n');
9
+ const names = ['layout-sidebar-left-collapse','layout-sidebar-left-expand','files','inbox','plus','upload','settings','users','database-export','archive','webhook','refresh','language','mail','user-circle','key','logout','login','code','chevron-down','device-desktop','sun','moon'];
10
+ const icons = {};
11
+ for (const name of names) {
12
+ const svg = await readFile(new URL(`node_modules/@tabler/icons/icons/outline/${name}.svg`, root), 'utf8');
13
+ icons[name] = svg.slice(svg.indexOf('>') + 1, svg.lastIndexOf('</svg>')).trim().replace(/\s+/g, ' ');
14
+ }
15
+ await writeFile(new URL('src/lib/ui-icons.js', root), `// Generated from @tabler/icons v3.49.0 (MIT), by npm run build:ui.\nconst paths = ${JSON.stringify(icons, null, 2)};\n\nexport function uiIcon(name, { size = 18 } = {}) {\n if (!Object.hasOwn(paths, name)) throw new Error('Unknown UI icon');\n const dimension = [16, 18, 20, 24].includes(size) ? size : 18;\n return '<svg class="ui-icon" width="' + dimension + '" height="' + dimension + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' + paths[name] + '</svg>';\n}\n`);
16
+ console.log('Built local Bootstrap controls and 23 Tabler icons.');
package/src/cli.js CHANGED
@@ -503,7 +503,7 @@ async function main() {
503
503
  const id = options._[0];
504
504
  const visibility = options._[1];
505
505
  if (!id || !visibility) {
506
- throw new Error("visibility requires an artifact id and a value: private or team");
506
+ throw new Error("visibility requires an artifact id and a value: private, team, or public");
507
507
  }
508
508
  const artifact = await setArtifactVisibility(store, id, visibility, { audit: cliAuditContext() });
509
509
  printJson(await withUrls(store, artifact));
@@ -1006,17 +1006,17 @@ Usage:
1006
1006
  artifacty status [--home ~/.artifacty]
1007
1007
  artifacty stop [--home ~/.artifacty] [--timeout 30000] [--force]
1008
1008
  artifacty doctor [--home ~/.artifacty] [--skip-mcp] [--timeout 5000]
1009
- artifacty publish --title <title> (--file <path> | --content <text>) [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--source agent] [--tag tag] [--visibility private|team]
1009
+ artifacty publish --title <title> (--file <path> | --content <text>) [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--source agent] [--tag tag] [--visibility private|team|public]
1010
1010
  artifacty import --agent claude|codex|gemini|copilot|cursor|auto (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--tag tag]
1011
1011
  artifacty install claude|codex|gemini|copilot|cursor|all [--dry-run] [--config <path>] [--server-path <path>] [--url http://127.0.0.1:8787] [--mcp-url http://127.0.0.1:8787/mcp] [--api-token token] [--transport local|bridge] [--timeout 30000]
1012
1012
  artifacty check [--server-path <path>] [--timeout 5000]
1013
1013
  artifacty update [--check] [--target-version <version>] [--home <path>]
1014
1014
  artifacty upgrade [--check] [--target-version <version>] [--home <path>]
1015
- artifacty update <id> (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--expected-version N] [--visibility private|team]
1015
+ artifacty update <id> (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--expected-version N] [--visibility private|team|public]
1016
1016
  artifacty samples install|remove
1017
1017
  artifacty archive <id>
1018
1018
  artifacty restore <id>
1019
- artifacty visibility <id> private|team
1019
+ artifacty visibility <id> private|team|public
1020
1020
  artifacty audit [--artifact <id>] [--limit 100]
1021
1021
  artifacty index rebuild [--embeddings]
1022
1022
  artifacty integrity
@@ -0,0 +1,32 @@
1
+ // Progressive enhancement only: native disclosures and examples work without JavaScript.
2
+ export function matchesOperation(searchText, query) {
3
+ return query.trim().toLowerCase().split(/\s+/).every((word) => searchText.toLowerCase().includes(word));
4
+ }
5
+
6
+ export function enhanceApiDocs(document, clipboard) {
7
+ const search = document.querySelector('#api-search');
8
+ const operations = [...document.querySelectorAll('[data-operation]')];
9
+ if (search) {
10
+ document.querySelector('[data-search-controls]').hidden = false;
11
+ search.addEventListener('input', () => {
12
+ for (const operation of operations) operation.hidden = !matchesOperation(operation.dataset.search, search.value);
13
+ for (const group of document.querySelectorAll('[data-tag-group]')) group.hidden = ![...group.querySelectorAll('[data-operation]')].some((operation) => !operation.hidden);
14
+ document.querySelector('[data-no-results]').hidden = operations.some((operation) => !operation.hidden);
15
+ });
16
+ }
17
+ if (!clipboard?.writeText) return;
18
+ for (const button of document.querySelectorAll('[data-copy]')) {
19
+ button.hidden = false;
20
+ button.addEventListener('click', async () => {
21
+ const status = document.querySelector('[data-copy-status]');
22
+ try {
23
+ await clipboard.writeText(button.parentElement.querySelector('code').textContent);
24
+ status.textContent = document.body.dataset.copySuccess;
25
+ } catch {
26
+ status.textContent = document.body.dataset.copyFailure;
27
+ }
28
+ });
29
+ }
30
+ }
31
+
32
+ if (typeof document !== 'undefined') enhanceApiDocs(document, navigator.clipboard);
@@ -0,0 +1,20 @@
1
+ // Only opt-in GET forms auto-apply. Account and server mutations stay explicit.
2
+ export function enhanceAutoApply(document) {
3
+ for (const form of document.querySelectorAll('form[data-auto-apply]')) {
4
+ if (form.method.toLowerCase() !== 'get' || typeof form.requestSubmit !== 'function') continue;
5
+ const controls = form.dataset.autoApply === 'filters'
6
+ ? form.querySelectorAll('.dashboard-advanced select, .dashboard-advanced input')
7
+ : form.querySelectorAll('select');
8
+ for (const control of controls) {
9
+ control.addEventListener('change', () => {
10
+ if (!form.reportValidity()) return;
11
+ form.requestSubmit();
12
+ });
13
+ }
14
+ const buttons = form.dataset.autoApply === 'filters'
15
+ ? form.querySelectorAll('.dashboard-filter-actions')
16
+ : form.querySelectorAll('button[type="submit"]');
17
+ for (const button of buttons) button.hidden = true;
18
+ }
19
+ }
20
+ if (typeof document !== 'undefined') enhanceAutoApply(document);
@@ -331,28 +331,83 @@ function invalidJsonMessage(error) {
331
331
  return messages.invalidJson.replaceAll("{message}", error.message);
332
332
  }
333
333
 
334
+ // Deliberately renders a small, escaped Markdown subset. Links and raw HTML
335
+ // remain text; previewing a document must not activate its URLs or scripts.
334
336
  function markdownPreview(content) {
335
- return String(content || "")
336
- .split(/\r?\n/)
337
- .map((line) => {
338
- if (/^###\s+/.test(line)) {
339
- return `<h3>${escapeHtml(line.replace(/^###\s+/, ""))}</h3>`;
340
- }
341
- if (/^##\s+/.test(line)) {
342
- return `<h2>${escapeHtml(line.replace(/^##\s+/, ""))}</h2>`;
343
- }
344
- if (/^#\s+/.test(line)) {
345
- return `<h1>${escapeHtml(line.replace(/^#\s+/, ""))}</h1>`;
346
- }
347
- if (/^[-*]\s+/.test(line)) {
348
- return `<p>• ${escapeHtml(line.replace(/^[-*]\s+/, ""))}</p>`;
349
- }
350
- if (!line.trim()) {
351
- return "<br>";
337
+ const lines = String(content || "").split(/\r?\n/);
338
+ const output = [];
339
+ let paragraph = [];
340
+ let list = "";
341
+ const inline = (value) => escapeHtml(value).replace(/`([^`]+)`/g, "<code>$1</code>");
342
+ const flush = () => {
343
+ if (paragraph.length) {
344
+ output.push(`<p>${inline(paragraph.join(" "))}</p>`);
345
+ paragraph = [];
346
+ }
347
+ if (list) {
348
+ output.push(`</${list}>`);
349
+ list = "";
350
+ }
351
+ };
352
+ const cells = (line) => line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
353
+ const tableRow = (line = "") => /^\s*\|.*\|\s*$/.test(line);
354
+
355
+ for (let index = 0; index < lines.length; index += 1) {
356
+ const line = lines[index];
357
+ const fence = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
358
+ if (fence) {
359
+ flush();
360
+ const language = /^[a-zA-Z0-9+#_.-]+$/.test(fence[2].trim()) ? fence[2].trim().toLowerCase() : "";
361
+ const source = [];
362
+ const closing = new RegExp(`^ {0,3}${fence[1][0]}{${fence[1].length},}\\s*$`);
363
+ while (++index < lines.length && !closing.test(lines[index])) source.push(lines[index]);
364
+ output.push(`<pre><code${language ? ` class="language-${language}"` : ""}>${escapeHtml(source.join("\n"))}</code></pre>`);
365
+ continue;
366
+ }
367
+ if (!line.trim()) {
368
+ flush();
369
+ continue;
370
+ }
371
+ const headers = cells(line);
372
+ const separators = cells(lines[index + 1] || "");
373
+ if (tableRow(line) && headers.length === separators.length && separators.every((cell) => /^:?-{3,}:?$/.test(cell))) {
374
+ flush();
375
+ const alignment = (column) => {
376
+ const value = separators[column];
377
+ return value.endsWith(":") ? (value.startsWith(":") ? "center" : "right") : "left";
378
+ };
379
+ const row = (values, tag) => `<tr>${headers.map((_, column) => `<${tag} style="text-align:${alignment(column)}">${inline(values[column] || "")}</${tag}>`).join("")}</tr>`;
380
+ output.push(`<div class="artifact-table-scroll"><table class="artifact-table"><thead>${row(headers, "th")}</thead><tbody>`);
381
+ index += 1;
382
+ while (tableRow(lines[index + 1])) output.push(row(cells(lines[++index]), "td"));
383
+ output.push("</tbody></table></div>");
384
+ continue;
385
+ }
386
+ const heading = /^(#{1,6})\s+(.+)$/.exec(line);
387
+ if (heading) {
388
+ flush();
389
+ output.push(`<h${heading[1].length}>${inline(heading[2])}</h${heading[1].length}>`);
390
+ continue;
391
+ }
392
+ const item = /^([-*+] |\d+[.)] )\s*(.+)$/.exec(line);
393
+ if (item) {
394
+ const type = /^\d/.test(item[1]) ? "ol" : "ul";
395
+ if (paragraph.length || (list && list !== type)) flush();
396
+ if (!list) {
397
+ output.push(`<${type}>`);
398
+ list = type;
352
399
  }
353
- return `<p>${escapeHtml(line)}</p>`;
354
- })
355
- .join("");
400
+ const task = /^\[([ xX])\]\s+(.+)$/.exec(item[2]);
401
+ output.push(task
402
+ ? `<li class="task-list-item"><label><input type="checkbox" disabled${task[1].toLowerCase() === "x" ? " checked" : ""}> ${inline(task[2])}</label></li>`
403
+ : `<li>${inline(item[2])}</li>`);
404
+ continue;
405
+ }
406
+ if (list) flush();
407
+ paragraph.push(line.trim());
408
+ }
409
+ flush();
410
+ return output.join("\n");
356
411
  }
357
412
 
358
413
  function escapeHtml(value) {
@@ -0,0 +1,31 @@
1
+ let sidebarCollapsed = false;
2
+ try { sidebarCollapsed = localStorage.getItem("artifacty_sidebar") === "collapsed"; } catch { /* Default expanded. */ }
3
+
4
+ function applySidebar(collapsed) {
5
+ sidebarCollapsed = collapsed;
6
+ if (collapsed) document.documentElement.setAttribute("data-sidebar", "collapsed");
7
+ else document.documentElement.removeAttribute("data-sidebar");
8
+ for (const button of document.querySelectorAll("[data-sidebar-toggle]")) {
9
+ button.setAttribute("aria-expanded", String(!collapsed));
10
+ const label = collapsed ? button.dataset.expandLabel : button.dataset.collapseLabel;
11
+ button.setAttribute("aria-label", label);
12
+ button.title = label;
13
+ button.querySelector("[data-sidebar-collapse-icon]").hidden = collapsed;
14
+ button.querySelector("[data-sidebar-expand-icon]").hidden = !collapsed;
15
+ }
16
+ }
17
+ applySidebar(sidebarCollapsed);
18
+ document.addEventListener("DOMContentLoaded", () => {
19
+ for (const button of document.querySelectorAll("[data-sidebar-toggle]")) {
20
+ button.hidden = false;
21
+ button.addEventListener("click", () => {
22
+ applySidebar(!sidebarCollapsed);
23
+ try { localStorage.setItem("artifacty_sidebar", sidebarCollapsed ? "collapsed" : "expanded"); } catch { /* Current page remains usable. */ }
24
+ });
25
+ }
26
+ applySidebar(sidebarCollapsed);
27
+ });
28
+ window.addEventListener("storage", (event) => {
29
+ if (event.key !== "artifacty_sidebar" && event.key !== null) return;
30
+ try { applySidebar(localStorage.getItem("artifacty_sidebar") === "collapsed"); } catch { /* Retain the current page choice. */ }
31
+ });
@@ -0,0 +1,42 @@
1
+ const themeChoices = new Set(["system", "light", "dark"]);
2
+ let selectedTheme = "system";
3
+ const systemTheme = window.matchMedia?.("(prefers-color-scheme: dark)");
4
+
5
+ function readSavedTheme() {
6
+ try {
7
+ const saved = localStorage.getItem("artifacty_theme");
8
+ return themeChoices.has(saved) ? saved : "system";
9
+ } catch {
10
+ return "system";
11
+ }
12
+ }
13
+
14
+ function applyTheme(theme) {
15
+ selectedTheme = themeChoices.has(theme) ? theme : "system";
16
+ if (selectedTheme === "system") document.documentElement.removeAttribute("data-theme");
17
+ else document.documentElement.setAttribute("data-theme", selectedTheme);
18
+ document.documentElement.setAttribute("data-bs-theme", selectedTheme === "system" ? (systemTheme?.matches ? "dark" : "light") : selectedTheme);
19
+ for (const button of document.querySelectorAll("[data-theme-choice]")) {
20
+ button.setAttribute("aria-pressed", String(button.dataset.themeChoice === selectedTheme));
21
+ }
22
+ }
23
+
24
+ applyTheme(readSavedTheme());
25
+ systemTheme?.addEventListener("change", () => { if (selectedTheme === "system") applyTheme("system"); });
26
+
27
+ document.addEventListener("DOMContentLoaded", () => {
28
+ for (const control of document.querySelectorAll("[data-theme-controls]")) control.hidden = false;
29
+ for (const button of document.querySelectorAll("[data-theme-choice]")) {
30
+ button.addEventListener("click", () => {
31
+ const choice = button.dataset.themeChoice;
32
+ if (!themeChoices.has(choice)) return;
33
+ applyTheme(choice);
34
+ try { localStorage.setItem("artifacty_theme", choice); } catch { /* Keep the current page usable when storage is blocked. */ }
35
+ });
36
+ }
37
+ applyTheme(selectedTheme);
38
+ });
39
+
40
+ window.addEventListener("storage", (event) => {
41
+ if (event.key === "artifacty_theme" || event.key === null) applyTheme(readSavedTheme());
42
+ });
@@ -0,0 +1,2 @@
1
+ /*! Bootstrap v5.3.8 | MIT | https://getbootstrap.com/ | Generated by npm run build:ui */
2
+ :root,[data-bs-theme=light]{--bs-blue: #0d6efd;--bs-indigo: #6610f2;--bs-purple: #6f42c1;--bs-pink: #d63384;--bs-red: #dc3545;--bs-orange: #fd7e14;--bs-yellow: #ffc107;--bs-green: #198754;--bs-teal: #20c997;--bs-cyan: #0dcaf0;--bs-black: #000;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #343a40;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #343a40;--bs-gray-900: #212529;--bs-primary: #315ca8;--bs-secondary: #58616e;--bs-success: #198754;--bs-info: #0dcaf0;--bs-warning: #ffc107;--bs-danger: #b42332;--bs-light: #f8f9fa;--bs-dark: #212529;--bs-primary-rgb: 49, 92, 168;--bs-secondary-rgb: 88, 97, 110;--bs-success-rgb: 25, 135, 84;--bs-info-rgb: 13, 202, 240;--bs-warning-rgb: 255, 193, 7;--bs-danger-rgb: 180, 35, 50;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 33, 37, 41;--bs-primary-text-emphasis: rgb(7.6862745098%, 14.431372549%, 26.3529411765%);--bs-secondary-text-emphasis: rgb(13.8039215686%, 15.2156862745%, 17.2549019608%);--bs-success-text-emphasis: rgb(3.9215686275%, 21.1764705882%, 13.1764705882%);--bs-info-text-emphasis: rgb(2.0392156863%, 31.6862745098%, 37.6470588235%);--bs-warning-text-emphasis: rgb(40%, 30.2745098039%, 1.0980392157%);--bs-danger-text-emphasis: #480e14;--bs-light-text-emphasis: #495057;--bs-dark-text-emphasis: #495057;--bs-primary-bg-subtle: rgb(83.8431372549%, 87.2156862745%, 93.1764705882%);--bs-secondary-bg-subtle: rgb(86.9019607843%, 87.6078431373%, 88.6274509804%);--bs-success-bg-subtle: rgb(81.9607843137%, 90.5882352941%, 86.5882352941%);--bs-info-bg-subtle: rgb(81.0196078431%, 95.8431372549%, 98.8235294118%);--bs-warning-bg-subtle: rgb(100%, 95.137254902%, 80.5490196078%);--bs-danger-bg-subtle: #f0d3d6;--bs-light-bg-subtle: rgb(98.6274509804%, 98.8235294118%, 99.0196078431%);--bs-dark-bg-subtle: #ced4da;--bs-primary-border-subtle: rgb(67.6862745098%, 74.431372549%, 86.3529411765%);--bs-secondary-border-subtle: rgb(73.8039215686%, 75.2156862745%, 77.2549019608%);--bs-success-border-subtle: rgb(63.9215686275%, 81.1764705882%, 73.1764705882%);--bs-info-border-subtle: rgb(62.0392156863%, 91.6862745098%, 97.6470588235%);--bs-warning-border-subtle: rgb(100%, 90.2745098039%, 61.0980392157%);--bs-danger-border-subtle: #e1a7ad;--bs-light-border-subtle: #e9ecef;--bs-dark-border-subtle: #adb5bd;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-font-sans-serif: ui-sans-serif, system-ui, -apple-system, "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family: var(--bs-font-sans-serif);--bs-body-font-size:0.875rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #212529;--bs-body-color-rgb: 33, 37, 41;--bs-body-bg: #fff;--bs-body-bg-rgb: 255, 255, 255;--bs-emphasis-color: #000;--bs-emphasis-color-rgb: 0, 0, 0;--bs-secondary-color: rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb: 33, 37, 41;--bs-secondary-bg: #e9ecef;--bs-secondary-bg-rgb: 233, 236, 239;--bs-tertiary-color: rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb: 33, 37, 41;--bs-tertiary-bg: #f8f9fa;--bs-tertiary-bg-rgb: 248, 249, 250;--bs-heading-color: inherit;--bs-link-color: #315ca8;--bs-link-color-rgb: 49, 92, 168;--bs-link-decoration: underline;--bs-link-hover-color: rgb(15.3725490196%, 28.862745098%, 52.7058823529%);--bs-link-hover-color-rgb: 39, 74, 134;--bs-code-color: #d63384;--bs-highlight-color: #212529;--bs-highlight-bg: rgb(100%, 95.137254902%, 80.5490196078%);--bs-border-width: 1px;--bs-border-style: solid;--bs-border-color: #dee2e6;--bs-border-color-translucent: rgba(0, 0, 0, 0.175);--bs-border-radius: 0.375rem;--bs-border-radius-sm: 0.25rem;--bs-border-radius-lg: 0.5rem;--bs-border-radius-xl: 1rem;--bs-border-radius-xxl: 2rem;--bs-border-radius-2xl: var(--bs-border-radius-xxl);--bs-border-radius-pill: 50rem;--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width: 0.25rem;--bs-focus-ring-opacity: 0.25;--bs-focus-ring-color: rgba(49, 92, 168, 0.25);--bs-form-valid-color: #198754;--bs-form-valid-border-color: #198754;--bs-form-invalid-color: #b42332;--bs-form-invalid-border-color: #b42332}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color: #dee2e6;--bs-body-color-rgb: 222, 226, 230;--bs-body-bg: #212529;--bs-body-bg-rgb: 33, 37, 41;--bs-emphasis-color: #fff;--bs-emphasis-color-rgb: 255, 255, 255;--bs-secondary-color: rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb: 222, 226, 230;--bs-secondary-bg: #343a40;--bs-secondary-bg-rgb: 52, 58, 64;--bs-tertiary-color: rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb: 222, 226, 230;--bs-tertiary-bg: rgb(16.6666666667%, 18.6274509804%, 20.5882352941%);--bs-tertiary-bg-rgb: 43, 48, 53;--bs-primary-text-emphasis: rgb(51.5294117647%, 61.6470588235%, 79.5294117647%);--bs-secondary-text-emphasis: rgb(60.7058823529%, 62.8235294118%, 65.8823529412%);--bs-success-text-emphasis: rgb(45.8823529412%, 71.7647058824%, 59.7647058824%);--bs-info-text-emphasis: rgb(43.0588235294%, 87.5294117647%, 96.4705882353%);--bs-warning-text-emphasis: rgb(100%, 85.4117647059%, 41.6470588235%);--bs-danger-text-emphasis: #d27b84;--bs-light-text-emphasis: #f8f9fa;--bs-dark-text-emphasis: #dee2e6;--bs-primary-bg-subtle: rgb(3.8431372549%, 7.2156862745%, 13.1764705882%);--bs-secondary-bg-subtle: rgb(6.9019607843%, 7.6078431373%, 8.6274509804%);--bs-success-bg-subtle: rgb(1.9607843137%, 10.5882352941%, 6.5882352941%);--bs-info-bg-subtle: rgb(1.0196078431%, 15.8431372549%, 18.8235294118%);--bs-warning-bg-subtle: rgb(20%, 15.137254902%, 0.5490196078%);--bs-danger-bg-subtle: #24070a;--bs-light-bg-subtle: #343a40;--bs-dark-bg-subtle: #1a1d20;--bs-primary-border-subtle: rgb(11.5294117647%, 21.6470588235%, 39.5294117647%);--bs-secondary-border-subtle: rgb(20.7058823529%, 22.8235294118%, 25.8823529412%);--bs-success-border-subtle: rgb(5.8823529412%, 31.7647058824%, 19.7647058824%);--bs-info-border-subtle: rgb(3.0588235294%, 47.5294117647%, 56.4705882353%);--bs-warning-border-subtle: rgb(60%, 45.4117647059%, 1.6470588235%);--bs-danger-border-subtle: #6c151e;--bs-light-border-subtle: #495057;--bs-dark-border-subtle: #343a40;--bs-heading-color: inherit;--bs-link-color: rgb(51.5294117647%, 61.6470588235%, 79.5294117647%);--bs-link-hover-color: rgb(61.2235294118%, 69.3176470588%, 83.6235294118%);--bs-link-color-rgb: 131, 157, 203;--bs-link-hover-color-rgb: 156, 177, 213;--bs-code-color: rgb(90.3529411765%, 52%, 71.0588235294%);--bs-highlight-color: #dee2e6;--bs-highlight-bg: rgb(40%, 30.2745098039%, 1.0980392157%);--bs-border-color: #495057;--bs-border-color-translucent: rgba(255, 255, 255, 0.15);--bs-form-valid-color: rgb(45.8823529412%, 71.7647058824%, 59.7647058824%);--bs-form-valid-border-color: rgb(45.8823529412%, 71.7647058824%, 59.7647058824%);--bs-form-invalid-color: rgb(91.7647058824%, 52.4705882353%, 56.2352941176%);--bs-form-invalid-border-color: rgb(91.7647058824%, 52.4705882353%, 56.2352941176%)}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(0.4375rem + var(--bs-border-width));padding-bottom:calc(0.4375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + var(--bs-border-width));padding-bottom:calc(0.5rem + var(--bs-border-width));font-size:1.09375rem}.col-form-label-sm{padding-top:calc(0.25rem + var(--bs-border-width));padding-bottom:calc(0.25rem + var(--bs-border-width));font-size:0.765625rem}.form-text{margin-top:.25rem;font-size:0.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.4375rem .75rem;font-size:0.8125rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:rgb(59.6078431373%,68.0392156863%,82.9411764706%);outline:0;box-shadow:0 0 0 .25rem rgba(49,92,168,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::file-selector-button{padding:.4375rem .75rem;margin:-0.4375rem -0.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.4375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:rgba(0,0,0,0);border:solid rgba(0,0,0,0);border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:0.765625rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.09375rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + 0.875rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + 0.875rem + calc(var(--bs-border-width) * 2));padding:.4375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0 !important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0 !important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.4375rem 2.25rem .4375rem .75rem;font-size:0.8125rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon, none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-select{transition:none}}.form-select:focus{border-color:rgb(59.6078431373%,68.0392156863%,82.9411764706%);outline:0;box-shadow:0 0 0 .1875rem rgba(49,92,168,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:rgba(0,0,0,0);text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:0.765625rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.09375rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.3125rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg: var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:rgb(59.6078431373%,68.0392156863%,82.9411764706%);outline:0;box-shadow:0 0 0 .25rem rgba(49,92,168,.25)}.form-check-input:checked{background-color:#315ca8;border-color:#315ca8}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#315ca8;border-color:#315ca8;--bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.form-check-input:disabled~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgb%2859.6078431373%, 68.0392156863%, 82.9411764706%%29'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.375rem;padding:0;appearance:none;background-color:rgba(0,0,0,0)}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(49,92,168,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(49,92,168,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;appearance:none;background-color:#315ca8;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-range::-webkit-slider-thumb{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:rgb(75.7647058824%,80.8235294118%,89.7647058824%)}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:var(--bs-secondary-bg);border-color:rgba(0,0,0,0);border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;appearance:none;background-color:#315ca8;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-range::-moz-range-thumb{transition:none}}.form-range::-moz-range-thumb:active{background-color:rgb(75.7647058824%,80.8235294118%,89.7647058824%)}.form-range::-moz-range-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:var(--bs-secondary-bg);border-color:rgba(0,0,0,0);border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem .75rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb), 0.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid rgba(0,0,0,0);transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media(prefers-reduced-motion: reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control::placeholder,.form-floating>.form-control-plaintext::placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown),.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill,.form-floating>.form-control-plaintext:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:.75rem}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-control-plaintext~label,.form-floating>.form-select~label{transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem .375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>textarea:disabled~label::after{background-color:var(--bs-secondary-bg)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>:disabled~label,.form-floating>.form-control:disabled~label{color:#6c757d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select,.input-group>.form-floating{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus,.input-group>.form-floating:focus-within{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.4375rem .75rem;font-size:0.8125rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:1.09375rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:0.765625rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select{border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating),.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(-1*var(--bs-border-width));border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.765625rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + 0.875rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.21875rem) center;background-size:calc(0.75em + 0.4375rem) calc(0.75em + 0.4375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .1875rem rgba(var(--bs-success-rgb), 0.2)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.875rem);background-position:top calc(0.375em + 0.21875rem) right calc(0.375em + 0.21875rem)}.was-validated .form-select:valid,.form-select.is-valid{border-color:var(--bs-form-valid-border-color)}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.4375rem) calc(0.75em + 0.4375rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .1875rem rgba(var(--bs-success-rgb), 0.2)}.was-validated .form-control-color:valid,.form-control-color.is-valid{width:calc(3rem + calc(1.5em + 0.875rem))}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:var(--bs-form-valid-border-color)}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:var(--bs-form-valid-color)}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .1875rem rgba(var(--bs-success-rgb), 0.2)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):valid,.input-group>.form-control:not(:focus).is-valid,.was-validated .input-group>.form-select:not(:focus):valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.input-group>.form-floating:not(:focus-within).is-valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.765625rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + 0.875rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23b42332'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23b42332' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.21875rem) center;background-size:calc(0.75em + 0.4375rem) calc(0.75em + 0.4375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .1875rem rgba(var(--bs-danger-rgb), 0.2)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.875rem);background-position:top calc(0.375em + 0.21875rem) right calc(0.375em + 0.21875rem)}.was-validated .form-select:invalid,.form-select.is-invalid{border-color:var(--bs-form-invalid-border-color)}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23b42332'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23b42332' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.4375rem) calc(0.75em + 0.4375rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .1875rem rgba(var(--bs-danger-rgb), 0.2)}.was-validated .form-control-color:invalid,.form-control-color.is-invalid{width:calc(3rem + calc(1.5em + 0.875rem))}.was-validated .form-check-input:invalid,.form-check-input.is-invalid{border-color:var(--bs-form-invalid-border-color)}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked{background-color:var(--bs-form-invalid-color)}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus{box-shadow:0 0 0 .1875rem rgba(var(--bs-danger-rgb), 0.2)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group>.form-control:not(:focus):invalid,.input-group>.form-control:not(:focus).is-invalid,.was-validated .input-group>.form-select:not(:focus):invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.input-group>.form-floating:not(:focus-within).is-invalid{z-index:4}.btn{--bs-btn-padding-x: 0.75rem;--bs-btn-padding-y: 0.4375rem;--bs-btn-font-family: ;--bs-btn-font-size:0.8125rem;--bs-btn-font-weight: 500;--bs-btn-line-height: 1.5;--bs-btn-color: var(--bs-body-color);--bs-btn-bg: transparent;--bs-btn-border-width: var(--bs-border-width);--bs-btn-border-color: transparent;--bs-btn-border-radius: var(--bs-border-radius);--bs-btn-hover-border-color: transparent;--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity: 0.65;--bs-btn-focus-box-shadow: 0 0 0 0.1875rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;white-space:nowrap;vertical-align:middle;cursor:pointer;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,:not(.btn-check)+.btn:active,.btn:first-child:active,.btn.active,.btn.show{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,:not(.btn-check)+.btn:active:focus-visible,.btn:first-child:active:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color: #fff;--bs-btn-bg: #315ca8;--bs-btn-border-color: #315ca8;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: rgb(16.3333333333%, 30.6666666667%, 56%);--bs-btn-hover-border-color: rgb(15.3725490196%, 28.862745098%, 52.7058823529%);--bs-btn-focus-shadow-rgb: 80, 116, 181;--bs-btn-active-color: #fff;--bs-btn-active-bg: rgb(15.3725490196%, 28.862745098%, 52.7058823529%);--bs-btn-active-border-color: rgb(14.4117647059%, 27.0588235294%, 49.4117647059%);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #315ca8;--bs-btn-disabled-border-color: #315ca8}.btn-secondary{--bs-btn-color: #fff;--bs-btn-bg: #58616e;--bs-btn-border-color: #58616e;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: rgb(29.3333333333%, 32.3333333333%, 36.6666666667%);--bs-btn-hover-border-color: rgb(27.6078431373%, 30.431372549%, 34.5098039216%);--bs-btn-focus-shadow-rgb: 113, 121, 132;--bs-btn-active-color: #fff;--bs-btn-active-bg: rgb(27.6078431373%, 30.431372549%, 34.5098039216%);--bs-btn-active-border-color: rgb(25.8823529412%, 28.5294117647%, 32.3529411765%);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #58616e;--bs-btn-disabled-border-color: #58616e}.btn-success{--bs-btn-color: #fff;--bs-btn-bg: #198754;--bs-btn-border-color: #198754;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: rgb(8.3333333333%, 45%, 28%);--bs-btn-hover-border-color: rgb(7.8431372549%, 42.3529411765%, 26.3529411765%);--bs-btn-focus-shadow-rgb: 60, 153, 110;--bs-btn-active-color: #fff;--bs-btn-active-bg: rgb(7.8431372549%, 42.3529411765%, 26.3529411765%);--bs-btn-active-border-color: rgb(7.3529411765%, 39.7058823529%, 24.7058823529%);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #198754;--bs-btn-disabled-border-color: #198754}.btn-info{--bs-btn-color: #000;--bs-btn-bg: #0dcaf0;--bs-btn-border-color: #0dcaf0;--bs-btn-hover-color: #000;--bs-btn-hover-bg: rgb(19.3333333333%, 82.3333333333%, 95%);--bs-btn-hover-border-color: rgb(14.5882352941%, 81.2941176471%, 94.7058823529%);--bs-btn-focus-shadow-rgb: 11, 172, 204;--bs-btn-active-color: #000;--bs-btn-active-bg: rgb(24.0784313725%, 83.3725490196%, 95.2941176471%);--bs-btn-active-border-color: rgb(14.5882352941%, 81.2941176471%, 94.7058823529%);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #0dcaf0;--bs-btn-disabled-border-color: #0dcaf0}.btn-warning{--bs-btn-color: #000;--bs-btn-bg: #ffc107;--bs-btn-border-color: #ffc107;--bs-btn-hover-color: #000;--bs-btn-hover-bg: rgb(100%, 79.3333333333%, 17.3333333333%);--bs-btn-hover-border-color: rgb(100%, 78.1176470588%, 12.4705882353%);--bs-btn-focus-shadow-rgb: 217, 164, 6;--bs-btn-active-color: #000;--bs-btn-active-bg: rgb(100%, 80.5490196078%, 22.1960784314%);--bs-btn-active-border-color: rgb(100%, 78.1176470588%, 12.4705882353%);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #ffc107;--bs-btn-disabled-border-color: #ffc107}.btn-danger{--bs-btn-color: #fff;--bs-btn-bg: #b42332;--bs-btn-border-color: #b42332;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: rgb(60%, 11.6666666667%, 16.6666666667%);--bs-btn-hover-border-color: #901c28;--bs-btn-focus-shadow-rgb: 191, 68, 81;--bs-btn-active-color: #fff;--bs-btn-active-bg: #901c28;--bs-btn-active-border-color: rgb(52.9411764706%, 10.2941176471%, 14.7058823529%);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #b42332;--bs-btn-disabled-border-color: #b42332}.btn-light{--bs-btn-color: #000;--bs-btn-bg: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: rgb(82.6666666667%, 83%, 83.3333333333%);--bs-btn-hover-border-color: rgb(77.8039215686%, 78.1176470588%, 78.431372549%);--bs-btn-focus-shadow-rgb: 211, 212, 213;--bs-btn-active-color: #000;--bs-btn-active-bg: rgb(77.8039215686%, 78.1176470588%, 78.431372549%);--bs-btn-active-border-color: rgb(72.9411764706%, 73.2352941176%, 73.5294117647%);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #000;--bs-btn-disabled-bg: #f8f9fa;--bs-btn-disabled-border-color: #f8f9fa}.btn-dark{--bs-btn-color: #fff;--bs-btn-bg: #212529;--bs-btn-border-color: #212529;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: rgb(26%, 27.3333333333%, 28.6666666667%);--bs-btn-hover-border-color: rgb(21.6470588235%, 23.0588235294%, 24.4705882353%);--bs-btn-focus-shadow-rgb: 66, 70, 73;--bs-btn-active-color: #fff;--bs-btn-active-bg: rgb(30.3529411765%, 31.6078431373%, 32.862745098%);--bs-btn-active-border-color: rgb(21.6470588235%, 23.0588235294%, 24.4705882353%);--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #fff;--bs-btn-disabled-bg: #212529;--bs-btn-disabled-border-color: #212529}.btn-outline-primary{--bs-btn-color: #315ca8;--bs-btn-border-color: #315ca8;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #315ca8;--bs-btn-hover-border-color: #315ca8;--bs-btn-focus-shadow-rgb: 49, 92, 168;--bs-btn-active-color: #fff;--bs-btn-active-bg: #315ca8;--bs-btn-active-border-color: #315ca8;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #315ca8;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #315ca8;--bs-gradient: none}.btn-outline-secondary{--bs-btn-color: #58616e;--bs-btn-border-color: #58616e;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #58616e;--bs-btn-hover-border-color: #58616e;--bs-btn-focus-shadow-rgb: 88, 97, 110;--bs-btn-active-color: #fff;--bs-btn-active-bg: #58616e;--bs-btn-active-border-color: #58616e;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #58616e;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #58616e;--bs-gradient: none}.btn-outline-success{--bs-btn-color: #198754;--bs-btn-border-color: #198754;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #198754;--bs-btn-hover-border-color: #198754;--bs-btn-focus-shadow-rgb: 25, 135, 84;--bs-btn-active-color: #fff;--bs-btn-active-bg: #198754;--bs-btn-active-border-color: #198754;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #198754;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #198754;--bs-gradient: none}.btn-outline-info{--bs-btn-color: #0dcaf0;--bs-btn-border-color: #0dcaf0;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #0dcaf0;--bs-btn-hover-border-color: #0dcaf0;--bs-btn-focus-shadow-rgb: 13, 202, 240;--bs-btn-active-color: #000;--bs-btn-active-bg: #0dcaf0;--bs-btn-active-border-color: #0dcaf0;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #0dcaf0;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #0dcaf0;--bs-gradient: none}.btn-outline-warning{--bs-btn-color: #ffc107;--bs-btn-border-color: #ffc107;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #ffc107;--bs-btn-hover-border-color: #ffc107;--bs-btn-focus-shadow-rgb: 255, 193, 7;--bs-btn-active-color: #000;--bs-btn-active-bg: #ffc107;--bs-btn-active-border-color: #ffc107;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #ffc107;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #ffc107;--bs-gradient: none}.btn-outline-danger{--bs-btn-color: #b42332;--bs-btn-border-color: #b42332;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #b42332;--bs-btn-hover-border-color: #b42332;--bs-btn-focus-shadow-rgb: 180, 35, 50;--bs-btn-active-color: #fff;--bs-btn-active-bg: #b42332;--bs-btn-active-border-color: #b42332;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #b42332;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #b42332;--bs-gradient: none}.btn-outline-light{--bs-btn-color: #f8f9fa;--bs-btn-border-color: #f8f9fa;--bs-btn-hover-color: #000;--bs-btn-hover-bg: #f8f9fa;--bs-btn-hover-border-color: #f8f9fa;--bs-btn-focus-shadow-rgb: 248, 249, 250;--bs-btn-active-color: #000;--bs-btn-active-bg: #f8f9fa;--bs-btn-active-border-color: #f8f9fa;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #f8f9fa;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #f8f9fa;--bs-gradient: none}.btn-outline-dark{--bs-btn-color: #212529;--bs-btn-border-color: #212529;--bs-btn-hover-color: #fff;--bs-btn-hover-bg: #212529;--bs-btn-hover-border-color: #212529;--bs-btn-focus-shadow-rgb: 33, 37, 41;--bs-btn-active-color: #fff;--bs-btn-active-bg: #212529;--bs-btn-active-border-color: #212529;--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color: #212529;--bs-btn-disabled-bg: transparent;--bs-btn-disabled-border-color: #212529;--bs-gradient: none}.btn-link{--bs-btn-font-weight: 400;--bs-btn-color: var(--bs-link-color);--bs-btn-bg: transparent;--bs-btn-border-color: transparent;--bs-btn-hover-color: var(--bs-link-hover-color);--bs-btn-hover-border-color: transparent;--bs-btn-active-color: var(--bs-link-hover-color);--bs-btn-active-border-color: transparent;--bs-btn-disabled-color: #6c757d;--bs-btn-disabled-border-color: transparent;--bs-btn-box-shadow: 0 0 0 #000;--bs-btn-focus-shadow-rgb: 80, 116, 181;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-lg,.btn-group-lg>.btn{--bs-btn-padding-y: 0.5rem;--bs-btn-padding-x: 1rem;--bs-btn-font-size:1.09375rem;--bs-btn-border-radius: var(--bs-border-radius-lg)}.btn-sm,.btn-group-sm>.btn{--bs-btn-padding-y: 0.25rem;--bs-btn-padding-x: 0.5rem;--bs-btn-font-size:0.765625rem;--bs-btn-border-radius: var(--bs-border-radius-sm)}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>:not(.btn-check:first-child)+.btn,.btn-group>.btn-group:not(:first-child){margin-left:calc(-1*var(--bs-border-width))}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn,.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:calc(-1*var(--bs-border-width))}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn,.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}:root,[data-bs-theme=dark]{--bs-body-font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Apple SD Gothic Neo, Malgun Gothic, sans-serif;--bs-body-color: var(--text, #20242b);--bs-body-bg: var(--panel, #fff);--bs-secondary-color: var(--muted, #58616e);--bs-border-color: var(--line-2, #b9c1cc);--bs-secondary-bg: var(--panel-2, #eef0f3)}.btn{min-height:36px;display:inline-flex;align-items:center;justify-content:center;gap:.375rem}.btn[hidden]{display:none !important}.form-control,.form-select{min-height:38px;font-family:var(--bs-body-font-family)}.form-control::placeholder{color:var(--muted, #58616e);opacity:.85}.form-control:focus,.form-select:focus{border-color:var(--accent, #315ca8)}.btn-outline-secondary,.btn-ghost{--bs-btn-color: var(--text, #20242b);--bs-btn-border-color: var(--line, #dfe3e8);--bs-btn-hover-color: var(--text, #20242b);--bs-btn-hover-bg: var(--panel-2, #eef0f3);--bs-btn-hover-border-color: var(--line-2, #b9c1cc);--bs-btn-active-color: var(--text, #20242b);--bs-btn-active-bg: var(--panel-2, #eef0f3);--bs-btn-active-border-color: var(--line-2, #b9c1cc);--bs-btn-disabled-color: var(--muted, #58616e);--bs-btn-disabled-border-color: var(--line, #dfe3e8);--bs-btn-focus-shadow-rgb: 49, 92, 168}.btn-ghost{--bs-btn-border-color: transparent}[data-bs-theme=dark] .btn-outline-danger{--bs-btn-color: #ff9ba5;--bs-btn-border-color: #73404a;--bs-btn-hover-bg: #93313e;--bs-btn-hover-border-color: #93313e}.btn:focus-visible{outline:2px solid var(--ring, #315ca8);outline-offset:2px}@media(pointer: coarse){.btn,.form-control,.form-select{min-height:44px}}form[data-auto-apply] [hidden]{display:none !important}
@@ -0,0 +1,33 @@
1
+ const panel = document.querySelector("[data-update-status]");
2
+
3
+ if (panel?.dataset.active === "true") {
4
+ const messages = JSON.parse(panel.dataset.messages);
5
+ const stateLabel = panel.querySelector("[data-update-state]");
6
+ const errorLabel = panel.querySelector("[data-update-error]");
7
+ const versionLabel = document.querySelector("[data-update-version]");
8
+
9
+ async function refreshStatus() {
10
+ let retry = true;
11
+ try {
12
+ const response = await fetch("/admin/update/status", { credentials: "same-origin", cache: "no-store", headers: { Accept: "application/json" } });
13
+ if ([401, 403].includes(response.status)) {
14
+ stateLabel.textContent = messages.signIn;
15
+ panel.querySelector("[data-update-login]").hidden = false;
16
+ return;
17
+ }
18
+ if (!response.ok) throw new Error("Status unavailable");
19
+ const status = await response.json();
20
+ if (status.currentVersion) versionLabel.textContent = status.currentVersion;
21
+ if (status.run) {
22
+ stateLabel.textContent = messages[status.run.state] || messages.running;
23
+ errorLabel.textContent = status.run.error || "";
24
+ retry = ["queued", "running"].includes(status.run.state);
25
+ }
26
+ } catch {
27
+ stateLabel.textContent = messages.reconnect;
28
+ }
29
+ if (retry) window.setTimeout(refreshStatus, 3000);
30
+ }
31
+
32
+ window.setTimeout(refreshStatus, 1500);
33
+ }