diff --git a/docs/api/admin.md b/docs/api/admin.md
index 051c655..b534c0c 100644
--- a/docs/api/admin.md
+++ b/docs/api/admin.md
@@ -1,19 +1,120 @@
---
type: api
title: Admin Endpoints
-description: User management endpoints mounted under /api/v1/admin (admin role required).
-tags: [api, admin, users]
-timestamp: 2026-07-21T18:28:00Z
+description: Admin-only endpoints for user management, general settings, categories, and supporting uploads.
+tags: [api, admin, users, general, categories]
+timestamp: 2026-07-22T12:00:00Z
---
# Endpoints
-| Method | Path | Auth | Source |
-|---------|-----------------------|---------|-----------------------------------------------------|
-| `GET` | `/api/v1/admin/users` | Admin | `backend/src/modules/admin/admin.controller.ts` |
-| `POST` | `/api/v1/admin/users` | Admin | Same. |
-| `PATCH` | `/api/v1/admin/users/:id` | Admin | Same. |
-| `DELETE`| `/api/v1/admin/users/:id` | Admin | Same. |
+All handlers below are mounted behind `JwtAuthGuard` (global) +
+`AdminGuard` + `@Roles('admin')`, so they require a valid admin JWT.
+CSRF is enforced on every mutating call (standard cookie + header
+pattern; nothing is added to the skip list).
+
+## User management — `/api/v1/admin`
+
+| Method | Path | Source |
+|----------|-------------------------------|---------------------------------------------------|
+| `GET` | `/api/v1/admin/users` | `backend/src/modules/admin/admin.controller.ts` |
+| `POST` | `/api/v1/admin/users` | Same. |
+| `PATCH` | `/api/v1/admin/users/:id` | Same. |
+| `DELETE` | `/api/v1/admin/users/:id` | Same. |
+
+`AdminService` enforces the **last-admin invariant** (`LAST_ADMIN` /
+`409`) — the final admin row cannot be demoted or deleted. `POST`
+creates a new user; `PATCH .../:id` changes role / status; `DELETE .../:id`
+removes the row.
+
+## General settings — `/api/v1/admin/general`
+
+| Method | Path | Source |
+|--------|---------------------------------------|-------------------------------------------------------------|
+| `GET` | `/api/v1/admin/general/settings` | `backend/src/modules/admin/admin-general.controller.ts` |
+| `PUT` | `/api/v1/admin/general/settings` | Same. |
+| `GET` | `/api/v1/admin/general/themes` | Same. |
+
+`GET /settings` returns the full `GeneralSettingsView`
+(`pageTitle`, `logo`, `welcomeMarkdown`, `themeKey`, `eventStartUtc`,
+`eventEndUtc`, `defaultChallengeIp`, `registrationsEnabled`).
+
+`PUT /settings` is validated by `GeneralSettingsSchema` (zod):
+
+* `pageTitle` 1–120 chars
+* `logo` up to 2048 chars (a public URL — uploaded separately)
+* `welcomeMarkdown` up to 64 000 chars
+* `themeKey` is one of `THEME_IDS`
+* `eventStartUtc` / `eventEndUtc` parseable dates; `superRefine`
+ enforces `eventEndUtc > eventStartUtc` and reports the issue on
+ `eventEndUtc`
+* `defaultChallengeIp` 1–255 chars
+* `registrationsEnabled` boolean
+
+On success the handler persists every field via `SettingsService`,
+emits an SSE `general` event (`{ topic: 'general', themeKey }`) via
+`SseHubService`, and returns the updated view.
+
+`GET /themes` returns the `ThemeView[]` for which a corresponding
+JSON file exists under `THEMES_DIR`. Each item is `{ id, key, name }`.
+
+See [Admin — General Settings](/guides/admin-general-settings.md).
+
+## Categories — `/api/v1/admin/categories`
+
+| Method | Path | Source |
+|----------|-------------------------------------|-----------------------------------------------------------------|
+| `GET` | `/api/v1/admin/categories` | `backend/src/modules/admin/admin-categories.controller.ts` |
+| `POST` | `/api/v1/admin/categories` | Same. |
+| `PUT` | `/api/v1/admin/categories/:id` | Same. |
+| `DELETE` | `/api/v1/admin/categories/:id` | Same. |
+
+Behavior:
+
+* `GET` returns all categories sorted by `LOWER(abbreviation)` ascending.
+* `POST` uppercases the abbreviation, rejects duplicates with `409 CONFLICT`,
+ and persists the row. Body validated by `CreateCategorySchema`
+ (`name` 1–120 chars; `abbreviation` 2–6 chars; `description` up to
+ 2000 chars; `iconPath` optional, up to 2048 chars).
+* `PUT` validates the body with `UpdateCategorySchema` (every field
+ optional). System rows (`system_key IS NOT NULL`) cannot change their
+ abbreviation — server returns `409 SYSTEM_PROTECTED`. Duplicate
+ abbreviations on user rows return `409 CONFLICT`. Updates the
+ `updated_at` timestamp on success.
+* `DELETE` is blocked for system rows (`403 SYSTEM_PROTECTED`) and for
+ rows with at least one attached challenge (`409 CATEGORY_HAS_CHALLENGES`
+ with `{ count }` in `details`). Returns `404 NOT_FOUND` for unknown ids.
+
+Response shape (`CategoryView`):
+
+```json
+{
+ "id": "uuid",
+ "name": "Cryptography",
+ "abbreviation": "CRY",
+ "description": "Cryptographic challenges",
+ "iconPath": "/uploads/icons/CRY.png",
+ "isSystem": true,
+ "systemKey": "CRY",
+ "createdAt": "2026-07-21T18:00:00.000Z",
+ "updatedAt": "2026-07-21T18:00:00.000Z"
+}
+```
+
+See [Admin — Categories](/guides/admin-categories.md).
+
+## Supporting uploads
+
+Logo and category-icon uploads live on the uploads module and are
+documented in [Uploads Endpoints](/api/uploads.md):
+
+| Method | Path | Purpose |
+|--------|-----------------------------------|----------------------------------|
+| `POST` | `/api/v1/uploads/logo` | Logo used by General settings. |
+| `POST` | `/api/v1/uploads/category-icon` | Icon used by Categories. |
+
+Both endpoints are admin-only and use `FileInterceptor('file')` for a
+single multipart part named `file`.
# Guard chain
@@ -21,16 +122,13 @@ timestamp: 2026-07-21T18:28:00Z
is `@Public()`. Admin handlers are not.
2. `AdminGuard` (`backend/src/common/guards/admin.guard.ts`) requires
`req.user.role === 'admin'`.
-
-# Behavior
-
-* `AdminService` enforces the **last-admin invariant** (`LAST_ADMIN` /
- `409`) — the final admin row cannot be demoted or deleted.
-* `POST /api/v1/admin/users` creates a new user; `PATCH /api/v1/admin/users/:id`
- changes role / status; `DELETE /api/v1/admin/users/:id` removes the row.
+3. `RolesGuard` enforces `@Roles('admin')` metadata on the controller.
# See also
- [Backend Module Map](/architecture/backend-modules.md)
-- [Admin Shell & User Management](/guides/admin-shell.md)
+- [Admin Shell](/guides/admin-shell.md)
+- [Admin — General Settings](/guides/admin-general-settings.md)
+- [Admin — Categories](/guides/admin-categories.md)
+- [Uploads Endpoints](/api/uploads.md)
- [REST API Overview](/api/rest-overview.md)
diff --git a/docs/architecture/backend-modules.md b/docs/architecture/backend-modules.md
index 740383e..a09ca23 100644
--- a/docs/architecture/backend-modules.md
+++ b/docs/architecture/backend-modules.md
@@ -27,7 +27,7 @@ also registers two global providers:
| `UsersModule` | `backend/src/modules/users/users.module.ts` | `UsersController` (`/api/v1/auth/register-first-admin`) + `UsersService` (last-admin invariant) + `UsersRankService` (`rankOfUser(manager, userId) -> {rank, points}` over the `solve` table). |
| `SetupModule` | `backend/src/modules/setup/setup.module.ts` | `SetupController` (`POST /api/v1/setup/create-admin`) + `SetupService`. Returns 409 `SYSTEM_INITIALIZED` once any admin exists; serializes concurrent first-admin attempts via an in-process promise chain. |
| `SystemModule` | `backend/src/modules/system/system.module.ts` | `SystemController` (`/api/v1/bootstrap`, `/event/status`, SSE streams) + `SystemService`. |
-| `AdminModule` | `backend/src/modules/admin/admin.module.ts` | `AdminController` (`/api/v1/admin/users*`) + `AdminService`. Gated by `AdminGuard` + `@Roles('admin')`. |
+| `AdminModule` | `backend/src/modules/admin/admin.module.ts` | Three controllers (`AdminController` for `/api/v1/admin/users*`, `AdminGeneralController` for `/api/v1/admin/general/*`, `AdminCategoriesController` for `/api/v1/admin/categories*`) plus their services (`AdminService`, `AdminGeneralService`, `AdminCategoriesService`). Gated by `AdminGuard` + `@Roles('admin')`. Imports `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity])`, `AuthModule`, `UsersModule`, `SettingsModule`, and `CommonModule` (for `SseHubService`, `ThemeLoaderService`). |
| `UploadsModule` | `backend/src/modules/uploads/uploads.module.ts` | `UploadsController` (`/api/v1/uploads/*`). Admin-only multipart. |
| `BlogModule` | `backend/src/modules/blog/blog.module.ts` | `BlogController` (`GET /api/v1/blog/posts`) + `BlogService` (lists `status='published'` rows). |
| `FrontendModule` | `backend/src/frontend/frontend.module.ts` | Registers `SpaFallbackMiddleware` and the uploads static helper. |
@@ -40,6 +40,8 @@ also registers two global providers:
| `UsersController` | `/api/v1/auth/register-first-admin` | Public (bootstrap-only) | `backend/src/modules/users/users.controller.ts` |
| `SetupController` | `/api/v1/setup/create-admin` | Public (bootstrap-only; 409 `SYSTEM_INITIALIZED` once an admin exists) | `backend/src/modules/setup/setup.controller.ts` |
| `AdminController` | `/api/v1/admin` | Admin only | `backend/src/modules/admin/admin.controller.ts` |
+| `AdminGeneralController` | `/api/v1/admin/general` | Admin only | `backend/src/modules/admin/admin-general.controller.ts` |
+| `AdminCategoriesController` | `/api/v1/admin/categories` | Admin only | `backend/src/modules/admin/admin-categories.controller.ts` |
| `SystemController` | `/api/v1` | Mixed (bootstrap/event/scoreboard/settings are public; `events/status` SSE is JWT-protected) | `backend/src/modules/system/system.controller.ts` |
| `UploadsController` | `/api/v1/uploads` | Admin only | `backend/src/modules/uploads/uploads.controller.ts` |
| `BlogController` | `/api/v1/blog` | Public | `backend/src/modules/blog/blog.controller.ts` |
diff --git a/docs/database/challenges.md b/docs/database/challenges.md
index aef6b86..9b4b90b 100644
--- a/docs/database/challenges.md
+++ b/docs/database/challenges.md
@@ -13,14 +13,26 @@ timestamp: 2026-07-21T14:18:00Z
| Column | Type | Description |
|----------------|---------|------------------------------------------------------------------------------|
| `id` | TEXT PK | UUID v4. |
-| `system_key` | TEXT | One of `crypto`, `forensics`, `pwn`, `web`, `misc`, `osint` (unique where NOT NULL) or `NULL` for user-created categories. |
+| `system_key` | TEXT | Non-null for system categories (e.g. `CRY`, `MSC`, `PWN`, `REV`, `WEB`, `HW`); unique where NOT NULL; `NULL` for user-created categories. |
| `name` | TEXT | Display name (e.g. `Cryptography`). |
-| `abbreviation` | TEXT | Short label (e.g. `CRY`). |
+| `abbreviation` | TEXT | Short label (e.g. `CRY`), 2–6 chars, server-uppercased. Globally unique — enforced by `uq_category_abbreviation`. |
| `description` | TEXT | Optional description. |
-| `icon_path` | TEXT | Default icon URL (e.g. `/uploads/icons/crypto.svg`). |
+| `icon_path` | TEXT | Default icon URL (e.g. `/uploads/icons/CRY.png`). |
+| `created_at` | TEXT | ISO 8601 timestamp the row was created (added by `AddCategoryTimestampsAndUniqueAbbrev1700000000200`). |
+| `updated_at` | TEXT | ISO 8601 timestamp the row was last updated (added by the same migration; bumped on `PUT /api/v1/admin/categories/:id`). |
-The seed migration inserts one row per `SYSTEM_CATEGORY_KEYS` value from
-`backend/src/config/env.schema.ts`.
+System rows are seeded by the
+`UpdateSystemCategoryKeys1700000000300` migration so the canonical
+keys are `CRY` (Cryptography), `MSC` (Misc), `PWN` (Binary
+exploitation), `REV` (Reverse Engineering), `WEB` (Web), and `HW`
+(Hardware). The migration drops any legacy seeded rows that no
+longer match the canonical set before inserting the missing entries.
+User-created categories leave `system_key` as `NULL`.
+
+The uniqueness on `abbreviation` is enforced both by application
+validation (uppercased on save, duplicates rejected) and by the
+`uq_category_abbreviation` index created in migration
+`1700000000200`.
## `challenge`
diff --git a/docs/guides/admin-categories.md b/docs/guides/admin-categories.md
new file mode 100644
index 0000000..4f0bc45
--- /dev/null
+++ b/docs/guides/admin-categories.md
@@ -0,0 +1,153 @@
+---
+type: guide
+title: Admin — Categories
+description: How an admin lists, creates, edits, and deletes challenge categories from the /admin/categories page, including system-row protection and challenge-attached protection.
+tags: [guide, admin, categories, tester]
+timestamp: 2026-07-22T12:00:00Z
+---
+
+# When this view is available
+
+The Categories admin page renders at `/admin/categories` for users with
+`role === 'admin'`. It is reached from the [Admin Shell](/guides/admin-shell.md)
+side-nav (categories are also embedded inside the General settings page).
+
+| Layer | File | Check |
+|------------------|----------------------------------------------------------------------------|------------------------------------------------------|
+| Client route | `frontend/src/app/app.routes.ts` | `/admin/categories` child of `adminGuard`. |
+| Client component | `frontend/src/app/features/admin/categories/categories.component.ts` | `AdminCategoriesComponent.ngOnInit` fetches the list. |
+| Client modals | `frontend/src/app/features/admin/categories/category-form-modal.component.ts` | Create / edit modal. |
+| | `frontend/src/app/features/admin/categories/category-delete-modal.component.ts` | Delete confirmation modal. |
+| Server route | `backend/src/modules/admin/admin-categories.controller.ts` | `GET/POST /api/v1/admin/categories`, `PUT/DELETE /:id`. |
+
+# How to access (tester steps)
+
+1. Sign in as an admin user.
+2. Open **Admin area** → **Categories** in the side-nav, or visit
+ `/admin/categories` directly. (The page also renders below the
+ General settings form at `/admin/general`.)
+3. The page shows `Loading categories...` (`data-testid="cat-loading"`)
+ while the list request is in flight, then renders one row per
+ category sorted alphabetically by (lowercased) abbreviation.
+
+# Visual elements
+
+| Element | Selector | Purpose |
+|------------------------|--------------------------------------------|---------|
+| Page section | `[data-testid="admin-categories"]` | Root container. |
+| Add button (`+`) | `[data-testid="cat-add"]` | Opens the create modal. |
+| Loading | `[data-testid="cat-loading"]` | "Loading categories..." placeholder. |
+| Load error | `[data-testid="cat-error"]` | Red error text. |
+| List | `[data-testid="cat-list"]` | `
` of category rows. |
+| Row | `[data-testid="cat-row-{ABBR}"]` | One `
` per category. |
+| Edit button | `[data-testid="cat-edit-{ABBR}"]` | Opens the edit modal. Disabled for system rows (the abbreviation input becomes `readonly`). |
+| Delete button | `[data-testid="cat-delete-{ABBR}"]` | Opens the delete confirmation modal. |
+| Form modal | `[data-testid="cat-form-modal"]` | Create/edit dialog (`cat-form-backdrop` is the click-outside dismiss layer). |
+| Delete modal | `[data-testid="cat-delete-modal"]` | Confirmation dialog (`cat-delete-backdrop` is the dismiss layer). |
+| Delete error | `[data-testid="cat-delete-error"]` | Inline error message after a failed delete. |
+
+# Form modal fields
+
+| Label | `data-testid` | Notes |
+|-------------------------------|----------------|-------|
+| Name | `cf-name` | Required, max 120 chars. |
+| Abbreviation (uppercase) | `cf-abbr` | Required, 2–6 chars; server upper-cases on save. `readonly` when editing a system row. |
+| Description | `cf-desc` | Optional, max 2000 chars. |
+| Icon (file picker) | `cf-icon` | Optional image; uploaded to `POST /api/v1/uploads/category-icon` and resized/normalized server-side. |
+| Cancel / OK | `cf-cancel`, `cf-ok` | OK disabled while form invalid or already submitting. |
+
+# Expected behavior
+
+## List
+
+* The list is sorted by `LOWER(abbreviation)` ascending. Server returns
+ rows already sorted; the client re-sorts defensively in
+ `AdminCategoriesComponent.load()`.
+* System rows (`isSystem === true`) render with the edit/delete actions
+ still visible, but the abbreviation field is locked when editing, and
+ the delete confirmation modal hides the "OK" button (see "Delete
+ behavior" below).
+
+## Create
+
+1. Click `cat-add` → `cat-form-modal` opens in create mode.
+2. Fill name, abbreviation, description. Optionally pick an icon file
+ (preview appears immediately).
+3. Click `cf-ok`. The component calls
+ `AdminService.createCategory({...})`, then — if an icon file was
+ selected — `uploadCategoryIcon(id, file)` and finally
+ `updateCategory(id, { iconPath: publicUrl })` to persist the icon
+ URL.
+4. On success the modal closes and the list refreshes.
+
+## Edit
+
+1. Click `cat-edit-{ABBR}` → modal opens in edit mode, pre-filled with
+ the row's values.
+2. For system rows, the abbreviation field is `readonly`.
+3. On save the component issues `updateCategory(id, {...})`. If an icon
+ file is selected, the new icon is uploaded first and the returned
+ `publicUrl` replaces `iconPath` in the same update.
+
+## Delete behavior
+
+* **System rows:** the delete modal renders
+ *"This is a system category and cannot be deleted."* and the OK
+ button is hidden. `canConfirm()` returns `false`. If somehow the
+ request is dispatched, the backend returns `403 SYSTEM_PROTECTED`
+ and the UI shows `System categories cannot be deleted.`.
+* **Rows with attached challenges:** the backend returns
+ `409 CATEGORY_HAS_CHALLENGES` with `{ count }` in `details`. The UI
+ maps this to `Cannot delete: category has N challenge(s) attached.`
+ and renders it in `cat-delete-error`.
+* **Normal row:** the modal confirms the name + abbreviation, OK
+ dispatches `DELETE /api/v1/admin/categories/:id`, and the list
+ refreshes.
+
+## Validation and error codes (server)
+
+| Code | HTTP | Triggered by |
+|----------------------------|------|---------------------------------------------------------------|
+| `VALIDATION_FAILED` | 400 | `CreateCategorySchema` / `UpdateCategorySchema` fails (length, required). |
+| `NOT_FOUND` | 404 | `PUT`/`DELETE` on a non-existent id. |
+| `CONFLICT` | 409 | Duplicate abbreviation on create or on update (user row). |
+| `SYSTEM_PROTECTED` | 409 | Update attempts to change a system row's abbreviation. |
+| `SYSTEM_PROTECTED` | 403 | Delete on a system row. |
+| `CATEGORY_HAS_CHALLENGES` | 409 | Delete on a category with at least one attached challenge (carries `{ count }` in `details`). |
+
+# Architecture map
+
+| Step | Where | What happens |
+|------|-------------------------------------------------------------|-----------------------------------------------------------------------------|
+| 1 | `frontend/src/app/app.routes.ts` | `/admin/categories` lazy-loads `AdminCategoriesComponent`. |
+| 2 | `frontend/src/app/features/admin/categories/categories.component.ts` | `ngOnInit` calls `AdminService.listCategories()`. |
+| 3 | `frontend/src/app/core/services/admin.service.ts` | `listCategories`, `createCategory`, `updateCategory`, `deleteCategory`, `uploadCategoryIcon`. |
+| 4 | `frontend/src/app/features/admin/categories/category-form-modal.component.ts` | Holds the form state, emits `CategoryFormSubmit`. |
+| 5 | `frontend/src/app/features/admin/categories/category-delete-modal.component.ts` | Maps error codes to user-friendly messages. |
+| 6 | `backend/src/modules/admin/admin-categories.controller.ts` | `AdminGuard` + `@Roles('admin')` on every handler. |
+| 7 | `backend/src/modules/admin/categories.service.ts` | `list` (sorted by `LOWER(abbreviation)`), `create` (uppercase + dup-check), `update` (system-abbr-immutable), `remove` (system-protected, challenge-count check). |
+| 8 | `backend/src/modules/admin/dto/categories.dto.ts` | zod schemas with length constraints; param validator for `:id`. |
+| 9 | `backend/src/modules/uploads/uploads.controller.ts` | `POST /api/v1/uploads/category-icon` (multipart) — also admin-only. |
+
+# Notes
+
+* Abbreviations are uppercased server-side before persistence and
+ uniqueness check (DB enforces uniqueness via the
+ `uq_category_abbreviation` index — see
+ [Challenge Tables](/database/challenges.md)).
+* System rows are seeded by the
+ `UpdateSystemCategoryKeys1700000000300` migration and identified by
+ a non-null `system_key` column.
+* The icon upload pipeline normalizes the image to a fixed size; the
+ returned `publicUrl` is stored in `category.icon_path`.
+* The Categories component is embedded inside the General settings
+ page, so changes to a category are visible on either route without
+ an extra refresh.
+
+# See also
+
+- [Admin Shell](/guides/admin-shell.md) — side-nav layout and the General settings page that embeds categories.
+- [Admin — General Settings](/guides/admin-general-settings.md)
+- [Admin Endpoints](/api/admin.md) — `GET/POST/PUT/DELETE /api/v1/admin/categories`.
+- [Challenge Tables](/database/challenges.md) — `category` schema and migrations.
+- [Uploads Endpoints](/api/uploads.md) — `POST /api/v1/uploads/category-icon`.
diff --git a/docs/guides/admin-general-settings.md b/docs/guides/admin-general-settings.md
new file mode 100644
index 0000000..b5442dc
--- /dev/null
+++ b/docs/guides/admin-general-settings.md
@@ -0,0 +1,126 @@
+---
+type: guide
+title: Admin — General Settings
+description: How an admin edits global platform settings (page title, logo, theme, event window, default challenge IP, registrations, welcome Markdown) from the /admin/general page.
+tags: [guide, admin, settings, general, tester]
+timestamp: 2026-07-22T12:00:00Z
+---
+
+# When this view is available
+
+The General Settings page is rendered at `/admin/general` for users with
+`role === 'admin'` once the instance is initialized. It is reached from
+the [Admin Shell](/guides/admin-shell.md) side-nav (`General` entry) or
+by navigating directly to the URL.
+
+| Layer | File | Check |
+|------------------|----------------------------------------------------------------------------|------------------------------------------------|
+| Client route | `frontend/src/app/app.routes.ts` | `/admin/general` child of `adminGuard`. |
+| Client component | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent.ngOnInit` fetches settings + themes. |
+| Client predicate | `frontend/src/app/features/admin/general.pure.ts` (`deriveEventState`) | Computes the read-only event-state label. |
+| Server route | `backend/src/modules/admin/admin-general.controller.ts` | `GET/PUT /api/v1/admin/general/settings` + `GET /api/v1/admin/general/themes`. |
+
+# How to access (tester steps)
+
+1. Sign in as an admin user (see [First-Run Bootstrap](/guides/bootstrap.md)
+ if no admin exists).
+2. Open the username menu in the shell header and click **Admin area**,
+ or use the side-nav's **General** entry, or visit `/admin/general`
+ directly.
+3. The page renders a `Loading settings...` placeholder while the
+ initial `GET /api/v1/admin/general/settings` + `GET .../themes`
+ requests are in flight.
+
+# Fields
+
+The page is a single reactive form with these controls (every
+`data-testid` listed is asserted in the existing test suite):
+
+| Label | `data-testid` | Backend field | Notes |
+|------------------------|---------------------------|-------------------------|-------|
+| Page title | `general-pageTitle` | `pageTitle` | Required, max 120 chars. |
+| Logo (file picker) | `general-logo-file` | `logo` (public URL) | Uploads via `POST /api/v1/uploads/logo`; the returned `publicUrl` is bound to a hidden input `general-logo`. |
+| Logo upload status | `general-logo-uploading` / `general-logo-error` / `general-logo-current` | — | Inline status text under the file picker. |
+| Global theme | `general-themeKey` | `themeKey` | `