Commit Graph
100 Commits
Author SHA1 Message Date
SimoandCursor 8cce8837bc Serve avatars from Habbo imager instead of broken self-hosted proxy.
Local Build and Deploy / deploy (push) Successful in 56s
Co-authored-by: Cursor <[email protected]>
2026-07-15 21:51:02 +02:00
SimoandCursor 2c68fb7e5a Polish admin language dropdown layout and selection styles.
Local Build and Deploy / deploy (push) Successful in 54s
Co-authored-by: Cursor <[email protected]>
2026-07-15 21:46:54 +02:00
SimoandCursor ea07950374 Fix radio admin StatusCard icons showing as Unicode escapes.
Local Build and Deploy / deploy (push) Successful in 56s
Co-authored-by: Cursor <[email protected]>
2026-07-15 21:45:57 +02:00
SimoandCursor 7b6c02c07d Make admin light/dark mode follow real palettes end-to-end.
Local Build and Deploy / deploy (push) Successful in 55s
Light admin derives from the public light theme; dark keeps HK overrides. Remap inputs, cards, muted text, and kill the public wallpaper on admin so fonts and boxes stay readable in both modes.

Co-authored-by: Cursor <[email protected]>
2026-07-15 21:37:25 +02:00
SimoandCursor 94904f2ddb Add language picker to admin sidebar and topbar.
Local Build and Deploy / deploy (push) Successful in 55s
Reuse LanguageSwitcher with an admin-styled dropdown next to the theme toggle.

Co-authored-by: Cursor <[email protected]>
2026-07-15 21:33:11 +02:00
SimoandCursor 1e3eb86495 Add dark/light toggle to admin sidebar and topbar.
Local Build and Deploy / deploy (push) Successful in 55s
Reuse ThemeSwitcher with an admin variant (Sun/Moon) next to the staff profile and in the housekeeping header.

Co-authored-by: Cursor <[email protected]>
2026-07-15 21:32:05 +02:00
SimoandCursor 363c9b3d56 Reorganize admin into tabbed hubs with a condensed sidebar.
Local Build and Deploy / deploy (push) Successful in 57s
Replace the long flat nav with hub entries, shared AdminHubChrome tabs, and AdminPageShell. Existing URLs stay stable; Settings/Radio and other sections now share one chrome.

Co-authored-by: Cursor <[email protected]>
2026-07-15 21:29:36 +02:00
SimoandCursor 367f69a439 Decouple admin from public max-w-7xl via (site) route group.
Local Build and Deploy / deploy (push) Successful in 54s
Public chrome and width live only under (site). /admin and /client inherit the root layout alone, so housekeeping width is no longer tied to the public template.

Co-authored-by: Cursor <[email protected]>
2026-07-15 21:12:52 +02:00
SimoandCursor a1dc7ff0a7 Set admin max width to 96rem, wider than public 80rem.
Local Build and Deploy / deploy (push) Successful in 55s
Public site stays max-w-7xl; admin is ~20% wider and still centered, never full viewport.

Co-authored-by: Cursor <[email protected]>
2026-07-15 21:09:20 +02:00
SimoandCursor e892b0426a Cap admin shell at 75vw instead of full bleed.
Local Build and Deploy / deploy (push) Successful in 54s
Keeps housekeeping wider than the public max-w-7xl grid without stretching edge-to-edge on large screens.

Co-authored-by: Cursor <[email protected]>
2026-07-15 21:07:01 +02:00
SimoandCursor e101ea474e Fix rooms list crash and rebuild CMS settings UI.
Rooms queried the wrong Prisma model and a non-existent owner relation. Settings now use grouped managed fields plus a searchable advanced key/value panel.

Co-authored-by: Cursor <[email protected]>
2026-07-15 21:02:57 +02:00
SimoandCursor c186f51ae0 Widen admin by escaping the public max-w-7xl chrome.
Local Build and Deploy / deploy (push) Successful in 54s
Render /admin outside the site header/nav/footer grid so housekeeping uses the full viewport width.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:59:53 +02:00
SimoandCursor 0096c55f96 Fix theme switcher desync and auto-correct unreadable saved colors.
Local Build and Deploy / deploy (push) Successful in 53s
Sync dark-mode state from the DOM after mount so admin text no longer needs a double toggle, and normalize text/button colors against their surfaces on theme save.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:54:46 +02:00
SimoandCursor ed56bf0917 Fix multi-theme contrast: separate public tokens from admin remap.
Local Build and Deploy / deploy (push) Successful in 54s
Public cards no longer paint with always-dark admin surfaces, and admin pages fully remap background/text/surface so shadcn UI stays readable on the admin canvas.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:51:20 +02:00
SimoandCursor d7278c77f9 Fix automatic readable text contrast for public and admin themes.
Local Build and Deploy / deploy (push) Successful in 55s
Derive admin/public text colors from WCAG contrast, unify ThemeVars CSS emission, and keep navbar overrides in sync with readable vars.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:41:34 +02:00
SimoandCursor 9d4d409b77 Restore self-hosted /api/imaging/avatar (and badge) endpoints.
Local Build and Deploy / deploy (push) Successful in 54s
The clothing importer and imager helpers pointed at a missing route; proxy Habbo with disk/memory cache so avatars work again.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:41:18 +02:00
SimoandCursor 3403d3b19f Fix admin permissions bounce, prefixes APIs, and Italian UI leftovers.
Local Build and Deploy / deploy (push) Successful in 56s
Gate permissions on ACL manage + resolve super-admin from live user ranks, restore prefixes API routes, and anglicize hardcoded admin copy with nav i18n.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:33:00 +02:00
SimoandCursor 0e89d03940 Finish fine-grained ACL across remaining admin pages and actions.
Local Build and Deploy / deploy (push) Successful in 56s
Replace leftover requireStaff gates with module PERMS, drop hardcoded room rank thresholds, and expand contract tests so admin mutations cannot regress to dashboard-only checks.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:15:22 +02:00
SimoandCursor 3c8a8ff888 Extend fine-grained ACL to settings, content, shop, and radio.
Local Build and Deploy / deploy (push) Successful in 54s
Gate pages and mutations on module PERMS instead of dashboard-only staff checks, add radio view/edit slugs with migration 0015, and expand the operations contract tests.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:11:55 +02:00
SimoandCursor f2427b3483 Harden admin ACL on critical write paths.
Local Build and Deploy / deploy (push) Successful in 54s
Gate translations, RCON, and user mutations on SETTINGS_EDIT, RCON_EXECUTE, and USERS_EDIT instead of dashboard/rank checks; redirect the legacy user-edit URL to the guarded canonical page.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:07:15 +02:00
SimoandCursor bae543baf6 Fix admin regressions from Biome refactor: theme init, mobile nav, i18n.
Local Build and Deploy / deploy (push) Successful in 54s
Restore valid browser JS in theme-init, close mobile sidebar on navigation, and split autoDjForm title/trackTitle across locales.

Co-authored-by: Cursor <[email protected]>
2026-07-15 20:02:25 +02:00
Simo 65e942b268 Update README.md
Local Build and Deploy / deploy (push) Successful in 1m16s
2026-07-12 20:54:51 +02:00
Simo 60278b9e71 feat: add admin operations suite
Local Build and Deploy / deploy (push) Successful in 50s
2026-07-12 20:11:28 +02:00
Simo 21a61068fb test: define admin operations contracts 2026-07-12 20:01:25 +02:00
Simo dbb9cad01e docs: plan admin operations port 2026-07-12 20:00:46 +02:00
Simo f5ba556e29 docs: define admin operations port 2026-07-12 19:59:45 +02:00
Simo c0ffc74f9b fix: externalize lzma for import build
Local Build and Deploy / deploy (push) Successful in 49s
2026-07-12 19:15:08 +02:00
Simo eb759f54bf feat: connect guarded asset import api 2026-07-12 19:12:25 +02:00
Simo 3497df9dfd feat: add asset import services 2026-07-12 19:12:25 +02:00
Simo 4b596226e0 fix: complete acl management 2026-07-12 19:12:24 +02:00
Simo 667ec9af4c test: define acl and import backend contracts 2026-07-12 19:01:28 +02:00
Simo 63ab21616c docs: define acl and import backend work 2026-07-12 19:00:47 +02:00
Simo 84e4123f09 fix: use semantic colors across admin pages
Local Build and Deploy / deploy (push) Successful in 49s
2026-07-12 18:50:45 +02:00
Simo 0fe482009c fix: isolate shared admin styling 2026-07-12 18:50:44 +02:00
Simo 4ce35e91fb feat: add semantic admin palette 2026-07-12 18:47:23 +02:00
Simo d4bcf89449 test: enforce admin theme isolation 2026-07-12 18:46:16 +02:00
Simo 1c581ca5df docs: plan admin theme isolation 2026-07-12 18:45:38 +02:00
Simo 907ac9bf5b docs: define admin theme isolation 2026-07-12 18:44:24 +02:00
Simo 2606092337 feat: add admin content module pages
Local Build and Deploy / deploy (push) Successful in 47s
2026-07-12 15:27:07 +02:00
Simo f422bb4a0b feat: add admin content module actions 2026-07-12 15:27:07 +02:00
Simo b9525c8e6b feat: add schema for admin content modules 2026-07-12 15:27:06 +02:00
Simo 9ead82bbca docs: plan first admin module port 2026-07-12 15:22:00 +02:00
Simo acc34ea8bf docs: define first admin module port 2026-07-12 15:19:48 +02:00
Simo 41002aa36d fix: preserve Next.js deploy cache
Local Build and Deploy / deploy (push) Successful in 45s
2026-07-12 15:17:16 +02:00
Simo f0b4bc1630 fix: enforce semantic contrast across admin
Local Build and Deploy / deploy (push) Successful in 44s
2026-07-12 15:14:15 +02:00
Simo 45c8178cfa docs: plan semantic theme contrast work 2026-07-12 15:08:13 +02:00
Simo 2227902c3e docs: define semantic theme contrast design 2026-07-12 15:07:30 +02:00
Simo b1a60adbc2 feat: rebrand CMS information as EpicNext
Local Build and Deploy / deploy (push) Successful in 45s
2026-07-12 14:49:37 +02:00
Simo 9cdd0b4000 feat: persist complete multitheme palettes 2026-07-12 14:49:36 +02:00
Simo 3fd2a27719 feat: generate preset-aware dark theme variables 2026-07-12 14:49:36 +02:00
Simo 48c596cf41 feat: add complete light and dark presets 2026-07-12 14:45:11 +02:00
Simo a550b06a58 docs: plan EpicNext multitheme implementation 2026-07-12 14:43:43 +02:00
Simo 7ec3095abd docs: define EpicNext multitheme design 2026-07-12 14:42:36 +02:00
Simo 5261cfaa1a feat: add CMS info footer popup
Local Build and Deploy / deploy (push) Successful in 43s
2026-07-12 14:37:00 +02:00
Simo 7d3430aeca fix: seed production ACL permissions
Local Build and Deploy / deploy (push) Successful in 56s
2026-07-12 14:29:01 +02:00
Simo 685033dcbd Update .gitea/workflows/deploy.yaml
Local Build and Deploy / deploy (push) Successful in 48s
2026-07-12 14:22:07 +02:00
Simo 48047f2782 Update .gitea/workflows/deploy.yaml
Local Build and Deploy / deploy (push) Failing after 17s
2026-07-12 14:11:06 +02:00
Simo ec7257f4ea Update .gitea/workflows/deploy.yaml
Local Build and Deploy / deploy (push) Failing after 14s
2026-07-12 14:09:57 +02:00
Simo 50496b20be Update .gitea/workflows/deploy.yaml
Local Build and Deploy / deploy (push) Failing after 18s
2026-07-12 14:08:44 +02:00
Simo 9b1bc2fd09 Update .gitea/workflows/deploy.yaml
Local Build and Deploy / deploy (push) Successful in 41s
2026-07-12 13:48:13 +02:00
Simo 69ca4b035b Update .gitea/workflows/deploy.yaml
Local Build and Deploy / deploy (push) Failing after 12s
2026-07-12 13:45:34 +02:00
Simo cdf180ee3f fix: isolate proxy session decoding
Local Build and Deploy / deploy (push) Successful in 1m2s
2026-07-12 13:39:25 +02:00
Simo c768d80cab Update .gitea/workflows/deploy.yaml
Remote Build and Deploy / deploy (push) Successful in 42s
2026-07-11 23:11:36 +02:00
Simo f9213beb9c Update .gitea/workflows/deploy.yaml
Remote Build and Deploy / deploy (push) Failing after 8s
2026-07-11 23:09:57 +02:00
Simo 3140cbbbd4 revert 71e0457e7e
Remote Build and Deploy / deploy (push) Failing after 1s
revert Update .gitea/workflows/deploy.yaml

Signed-off-by: Simo <[email protected]>
2026-07-11 23:07:56 +02:00
Simo 71e0457e7e Update .gitea/workflows/deploy.yaml
Remote Build and Deploy / deploy (push) Failing after 16s
Signed-off-by: Simo <[email protected]>
2026-07-11 23:06:52 +02:00
Simo f48532f2d0 Update .gitea/workflows/deploy.yaml
Remote Build and Deploy / deploy (push) Failing after 2s
Signed-off-by: Simo <[email protected]>
2026-07-11 23:05:14 +02:00
Simo c57a894d7a Update .gitea/workflows/deploy.yaml
Remote Build and Deploy / deploy (push) Failing after 26s
2026-07-11 23:01:53 +02:00
Simo c4bf6488d0 fix: use canonical Auth.js session in proxy
Remote Build and Deploy / deploy (push) Successful in 43s
2026-07-11 22:55:26 +02:00
Simo cfa7998dd7 fix: detect deployed Auth.js session cookie
Remote Build and Deploy / deploy (push) Successful in 44s
2026-07-11 22:46:04 +02:00
Simo 02bbcba240 fix: decode production admin session cookie
Remote Build and Deploy / deploy (push) Successful in 47s
2026-07-11 22:42:19 +02:00
Simo f08e56cf53 fix: authorize super admins by dynamic highest rank
Remote Build and Deploy / deploy (push) Successful in 42s
2026-07-11 22:35:40 +02:00
Simo bb847176ad Merge remote-tracking branch 'nextjs/main' into codex/publish-nextjs-current
Remote Build and Deploy / deploy (push) Successful in 42s
2026-07-11 22:18:52 +02:00
Simo d99b71a2d3 fix: make radio migrations safe for existing schemas 2026-07-11 22:18:45 +02:00
Simo 0ca23e6c01 Merge remote-tracking branch 'nextjs/main' into codex/publish-nextjs-current
Remote Build and Deploy / deploy (push) Successful in 27s
2026-07-11 22:11:39 +02:00
Simo 48ed1c20b6 fix: load environment for database migrations 2026-07-11 22:11:30 +02:00
Simo 2c9cb8c313 Merge remote-tracking branch 'nextjs/main' into codex/publish-nextjs-current
Remote Build and Deploy / deploy (push) Successful in 24s
2026-07-11 22:06:45 +02:00
Simo b695a33ead fix: enforce public contrast and audit rank errors 2026-07-11 22:06:45 +02:00
Simo 173274527f Merge remote-tracking branch 'nextjs/main' into codex/publish-nextjs-current
Remote Build and Deploy / deploy (push) Failing after 0s
2026-07-11 21:36:54 +02:00
Simo eaf5fc387d Merge remote-tracking branch 'nextjs/main' into codex/publish-nextjs-current 2026-07-11 21:36:17 +02:00
Simo 17264dfc06 fix: protect admin routes and ignore local docs 2026-07-11 21:35:37 +02:00
Simo 6a08db8dd0 style: normalize deploy workflow
Remote Build and Deploy / deploy (push) Failing after 0s
2026-07-11 21:27:42 +02:00
Simo 62db89119c Merge remote-tracking branch 'nextjs/main' into codex/publish-nextjs-current 2026-07-11 21:27:27 +02:00
Simo c4454a292c fix: parse SQL migration comments safely 2026-07-11 21:27:18 +02:00
Simo 8d37ae9ae0 style: normalize restored source endings
Remote Build and Deploy / deploy (push) Has been cancelled
2026-07-11 21:19:54 +02:00
Simo 9552d6b938 Merge remote-tracking branch 'nextjs/main' into codex/publish-nextjs-current 2026-07-11 21:19:37 +02:00
Simo 0cd753c735 fix: restore complete admin feature dependencies 2026-07-11 21:15:54 +02:00
Simo 5b4228261a Reapply "Add missing admin action files and navigation links"
This reverts commit 4d515bc400.
2026-07-11 20:52:56 +02:00
Simo 96ed768f14 test: add unresolved local import scanner 2026-07-11 20:52:55 +02:00
Simo cabafb4ea6 docs: plan complete admin feature recovery 2026-07-11 20:51:33 +02:00
Simo c670bd8c64 docs: design admin feature recovery 2026-07-11 20:48:45 +02:00
Simo 4d515bc400 Revert "Add missing admin action files and navigation links"
This reverts commit 41be6835bf.
2026-07-11 20:37:56 +02:00
Simo 4a1e1115b3 Harden CMS security and theme contrast 2026-07-11 20:27:20 +02:00
Simo 56cd6fd058 Fix Nitro client launcher: read the nitro_path setting
The /client launcher read a non-existent `nitro_client_url` setting, so it
always fell through to "No client configured" and the client never launched.
Faithful to AtomCMS's NitroController + nitro.blade.php, build the launch URL
as {nitro_path}/index.html?sso=<ticket> plus the toolbar colour params, via a
shared buildNitroClientUrl helper. The Flash launcher's Nitro fallback used the
same dead key and is fixed too.
2026-06-29 21:10:44 +02:00
Simo 54ec99de6d 101%: app-level DDoS guard, PWA, /api/health, API docs, worker JAR backup
Beyond parity — the web-feasible versions of the "host-only" items plus
extras AtomCMS doesn't have:
- App-level abuse/DDoS guard (src/lib/services/abuse-guard.ts): counts
  requests per IP and auto-adds flooders to website_ip_blacklist (enforced
  by the access guard) + fires ddosDetected(). OFF by default, tunable via
  settings. The iptables layer stays host-only; this is the real app-tier
  mitigation. Access guard now also enforces the IP blacklist (cached).
- PWA: a themeable web manifest (src/app/manifest.ts) + a service worker
  (public/sw.js, cache-first assets / network-first pages) registered after
  hydration — the hotel is now installable.
- /api/health: DB + emulator(RCON) + runtime status probe.
- /developers: a public API documentation page covering every REST endpoint
  with its method, path and auth requirement.
- jobs-worker: daily emulator JAR backup (runs host-side in the worker, like
  AtomCMS's backup command) — copies + prunes; no-ops unless EMULATOR_JAR_PATH
  + EMULATOR_BACKUP_DIR are set.

Verified live (prod, amx_test): /api/health ok, manifest + sw served, docs
page renders, normal pages unaffected by the guard. tsc 0, vitest 49/49,
next build 0.
2026-06-29 18:39:23 +02:00
Simo 4dfe698009 Close remaining web gaps: rich profile, login history, lightbox/slider, flash client, admin chatlog/DJ/chart/WYSIWYG, niche API
- Rich profile (/u/[username]): wallet (credits/duckets/diamonds), friends
  grid (messenger_friendships), and owned rooms sections.
- Login history: new website_login_logs table (model + migration 0007),
  recorded on every successful sign-in (ip + user-agent), surfaced on a new
  /settings/sessions page (with failed-attempt list from failed_logins).
- Photos lightbox + home article slider (client components, no Swiper dep).
- /client/flash launcher (SSO ticket like the Nitro page).
- Admin: private chatlogs section in /admin/logs, /admin/radio/moderation
  (shout moderation), a "users by rank" inline bar chart on the dashboard,
  and a TinyMCE rich-text editor on the article admin forms.
- Niche API: /api/values/[id], /api/guilds(+/[id]), /api/radio/auto-play.

Verified live (prod, amx_test): login recorded → /settings/sessions shows
it with device; profile renders wallet/friends/rooms; dashboard chart +
private-chat logs + /client/flash + /api/guilds all OK. Reverted test data.
tsc 0, vitest 49/49, next build 0.
2026-06-29 18:26:34 +02:00
Simo f7b3845131 Close the web-feasible 100% gaps: REST write/token API, tickets, draw-badge, /me, sanitisation, dusk, radio SSE
Final parity push (web-tier only):
- REST API write + token auth: POST /api/tokens (issue a personal_access_token
  for the session user), Bearer auth via src/lib/api-auth.ts, POST
  /api/articles/[slug]/comment, GET/DELETE /api/me/tokens, full tickets API
  (/api/tickets +[id] +[id]/reply), radio current-dj/points/points-leaderboard/
  embed-config + POST shouts, and a real-time /api/radio/stream (SSE). 31 public
  API routes total.
- Pages: /draw-badge (buy a custom profile badge → credits + RCON), /me
  dashboard (stats + online friends + referral claim). Wired into the nav.
- HTML sanitisation (sanitize-html) — the HTMLPurifier equivalent — applied to
  writeable boxes + article bodies before dangerouslySetInnerHTML.
- "Dusk" dark theme preset + a default-dark site option honoured by the
  no-flash boot script.

Verified live (prod, amx_test): token issue → Bearer endpoint 200, no-token
401; /api/me/tokens lists it; current-dj/leaderboard JSON; /me + /draw-badge
200; reverted the test user + tokens. tsc 0, vitest 49/49, next build 0.
2026-06-29 18:15:01 +02:00
Simo 8cedf5614e UI gaps: radio player widget, logo generator, public room page
Phase D of the parity push:
- Radio player: fixed bottom-right widget (port of atom's radio-player.blade)
  that streams the hotel radio via <audio>, with play/pause, volume, current
  DJ / now-playing and live listener count, polling the public API every 15s.
  A server gate (RadioPlayerGate) only mounts it when radio is enabled + a
  stream URL is set, so no widget JS ships when off. Mounted in the layout.
- Logo generator (/logo): client tool — hotel name + font/colour/size pickers
  with a live preview and "download PNG" via canvas.
- Public room page (/room/[id]): renders an emulator room (name, owner,
  users/max, state, category) in a ContentCard.

Verified live (prod, amx_test): /logo renders the generator, /room/50 shows
the real room "Dark Elegant Bundle", and with radio enabled the player
mounts fixed bottom-right (audio + play/pause + Test FM); reverted the test
settings. tsc 0, vitest 49/49, next build 0.
2026-06-28 21:48:42 +02:00
Simo 80f591a343 Add public REST API, anti-abuse protections, radio/GitHub cron jobs
Phase A — Public REST API (was the biggest gap). 20 JSON endpoints under
/api mirroring AtomCMS: users/[username], online(+/count), me, articles
(+/[slug]), photos, home, staff, teams, leaderboard, shop(+/categories),
values(+/categories), settings, radio/{config,now-playing,listeners,
shouts}. Shared src/lib/api.ts (apiJson — BigInt-safe + CORS, pagination).
Read-only, fail-soft, and field-safe (never exposes password/auth_ticket/
2FA secrets/mail).

Phase B — Anti-abuse on registration: CAPTCHA (Cloudflare Turnstile /
Google reCAPTCHA, settings-driven, widget rendered on the register page),
VPN/proxy detection (proxycheck.io / IPQualityScore via /admin/vpn
settings), and max-accounts-per-IP. All fail-open when unconfigured.
src/lib/services/{captcha,ip-lookup}.ts.

Phase C — jobs-worker cron suite: radio-record-songs (30s, logs track
changes to radio_song_plays), radio-auto-dj (rotates radio_auto_dj_playlist
when no live DJ), github-update-check (hourly, sets update_available).
Shared src/lib/services/radio.ts (now-playing/listeners parsing).

Verified live (prod, amx_test): /api/* return real JSON (leaderboard 6
users, settings carry no secrets, user endpoint hides password). tsc 0,
vitest 49/49, next build 0 (20 new API routes).
2026-06-28 21:44:02 +02:00
Simo 5a4b6f27e9 Extend the theme editor: typography, button/link colours, custom CSS, more presets
Grew /admin/theme from colours-only to a full theme editor, all applied
live via website_settings + ThemeVars:
- Typography: body font (10 web-safe + Google options; Google fonts load
  via an injected <link>) and H1/H2/H3 sizes (globals.css now reads
  --size-heading-* vars).
- Buttons & links: secondary/danger button colours + link/link-hover.
- Custom CSS: a raw textarea injected after the theme variables (staff-
  trusted), for anything the controls don't cover.
- Presets: 6 → 13 (added Galaxy, Royal, Cyberpunk, Neon, Coffee, Arctic,
  Christmas). ThemeVars now injects all the new vars + the font link.

Verified live (prod, amx_test): saved font=mono / H1=44px / custom CSS →
the public home reflected --font-family "Courier New", --size-heading-h1
44px and the injected rule; reverted the test settings. tsc 0,
vitest 49/49, next build 0.
2026-06-28 21:14:10 +02:00