@underpostnet/cyberia 3.3.73 → 3.3.77

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.
Files changed (269) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +10 -0
  3. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  4. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  5. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  8. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  9. package/.github/workflows/engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  11. package/AGENTS.md +61 -12
  12. package/CHANGELOG.md +159 -161
  13. package/CLI-HELP.md +20 -13
  14. package/Dockerfile +1 -1
  15. package/Dockerfile.dev +1 -1
  16. package/Dockerfile.test +2 -2
  17. package/README.md +28 -28
  18. package/bin/build.js +96 -21
  19. package/bin/cyberia.js +1247 -1373
  20. package/bin/index.js +1247 -1373
  21. package/compose.env +17 -5
  22. package/conf.js +7 -4
  23. package/deploy/cyberia-client/deploy.sh +10 -11
  24. package/deploy/cyberia-client/package.sh +0 -1
  25. package/deploy/cyberia-client/state.sh +3 -2
  26. package/deploy/cyberia-server/deploy.sh +10 -11
  27. package/deploy/cyberia-server/package.sh +0 -1
  28. package/deploy/cyberia-server/state.sh +3 -2
  29. package/deploy/dd-cyberia/deploy.sh +5 -5
  30. package/deploy/dd-cyberia/init.sh +15 -14
  31. package/deploy/dd-cyberia/package.sh +0 -1
  32. package/deploy/dd-cyberia/state.sh +2 -2
  33. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  34. package/deploy/lib/config.sh +29 -0
  35. package/deploy/lib/github-actions-logging.sh +92 -76
  36. package/deploy/lib/host.sh +28 -19
  37. package/deploy/lib/state.sh +4 -1
  38. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  39. package/deploy/release/deploy.sh +0 -2
  40. package/deployment.yaml +1 -1
  41. package/docker-compose.yml +23 -17
  42. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  43. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  44. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  45. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  46. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  47. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  48. package/docs/coverage/cyberia/base.css +362 -0
  49. package/docs/coverage/cyberia/block-navigation.js +82 -0
  50. package/docs/coverage/cyberia/favicon.png +0 -0
  51. package/docs/coverage/cyberia/index.html +161 -0
  52. package/docs/coverage/cyberia/prettify.css +101 -0
  53. package/docs/coverage/cyberia/prettify.js +937 -0
  54. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  55. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  56. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  57. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  58. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  59. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  60. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  61. package/docs/coverage/cyberia/sorter.js +205 -0
  62. package/hardhat/package-lock.json +2 -2
  63. package/hardhat/package.json +1 -1
  64. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  65. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  66. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  67. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  68. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  69. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  70. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  71. package/manifests/deployment/mongo-express/service.yaml +16 -0
  72. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  73. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  74. package/package.json +19 -17
  75. package/scripts/test-monitor.sh +3 -3
  76. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  77. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  80. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  81. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  82. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  83. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  84. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  85. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  86. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  87. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  88. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  90. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  91. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  92. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  93. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  94. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  95. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  96. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  97. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  98. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  99. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  100. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  101. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  102. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  103. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  104. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  107. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  110. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  111. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  112. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  113. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  114. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  115. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  116. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  118. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  119. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  120. package/src/api/file/file.ref.js +153 -0
  121. package/src/api/file/file.ref.json +11 -2
  122. package/src/api/object-layer/object-layer.model.js +47 -12
  123. package/src/api/object-layer/object-layer.router.js +10 -3
  124. package/src/api/object-layer/object-layer.service.js +113 -120
  125. package/src/cli/app.js +12 -1
  126. package/src/cli/client.js +2 -2
  127. package/src/cli/cluster.js +80 -18
  128. package/src/cli/deploy.js +215 -34
  129. package/src/cli/docker-compose.js +1 -1
  130. package/src/cli/fs-selection.js +115 -0
  131. package/src/cli/fs.js +158 -342
  132. package/src/cli/index.js +22 -10
  133. package/src/cli/release.js +3 -1
  134. package/src/cli/repository.js +35 -9
  135. package/src/cli/run.js +80 -199
  136. package/src/cli/test.js +4 -0
  137. package/src/cli/wireguard.js +32 -31
  138. package/src/client/components/core/Badge.js +0 -1
  139. package/src/client/components/core/CssCore.js +8 -1
  140. package/src/client/components/core/Docs.js +77 -100
  141. package/src/client/components/core/Modal.js +77 -23
  142. package/src/client/components/core/ToolTip.js +15 -0
  143. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  144. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  145. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  146. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  147. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  148. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  149. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  150. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  151. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  152. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  153. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  154. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  155. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  156. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  157. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  158. package/src/client/components/default/AppShellDefault.js +1 -4
  159. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  160. package/src/client/components/underpost/AppShellUnderpost.js +1 -1
  161. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  162. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  163. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  164. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  165. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  166. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  167. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  168. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  169. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  170. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  171. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  172. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  173. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  174. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  175. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  176. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  177. package/src/client/services/object-layer/object-layer.management.js +6 -10
  178. package/src/client/services/object-layer/object-layer.service.js +3 -1
  179. package/src/client/ssr/views/Cyberia404.js +3 -3
  180. package/src/client-builder/client-build-docs.js +52 -52
  181. package/src/client-builder/client-build.js +67 -44
  182. package/src/client-builder/client-bundle.js +347 -0
  183. package/src/db/mongo/MongoExpress.js +187 -0
  184. package/src/grpc/cyberia/grpc-server.js +5 -7
  185. package/src/index.js +1 -1
  186. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  187. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  188. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  189. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  190. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  191. package/src/projects/cyberia/gemini-client.js +3 -3
  192. package/src/projects/cyberia/generate-saga.js +4 -11
  193. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  194. package/src/projects/cyberia/instance-backup.js +210 -0
  195. package/src/projects/cyberia/instance-data.js +177 -257
  196. package/src/projects/cyberia/map-preview-generator.js +32 -68
  197. package/src/projects/cyberia/object-layer.js +152 -319
  198. package/src/projects/cyberia/seed-audio.js +139 -0
  199. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  200. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  201. package/src/projects/cyberia/stat-balance.js +81 -0
  202. package/src/projects/cyberia/stat-commands.js +23 -0
  203. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  204. package/src/runtime/cyberia-client/Dockerfile +8 -4
  205. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  206. package/src/runtime/cyberia-server/Dockerfile +9 -2
  207. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  208. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  209. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  210. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  211. package/src/runtime/engine-cyberia/compose.env +17 -5
  212. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  213. package/src/server/build/coverage.js +113 -42
  214. package/src/server/build/package.js +55 -12
  215. package/src/server/build/testing.js +59 -9
  216. package/src/server/network/middlewares.js +8 -2
  217. package/src/server/network/underpost-gateway.js +113 -14
  218. package/src/server/ops/logger.js +70 -35
  219. package/src/server/runtime/conf.js +40 -9
  220. package/src/server/runtime/start.js +11 -2
  221. package/src/server/storage/data-query.js +16 -0
  222. package/src/server/storage/downloader.js +62 -14
  223. package/src/server/storage/zip.js +153 -0
  224. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  225. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  226. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  227. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  228. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  229. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  230. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  231. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  232. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  233. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  234. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  235. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  236. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  237. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  238. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  239. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  240. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  241. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  242. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  243. package/test/unit/catalog.test.js +13 -2
  244. package/test/unit/client-build-docs.test.js +97 -70
  245. package/test/unit/client-bundle.test.js +313 -0
  246. package/test/unit/conf-resolution.test.js +58 -7
  247. package/test/unit/coverage-artifact.test.js +120 -29
  248. package/test/unit/cyberia/instance-backup.test.js +183 -0
  249. package/test/unit/cyberia/instance-data.test.js +314 -0
  250. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  251. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  252. package/test/unit/cyberia/stat-balance.test.js +69 -0
  253. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  254. package/test/unit/deploy-log-table.test.js +76 -5
  255. package/test/unit/downloader-integrity.test.js +98 -0
  256. package/test/unit/file-reference-registry.test.js +141 -0
  257. package/test/unit/fs-storage-paths.test.js +596 -0
  258. package/test/unit/gateway-pod-fetch.test.js +95 -0
  259. package/test/unit/letsencrypt-issuer.test.js +101 -0
  260. package/test/unit/logger-redaction.test.js +29 -0
  261. package/test/unit/package.test.js +70 -16
  262. package/test/unit/prepare-host.test.js +84 -1
  263. package/test/unit/release-bump.test.js +3 -1
  264. package/test/unit/start-options.test.js +12 -0
  265. package/test/unit/test-tiers.test.js +27 -0
  266. package/test/unit/zip-archive.test.js +142 -0
  267. package/vitest.config.js +11 -3
  268. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  269. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -1,23 +1,189 @@
1
+ // MainBodyCyberiaPortal.js — Cyberia portal landing.
2
+ //
3
+ // A "landing-style" world selector: the hero keeps the pixel-art particle
4
+ // field, grid, logo and title, and instead of a background video it surfaces
5
+ // the first three playable worlds from the same registry the full
6
+ // InstanceSelectionView consumes (`defaultInstanceProvider`, limited to 3).
7
+ //
8
+ // Each world is a space-framed thumbnail card (starfield frame + rotating
9
+ // beam) with a staggered pixel-step entrance and its own float loop. Clicking a
10
+ // card selects it: the selected world's thumbnail becomes the blurred hero
11
+ // backdrop and the single CTA enters that world directly. The full selector
12
+ // modal stays reachable through the secondary "browse all worlds" link.
13
+
1
14
  import { range } from '../core/CommonJs.js';
2
15
  import { ThemeEvents, darkTheme } from '../core/Css.js';
3
16
  import { EventsUI } from '../core/EventsUI.js';
17
+ import { NotificationManager } from '../core/NotificationManager.js';
4
18
  import { getProxyPath } from '../core/Router.js';
5
19
  import { htmls, s } from '../core/VanillaJs.js';
20
+ import {
21
+ defaultInstanceProvider,
22
+ placeholderThumbnail,
23
+ DEFAULT_CLIENT_BASE_URL,
24
+ STATUS_META,
25
+ } from '../cyberia/InstanceSelectionView.js';
26
+
27
+ const LANDING_WORLD_LIMIT = 3;
28
+
29
+ const statusMeta = (status) => STATUS_META[status] || STATUS_META.offline;
30
+
31
+ const escapeHtml = (value = '') =>
32
+ String(value).replace(
33
+ /[&<>"']/g,
34
+ (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c],
35
+ );
36
+
37
+ const worldCard = (instance, index, selected) => {
38
+ const meta = statusMeta(instance.status);
39
+ return html`
40
+ <button
41
+ class="hero-world hero-world-${index} ${selected ? 'hero-world-selected' : ''} ${
42
+ instance.playable ? '' : 'hero-world-unavailable'
43
+ }"
44
+ data-instance-id="${escapeHtml(instance.id)}"
45
+ type="button"
46
+ title="${escapeHtml(instance.name)}"
47
+ style="--world-index: ${index};"
48
+ >
49
+ <span class="hero-world-frame"></span>
50
+ <span class="hero-world-inner">
51
+ <img
52
+ class="hero-world-img"
53
+ src="${escapeHtml(instance.thumbnailUrl)}"
54
+ alt="${escapeHtml(instance.name)}"
55
+ loading="eager"
56
+ onerror="this.onerror=null;this.src='${placeholderThumbnail()}';"
57
+ />
58
+ <span class="hero-world-status hero-tone-${meta.tone}">
59
+ <span class="hero-world-dot"></span>${meta.label}
60
+ </span>
61
+ <span class="hero-world-check"><i class="fa-solid fa-check"></i></span>
62
+ <span class="hero-world-caption">
63
+ <span class="hero-world-name">${escapeHtml(instance.name)}</span>
64
+ ${instance.code ? html`<span class="hero-world-code">${escapeHtml(instance.code)}</span>` : ''}
65
+ </span>
66
+ </span>
67
+ </button>
68
+ `;
69
+ };
70
+
71
+ const worldSkeleton = () =>
72
+ range(0, LANDING_WORLD_LIMIT - 1)
73
+ .map(
74
+ (i) => html`
75
+ <div class="hero-world hero-world-${i} hero-world-skeleton" style="--world-index: ${i};">
76
+ <span class="hero-world-frame"></span>
77
+ <span class="hero-world-inner"></span>
78
+ </div>
79
+ `,
80
+ )
81
+ .join('');
6
82
 
7
83
  class MainBodyCyberiaPortal {
8
84
  static async instance() {
85
+ const id = 'cyberia-portal-landing';
86
+
87
+ // Per-render landing state: the 3 featured worlds and the one the CTA enters.
88
+ const state = { worlds: [], selectedId: null };
89
+ const selectedWorld = () => state.worlds.find((w) => w.id === state.selectedId) || null;
90
+
91
+ const worldsSel = `.hero-worlds`;
92
+ const ctaSel = `.cta-button`;
93
+ const backdropSel = `.hero-backdrop-img`;
94
+
95
+ const renderCta = () => {
96
+ const cta = s(ctaSel);
97
+ if (!cta) return;
98
+ const world = selectedWorld();
99
+ if (!world) {
100
+ cta.disabled = false;
101
+ cta.classList.remove('cta-button-disabled');
102
+ htmls(ctaSel, html`<i class="fa-solid fa-play"></i> Enter The World`);
103
+ return;
104
+ }
105
+ const meta = statusMeta(world.status);
106
+ cta.disabled = !world.playable;
107
+ cta.classList.toggle('cta-button-disabled', !world.playable);
108
+ htmls(
109
+ ctaSel,
110
+ world.playable
111
+ ? html`<i class="fa-solid fa-play"></i> Enter <span class="cta-world-name">${escapeHtml(world.name)}</span>`
112
+ : html`<i class="fa-solid fa-lock"></i> ${escapeHtml(world.name)} is ${meta.cta || meta.label}`,
113
+ );
114
+ };
115
+
116
+ const renderBackdrop = () => {
117
+ const img = s(backdropSel);
118
+ const world = selectedWorld();
119
+ if (!img || !world) return;
120
+ if (img.getAttribute('src') === world.thumbnailUrl) return;
121
+ img.classList.remove('hero-backdrop-visible');
122
+ img.onload = () => img.classList.add('hero-backdrop-visible');
123
+ img.onerror = () => img.classList.remove('hero-backdrop-visible');
124
+ img.src = world.thumbnailUrl;
125
+ };
126
+
127
+ const renderWorlds = () => {
128
+ if (!s(worldsSel)) return;
129
+ htmls(worldsSel, state.worlds.map((w, i) => worldCard(w, i, w.id === state.selectedId)).join(''));
130
+ };
131
+
132
+ const select = (instanceId) => {
133
+ if (!state.worlds.some((w) => w.id === instanceId)) return;
134
+ state.selectedId = instanceId;
135
+ const cards = s(worldsSel)?.querySelectorAll('.hero-world') || [];
136
+ cards.forEach((card) => card.classList.toggle('hero-world-selected', card.dataset.instanceId === instanceId));
137
+ renderCta();
138
+ renderBackdrop();
139
+ };
140
+
141
+ const launch = () => {
142
+ const world = selectedWorld();
143
+ if (!world) return (location.href = `${DEFAULT_CLIENT_BASE_URL}/`);
144
+ if (!world.playable) {
145
+ NotificationManager.Push({
146
+ html: `${world.name} is ${statusMeta(world.status).label}.`,
147
+ status: 'warning',
148
+ });
149
+ return;
150
+ }
151
+ location.href = world.playUrl || `${DEFAULT_CLIENT_BASE_URL}/`;
152
+ };
153
+
154
+ const load = async () => {
155
+ if (s(worldsSel)) htmls(worldsSel, worldSkeleton());
156
+ try {
157
+ state.worlds = await defaultInstanceProvider({ limit: LANDING_WORLD_LIMIT });
158
+ } catch (err) {
159
+ console.warn('[cyberia-portal-landing] could not load featured worlds', err);
160
+ state.worlds = [];
161
+ }
162
+ const first = state.worlds.find((w) => w.playable) || state.worlds[0];
163
+ state.selectedId = first ? first.id : null;
164
+ const section = s(`.hero-section`);
165
+ if (section) section.classList.toggle('hero-section-no-worlds', state.worlds.length === 0);
166
+ renderWorlds();
167
+ renderCta();
168
+ renderBackdrop();
169
+ };
170
+
9
171
  setTimeout(() => {
10
- EventsUI.onClick('.cta-button', () => {
11
- console.log('Enter the world button clicked');
12
- location.href = 'https://client.cyberiaonline.com/';
13
- });
14
- EventsUI.onClick('.cta-button-select-instance', () => {
15
- console.log('or select one button clicked');
16
- s(`.main-btn-instance-selection`).click();
17
- });
18
- });
172
+ EventsUI.onClick(ctaSel, launch);
173
+ EventsUI.onClick('.cta-button-select-instance', () => s(`.main-btn-instance-selection`).click());
19
174
 
20
- const id = 'cyberia-portal-landing';
175
+ const worlds = s(worldsSel);
176
+ if (worlds)
177
+ worlds.addEventListener('click', (event) => {
178
+ const card = event.target.closest?.('.hero-world:not(.hero-world-skeleton)');
179
+ if (!card) return;
180
+ // Second click on the already selected world enters it directly.
181
+ if (card.dataset.instanceId === state.selectedId) return launch();
182
+ select(card.dataset.instanceId);
183
+ });
184
+
185
+ load();
186
+ });
21
187
 
22
188
  // Stable pixel-art particle field: positions/motion fixed once so they don't jump on theme change.
23
189
  // Colors are theme-driven via CSS custom properties resolved in ThemeEvents.
@@ -56,12 +222,12 @@ class MainBodyCyberiaPortal {
56
222
  --footer-bg-color: ${darkTheme ? '#101010' : '#E3E3E3'};
57
223
  --card-bg-color: ${darkTheme ? '#2c2c2c' : '#FFFFFF'};
58
224
  --card-shadow: ${darkTheme ? '0 8px 25px rgba(0, 0, 0, 0.5)' : '0 8px 25px rgba(0, 0, 0, 0.1)'};
59
- --btn-primary-bg: ${darkTheme
60
- ? 'linear-gradient(45deg, #9b59b6, #8e44ad)'
61
- : 'linear-gradient(45deg, #ffcc00, #e6b800)'};
62
- --btn-primary-shadow: ${darkTheme
63
- ? '0 4px 15px rgba(155, 89, 182, 0.4)'
64
- : '0 4px 15px rgba(255, 204, 0, 0.4)'};
225
+ --btn-primary-bg: ${
226
+ darkTheme ? 'linear-gradient(45deg, #9b59b6, #8e44ad)' : 'linear-gradient(45deg, #ffcc00, #e6b800)'
227
+ };
228
+ --btn-primary-shadow: ${
229
+ darkTheme ? '0 4px 15px rgba(155, 89, 182, 0.4)' : '0 4px 15px rgba(255, 204, 0, 0.4)'
230
+ };
65
231
 
66
232
  /* Pixel-art particle + title palette (theme aware) */
67
233
  --particle-color-1: ${darkTheme ? '#bb8fce' : '#ffcc00'};
@@ -69,10 +235,39 @@ class MainBodyCyberiaPortal {
69
235
  --pixel-glow: ${darkTheme ? '14px' : '7px'};
70
236
  --title-accent: ${darkTheme ? '#bb8fce' : '#e6b800'};
71
237
  --title-shadow: ${darkTheme ? 'rgba(0, 0, 0, 0.85)' : 'rgba(0, 0, 0, 0.55)'};
72
- --hero-overlay: ${darkTheme
73
- ? 'linear-gradient(180deg, rgba(0, 0, 0, 0.34), rgba(0, 0, 0, 0.5))'
74
- : 'linear-gradient(180deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.35))'};
75
238
  --pixel-grid-color: ${darkTheme ? 'rgba(155, 89, 182, 0.75)' : 'rgba(255, 204, 0, 1)'};
239
+
240
+ /* Hero space backdrop: deep nebula gradient, the selected world's
241
+ thumbnail is blurred on top of it, then a tint keeps text legible */
242
+ --hero-space: ${
243
+ darkTheme
244
+ ? 'radial-gradient(ellipse 70% 60% at 20% 20%, rgba(155, 89, 182, 0.45), transparent 60%), radial-gradient(ellipse 60% 50% at 85% 80%, rgba(93, 41, 128, 0.5), transparent 60%), linear-gradient(180deg, #0b0a12, #15111f)'
245
+ : 'radial-gradient(ellipse 70% 60% at 20% 20%, rgba(255, 204, 0, 0.35), transparent 60%), radial-gradient(ellipse 60% 50% at 85% 80%, rgba(230, 150, 0, 0.35), transparent 60%), linear-gradient(180deg, #1c1f33, #2a2340)'
246
+ };
247
+ --hero-overlay: ${
248
+ darkTheme
249
+ ? 'linear-gradient(180deg, rgba(8, 6, 14, 0.55), rgba(8, 6, 14, 0.78))'
250
+ : 'linear-gradient(180deg, rgba(16, 14, 30, 0.42), rgba(16, 14, 30, 0.7))'
251
+ };
252
+ --hero-backdrop-opacity: ${darkTheme ? '0.55' : '0.6'};
253
+
254
+ /* World cards */
255
+ --world-frame-bg: ${darkTheme ? '#08060e' : '#12101f'};
256
+ --world-star: ${darkTheme ? 'rgba(255, 255, 255, 0.9)' : 'rgba(255, 244, 200, 0.95)'};
257
+ --world-beam: ${darkTheme ? '#bb8fce' : '#ffcc00'};
258
+ --world-beam-2: ${darkTheme ? '#8e44ad' : '#e6b800'};
259
+ --world-caption-bg: ${
260
+ darkTheme
261
+ ? 'linear-gradient(0deg, rgba(8, 6, 14, 0.95), rgba(8, 6, 14, 0))'
262
+ : 'linear-gradient(0deg, rgba(18, 16, 31, 0.95), rgba(18, 16, 31, 0))'
263
+ };
264
+ --world-glow: ${darkTheme ? 'rgba(187, 143, 206, 0.55)' : 'rgba(255, 204, 0, 0.55)'};
265
+ --world-ok: ${darkTheme ? '#43d17a' : '#3ddc84'};
266
+ --world-warn: ${darkTheme ? '#f0b429' : '#ffc233'};
267
+ --world-err: ${darkTheme ? '#ff5b5b' : '#ff6b6b'};
268
+ --world-info: ${darkTheme ? '#c084fc' : '#c9a2ff'};
269
+ --world-muted: ${darkTheme ? '#8a8a96' : '#b0b0bd'};
270
+ --world-accent: ${darkTheme ? '#bb8fce' : '#ffcc00'};
76
271
  }
77
272
 
78
273
  .landing-page {
@@ -90,36 +285,45 @@ class MainBodyCyberiaPortal {
90
285
  display: flex;
91
286
  align-items: center;
92
287
  justify-content: center;
93
- text-align: center;
94
288
  min-height: 100vh;
95
- /* background: url('${getProxyPath()}assets/lore/vectorized/lore8.svg') no-repeat center center/cover;*/
96
289
  position: relative;
97
- padding: 0 1rem;
290
+ padding: 4rem 1rem 3.5rem;
98
291
  overflow: hidden;
292
+ background: var(--hero-space);
99
293
  }
100
- .hero-video {
294
+ /* Selected world thumbnail, blurred and scaled, behind everything */
295
+ .hero-backdrop {
101
296
  position: absolute;
102
297
  inset: 0;
103
-
104
- width: 100%;
105
- height: 100%;
106
-
107
- display: block;
108
-
298
+ z-index: 0;
299
+ overflow: hidden;
300
+ pointer-events: none;
301
+ }
302
+ .hero-backdrop-img {
303
+ position: absolute;
304
+ inset: -6%;
305
+ width: 112%;
306
+ height: 112%;
109
307
  object-fit: cover;
110
308
  object-position: center center;
111
-
112
- pointer-events: none;
113
- user-select: none;
114
- z-index: 0;
309
+ image-rendering: pixelated;
310
+ filter: blur(18px) saturate(1.2);
311
+ opacity: 0;
312
+ transform: scale(1.04);
313
+ transition:
314
+ opacity 1.1s ease,
315
+ transform 12s ease-out;
316
+ }
317
+ .hero-backdrop-img.hero-backdrop-visible {
318
+ opacity: var(--hero-backdrop-opacity);
319
+ transform: scale(1);
115
320
  }
116
- /* Soft blurred backdrop over the video (kept light) */
321
+ /* Tint over the backdrop so the white hero text stays legible */
117
322
  .hero-section::after {
118
323
  content: '';
119
324
  position: absolute;
120
325
  inset: 0;
121
326
  background: var(--hero-overlay);
122
-
123
327
  z-index: 1;
124
328
  }
125
329
  /* Pixel grid overlay (theme aware, slow drift) */
@@ -137,6 +341,7 @@ class MainBodyCyberiaPortal {
137
341
  -webkit-mask-image: radial-gradient(circle at center, rgba(0, 0, 0, 1), transparent 92%);
138
342
  mask-image: radial-gradient(circle at center, rgba(0, 0, 0, 1), transparent 92%);
139
343
  animation: heroGridDrift 34s linear infinite;
344
+ opacity: 0.35;
140
345
  }
141
346
 
142
347
  /* Pixel-art particle field */
@@ -165,15 +370,26 @@ class MainBodyCyberiaPortal {
165
370
  box-shadow: 0 0 var(--pixel-glow) var(--particle-color-2);
166
371
  }
167
372
 
168
- .hero-content {
373
+ /* Two-column hero: copy + CTA left, world deck right */
374
+ .hero-layout {
169
375
  position: relative;
170
376
  z-index: 5;
171
- max-width: 800px;
377
+ display: grid;
378
+ grid-template-columns: minmax(0, 1fr);
379
+ gap: 2.5rem 3rem;
380
+ align-items: center;
381
+ width: 100%;
382
+ max-width: 1180px;
172
383
  animation: fadeInUp 1s ease-out;
173
384
  }
385
+ .hero-content {
386
+ text-align: center;
387
+ max-width: 620px;
388
+ margin: 0 auto;
389
+ }
174
390
  .hero-content .logo-image {
175
- max-width: 250px;
176
- margin-bottom: 1rem;
391
+ max-width: 210px;
392
+ margin-bottom: 0.5rem;
177
393
  image-rendering: pixelated;
178
394
  transform-origin: center bottom;
179
395
  animation:
@@ -184,7 +400,7 @@ class MainBodyCyberiaPortal {
184
400
  position: relative;
185
401
  display: inline-block;
186
402
  font-size: 4rem;
187
- margin-bottom: 1rem;
403
+ margin: 10px 0;
188
404
  font-weight: 700;
189
405
  letter-spacing: 4px;
190
406
  color: #fff;
@@ -194,17 +410,51 @@ class MainBodyCyberiaPortal {
194
410
  animation: heroTitlePixelIn 1s steps(18, end) 0.55s both;
195
411
  }
196
412
  .hero-content p {
197
- font-size: 1.25rem;
198
- margin-bottom: 2.5rem;
199
- max-width: 600px;
200
- margin-left: auto;
201
- margin-right: auto;
413
+ font-size: 1.2rem;
414
+ margin: 10px auto 1.8rem;
415
+ max-width: 560px;
202
416
  line-height: 1.6;
417
+ color: #fff;
203
418
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.6);
204
419
  animation: heroFadeUp 0.8s steps(6, end) 0.95s both;
205
420
  }
421
+ .hero-kicker {
422
+ display: inline-flex;
423
+ align-items: center;
424
+ gap: 8px;
425
+ margin-bottom: 0.9rem;
426
+ padding: 5px 12px;
427
+ border: 2px solid var(--world-accent);
428
+ color: #fff;
429
+ font-size: 0.72rem;
430
+ font-weight: 700;
431
+ letter-spacing: 3px;
432
+ text-transform: uppercase;
433
+ background: rgba(0, 0, 0, 0.35);
434
+ animation: heroFadeUp 0.7s steps(6, end) 1.15s both;
435
+ }
436
+ .hero-kicker-dot {
437
+ width: 8px;
438
+ height: 8px;
439
+ background: var(--world-ok);
440
+ box-shadow: 0 0 10px var(--world-ok);
441
+ animation: heroPulse 1.6s ease-in-out infinite;
442
+ }
443
+ .hero-actions {
444
+ display: flex;
445
+ flex-direction: column;
446
+ align-items: center;
447
+ gap: 10px;
448
+ animation: heroFadeUp 0.8s steps(6, end) 1.25s both;
449
+ }
206
450
 
207
451
  .cta-button {
452
+ position: relative;
453
+ display: inline-flex;
454
+ align-items: center;
455
+ justify-content: center;
456
+ gap: 12px;
457
+ max-width: 100%;
208
458
  padding: 15px 35px;
209
459
  border: none;
210
460
  border-radius: 50px;
@@ -215,34 +465,280 @@ class MainBodyCyberiaPortal {
215
465
  cursor: pointer;
216
466
  transition:
217
467
  transform 0.3s,
218
- box-shadow 0.3s;
468
+ box-shadow 0.3s,
469
+ opacity 0.3s;
219
470
  box-shadow: var(--btn-primary-shadow);
220
471
  text-transform: uppercase;
472
+ letter-spacing: 1px;
221
473
  }
222
- .cta-button:hover {
474
+ .cta-button .cta-world-name {
475
+ max-width: 260px;
476
+ overflow: hidden;
477
+ text-overflow: ellipsis;
478
+ white-space: nowrap;
479
+ }
480
+ .cta-button:hover:not(:disabled) {
223
481
  transform: translateY(-5px);
224
- box-shadow: 0 6px 20px rgba(231, 76, 60, 0.6);
482
+ box-shadow: 0 6px 20px var(--world-glow);
483
+ }
484
+ .cta-button:disabled,
485
+ .cta-button-disabled {
486
+ cursor: not-allowed;
487
+ opacity: 0.6;
488
+ filter: grayscale(0.5);
225
489
  }
226
490
  .cta-button-select-instance {
227
- margin-top: 12px;
228
- display: block;
229
- margin-left: auto;
230
- margin-right: auto;
231
- padding: 10px 25px;
232
- border: 1px solid var(--primary-color);
491
+ display: inline-flex;
492
+ align-items: center;
493
+ gap: 8px;
494
+ padding: 8px 18px;
495
+ border: 1px solid rgba(255, 255, 255, 0.35);
233
496
  border-radius: 50px;
234
- font-size: 0.95rem;
497
+ font-size: 0.9rem;
235
498
  cursor: pointer;
236
499
  transition:
237
500
  transform 0.3s,
238
- box-shadow 0.3s;
239
- background: var(--card-bg-color);
240
- color: var(--text-color);
241
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
501
+ box-shadow 0.3s,
502
+ border-color 0.3s;
503
+ background: rgba(0, 0, 0, 0.35);
504
+ color: #fff;
505
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
242
506
  }
243
507
  .cta-button-select-instance:hover {
244
508
  transform: translateY(-2px);
245
- box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
509
+ border-color: var(--world-accent);
510
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3);
511
+ }
512
+
513
+ /* World deck: 3 space-framed thumbnails in a staggered cascade */
514
+ .hero-worlds {
515
+ display: grid;
516
+ grid-template-columns: repeat(3, minmax(0, 1fr));
517
+ gap: 14px;
518
+ width: 100%;
519
+ max-width: 720px;
520
+ margin: 0 auto;
521
+ align-items: start;
522
+ }
523
+ .hero-section-no-worlds .hero-worlds {
524
+ display: none;
525
+ }
526
+ .hero-world {
527
+ position: relative;
528
+ display: block;
529
+ width: 100%;
530
+ padding: 0;
531
+ border: none;
532
+ background: transparent;
533
+ color: #fff;
534
+ font-family: inherit;
535
+ text-align: left;
536
+ cursor: pointer;
537
+ /* pixel-step entrance (after the title), then an endless gentle float
538
+ whose phase is offset per card so the deck never bobs in unison */
539
+ animation:
540
+ heroWorldIn 0.75s steps(8, end) calc(1.15s + var(--world-index, 0) * 0.16s) both,
541
+ heroWorldFloat calc(5.2s + var(--world-index, 0) * 0.7s) ease-in-out
542
+ calc(1.9s + var(--world-index, 0) * 0.16s) infinite;
543
+ }
544
+ .hero-world:focus-visible {
545
+ outline: 3px solid var(--world-accent);
546
+ outline-offset: 4px;
547
+ }
548
+ /* Space frame: starfield padding ring with a slowly rotating light beam */
549
+ .hero-world-frame {
550
+ position: absolute;
551
+ inset: 0;
552
+ overflow: hidden;
553
+ background-color: var(--world-frame-bg);
554
+ background-image:
555
+ radial-gradient(circle, var(--world-star) 0.9px, transparent 1.4px),
556
+ radial-gradient(circle, var(--world-star) 0.7px, transparent 1.2px),
557
+ radial-gradient(circle, var(--world-star) 1.1px, transparent 1.6px);
558
+ background-size:
559
+ 23px 23px,
560
+ 37px 41px,
561
+ 59px 53px;
562
+ background-position:
563
+ 0 0,
564
+ 11px 7px,
565
+ 29px 19px;
566
+ animation: heroStarDrift 60s linear infinite;
567
+ transition: box-shadow 0.35s ease;
568
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.55);
569
+ }
570
+ .hero-world-frame::before {
571
+ content: '';
572
+ position: absolute;
573
+ inset: -75%;
574
+ background: conic-gradient(
575
+ from 0deg,
576
+ transparent 0deg,
577
+ transparent 250deg,
578
+ var(--world-beam-2) 300deg,
579
+ var(--world-beam) 330deg,
580
+ transparent 360deg
581
+ );
582
+ opacity: 0.85;
583
+ animation: heroBeamSpin 7s linear infinite;
584
+ animation-delay: calc(var(--world-index, 0) * -2.3s);
585
+ }
586
+ .hero-world-inner {
587
+ position: relative;
588
+ z-index: 1;
589
+ display: block;
590
+ margin: 6px;
591
+ aspect-ratio: 16 / 11;
592
+ overflow: hidden;
593
+ background: var(--world-frame-bg);
594
+ transform: scale(1);
595
+ transition:
596
+ transform 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.2),
597
+ filter 0.35s ease;
598
+ }
599
+ .hero-world-img {
600
+ display: block;
601
+ width: 100%;
602
+ height: 100%;
603
+ object-fit: cover;
604
+ image-rendering: pixelated;
605
+ transform: scale(1.02);
606
+ transition:
607
+ transform 0.6s ease,
608
+ filter 0.35s ease;
609
+ filter: saturate(0.85) brightness(0.9);
610
+ }
611
+ .hero-world:hover .hero-world-img,
612
+ .hero-world-selected .hero-world-img {
613
+ transform: scale(1.1);
614
+ filter: saturate(1.1) brightness(1);
615
+ }
616
+ .hero-world:hover .hero-world-frame,
617
+ .hero-world-selected .hero-world-frame {
618
+ box-shadow:
619
+ 0 0 0 2px var(--world-accent),
620
+ 0 0 26px var(--world-glow),
621
+ 0 14px 32px rgba(0, 0, 0, 0.6);
622
+ }
623
+ .hero-world-selected .hero-world-frame::before {
624
+ opacity: 1;
625
+ animation-duration: 3.2s;
626
+ }
627
+ .hero-world-selected .hero-world-inner {
628
+ transform: scale(1.03);
629
+ }
630
+ .hero-world-unavailable .hero-world-img {
631
+ filter: grayscale(0.7) brightness(0.65);
632
+ }
633
+ .hero-world-status {
634
+ position: absolute;
635
+ top: 8px;
636
+ left: 8px;
637
+ z-index: 2;
638
+ display: inline-flex;
639
+ align-items: center;
640
+ gap: 6px;
641
+ padding: 3px 8px;
642
+ font-size: 0.62rem;
643
+ font-weight: 700;
644
+ letter-spacing: 1.5px;
645
+ text-transform: uppercase;
646
+ color: #fff;
647
+ background: rgba(0, 0, 0, 0.65);
648
+ border-left: 3px solid var(--tone, var(--world-muted));
649
+ }
650
+ .hero-world-dot {
651
+ width: 7px;
652
+ height: 7px;
653
+ background: var(--tone, var(--world-muted));
654
+ box-shadow: 0 0 8px var(--tone, var(--world-muted));
655
+ animation: heroPulse 1.6s ease-in-out infinite;
656
+ }
657
+ .hero-tone-ok {
658
+ --tone: var(--world-ok);
659
+ }
660
+ .hero-tone-warn {
661
+ --tone: var(--world-warn);
662
+ }
663
+ .hero-tone-err {
664
+ --tone: var(--world-err);
665
+ }
666
+ .hero-tone-info {
667
+ --tone: var(--world-info);
668
+ }
669
+ .hero-tone-muted {
670
+ --tone: var(--world-muted);
671
+ }
672
+ .hero-tone-accent {
673
+ --tone: var(--world-accent);
674
+ }
675
+ .hero-world-check {
676
+ position: absolute;
677
+ top: 6px;
678
+ right: 6px;
679
+ z-index: 2;
680
+ display: flex;
681
+ align-items: center;
682
+ justify-content: center;
683
+ width: 26px;
684
+ height: 26px;
685
+ font-size: 0.8rem;
686
+ color: #111;
687
+ background: var(--world-accent);
688
+ box-shadow: 0 0 12px var(--world-glow);
689
+ opacity: 0;
690
+ transform: scale(0.4);
691
+ transition:
692
+ opacity 0.25s ease,
693
+ transform 0.25s cubic-bezier(0.2, 0.9, 0.3, 1.4);
694
+ }
695
+ .hero-world-selected .hero-world-check {
696
+ opacity: 1;
697
+ transform: scale(1);
698
+ }
699
+ .hero-world-caption {
700
+ position: absolute;
701
+ left: 0;
702
+ right: 0;
703
+ bottom: 0;
704
+ z-index: 2;
705
+ display: flex;
706
+ flex-direction: column;
707
+ gap: 2px;
708
+ padding: 22px 10px 9px;
709
+ background: var(--world-caption-bg);
710
+ }
711
+ .hero-world-name {
712
+ font-size: 0.95rem;
713
+ font-weight: 700;
714
+ letter-spacing: 0.5px;
715
+ white-space: nowrap;
716
+ overflow: hidden;
717
+ text-overflow: ellipsis;
718
+ text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.7);
719
+ }
720
+ .hero-world-code {
721
+ font-size: 0.66rem;
722
+ letter-spacing: 2px;
723
+ text-transform: uppercase;
724
+ color: var(--world-accent);
725
+ white-space: nowrap;
726
+ overflow: hidden;
727
+ text-overflow: ellipsis;
728
+ }
729
+ .hero-world-skeleton {
730
+ cursor: default;
731
+ animation: heroWorldIn 0.75s steps(8, end) calc(1.15s + var(--world-index, 0) * 0.16s) both;
732
+ }
733
+ .hero-world-skeleton .hero-world-inner {
734
+ background: linear-gradient(
735
+ 90deg,
736
+ rgba(255, 255, 255, 0.04) 25%,
737
+ rgba(255, 255, 255, 0.12) 37%,
738
+ rgba(255, 255, 255, 0.04) 63%
739
+ );
740
+ background-size: 400% 100%;
741
+ animation: heroShimmer 1.3s ease infinite;
246
742
  }
247
743
 
248
744
  /* Features Section */
@@ -272,6 +768,36 @@ class MainBodyCyberiaPortal {
272
768
  .features-grid {
273
769
  grid-template-columns: repeat(auto-fit, minmax(22%, 1fr)); /* 4 columns */
274
770
  }
771
+ .hero-layout {
772
+ grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
773
+ }
774
+ .hero-content {
775
+ text-align: left;
776
+ margin: 0;
777
+ }
778
+ .hero-content p {
779
+ margin-left: 0;
780
+ margin-right: 0;
781
+ }
782
+ .hero-actions {
783
+ align-items: flex-start;
784
+ }
785
+ /* cascade: each card steps down and right, selected one pops forward */
786
+ .hero-worlds {
787
+ grid-template-columns: repeat(3, minmax(0, 1fr));
788
+ max-width: none;
789
+ padding-top: 24px;
790
+ padding-bottom: 24px;
791
+ }
792
+ .hero-world-0 {
793
+ --card-y: 24px;
794
+ }
795
+ .hero-world-1 {
796
+ --card-y: -12px;
797
+ }
798
+ .hero-world-2 {
799
+ --card-y: 36px;
800
+ }
275
801
  }
276
802
  .feature-card {
277
803
  background: var(--card-bg-color);
@@ -367,6 +893,73 @@ class MainBodyCyberiaPortal {
367
893
  }
368
894
  }
369
895
 
896
+ /* World cards: pixel-step drop-in, then a slow bob (offsets are baked in
897
+ via --card-y so the desktop cascade survives the float loop) */
898
+ @keyframes heroWorldIn {
899
+ 0% {
900
+ opacity: 0;
901
+ transform: translateY(calc(var(--card-y, 0px) + 48px)) scale(0.82);
902
+ }
903
+ 60% {
904
+ opacity: 1;
905
+ transform: translateY(calc(var(--card-y, 0px) - 8px)) scale(1.03);
906
+ }
907
+ 100% {
908
+ opacity: 1;
909
+ transform: translateY(var(--card-y, 0px)) scale(1);
910
+ }
911
+ }
912
+ @keyframes heroWorldFloat {
913
+ 0%,
914
+ 100% {
915
+ transform: translateY(var(--card-y, 0px));
916
+ }
917
+ 50% {
918
+ transform: translateY(calc(var(--card-y, 0px) - 9px));
919
+ }
920
+ }
921
+ @keyframes heroBeamSpin {
922
+ from {
923
+ transform: rotate(0deg);
924
+ }
925
+ to {
926
+ transform: rotate(360deg);
927
+ }
928
+ }
929
+ @keyframes heroStarDrift {
930
+ from {
931
+ background-position:
932
+ 0 0,
933
+ 11px 7px,
934
+ 29px 19px;
935
+ }
936
+ to {
937
+ background-position:
938
+ -46px 46px,
939
+ -63px 89px,
940
+ -89px 125px;
941
+ }
942
+ }
943
+ @keyframes heroShimmer {
944
+ 0% {
945
+ background-position: 100% 0;
946
+ }
947
+ 100% {
948
+ background-position: 0 0;
949
+ }
950
+ }
951
+ @keyframes heroPulse {
952
+ 0%,
953
+ 100% {
954
+ opacity: 1;
955
+ transform: scale(1);
956
+ }
957
+ 50% {
958
+ opacity: 0.45;
959
+ transform: scale(0.75);
960
+ }
961
+ }
962
+
370
963
  /* Pixel-art particles rise with drift + spin; grid drifts slowly */
371
964
  @keyframes heroParticleFloat {
372
965
  0% {
@@ -402,13 +995,29 @@ class MainBodyCyberiaPortal {
402
995
  .hero-pixel-grid,
403
996
  .hero-content .logo-image,
404
997
  .hero-content h1,
405
- .hero-content p {
998
+ .hero-content p,
999
+ .hero-kicker,
1000
+ .hero-kicker-dot,
1001
+ .hero-actions,
1002
+ .hero-world,
1003
+ .hero-world-frame,
1004
+ .hero-world-frame::before,
1005
+ .hero-world-dot,
1006
+ .hero-world-skeleton .hero-world-inner {
406
1007
  animation: none !important;
407
1008
  }
1009
+ .hero-backdrop-img,
1010
+ .hero-world-inner,
1011
+ .hero-world-img {
1012
+ transition: none !important;
1013
+ }
408
1014
  }
409
1015
 
410
1016
  /* Responsive Styles */
411
1017
  @media (max-width: 650px) {
1018
+ .hero-section {
1019
+ padding: 3.5rem 1rem 3rem;
1020
+ }
412
1021
  .hero-content h1 {
413
1022
  font-size: 2.2rem;
414
1023
  }
@@ -416,12 +1025,34 @@ class MainBodyCyberiaPortal {
416
1025
  font-size: 1rem;
417
1026
  }
418
1027
  .hero-content .logo-image {
419
- max-width: 180px;
1028
+ max-width: 160px;
420
1029
  }
421
1030
  .cta-button {
422
- padding: 12px 28px;
1031
+ padding: 12px 24px;
423
1032
  font-size: 1rem;
424
1033
  }
1034
+ .cta-button .cta-world-name {
1035
+ max-width: 160px;
1036
+ }
1037
+ .hero-worlds {
1038
+ gap: 10px;
1039
+ }
1040
+ .hero-world-inner {
1041
+ margin: 4px;
1042
+ }
1043
+ .hero-world-name {
1044
+ font-size: 0.78rem;
1045
+ }
1046
+ .hero-world-code {
1047
+ display: none;
1048
+ }
1049
+ .hero-world-status {
1050
+ font-size: 0.55rem;
1051
+ padding: 2px 6px;
1052
+ }
1053
+ .hero-world-caption {
1054
+ padding: 16px 7px 6px;
1055
+ }
425
1056
  .features-section {
426
1057
  padding: 3rem 1rem;
427
1058
  }
@@ -443,21 +1074,26 @@ class MainBodyCyberiaPortal {
443
1074
  <div class="style-${id}"></div>
444
1075
  <div class="landing-page">
445
1076
  <section class="hero-section">
446
- <video class="hero-video" autoplay muted loop playsinline preload="auto" poster="/assets/video/landing.webp">
447
- <!-- H.264 (compatibilidad) -->
448
- <source src="/assets/video/landing-web.mp4" type='video/mp4; codecs="avc1.42E01E"' />
449
- </video>
1077
+ <div class="hero-backdrop"><img class="hero-backdrop-img" alt="" aria-hidden="true" /></div>
450
1078
  <div class="hero-pixel-grid"></div>
451
1079
  <div class="hero-particles">${heroParticlesHtml}</div>
452
- <div class="hero-content">
453
- <img src="${getProxyPath()}assets/ui-icons/cyberia-white.png" alt="Cyberia Logo" class="logo-image" />
454
- <h1 style="margin: 10px">CYBERIA</h1>
455
- <p style="color: #fff; text-align: center; margin: 10px auto;">
456
- An action-packed Hack and Slash MMORPG. Explore a dynamic online sandbox pixel art universe, right from
457
- your browser.
458
- </p>
459
- <button class="cta-button">Enter The World</button>
460
- <button class="cta-button-select-instance">or select one</button>
1080
+ <div class="hero-layout">
1081
+ <div class="hero-content">
1082
+ <img src="${getProxyPath()}assets/ui-icons/cyberia-white.png" alt="Cyberia Logo" class="logo-image" />
1083
+ <h1>CYBERIA</h1>
1084
+ <p>
1085
+ An action-packed Hack and Slash MMORPG. Explore a dynamic online sandbox pixel art universe, right from
1086
+ your browser.
1087
+ </p>
1088
+ <div class="hero-kicker"><span class="hero-kicker-dot"></span> Pick a world · Jump in</div>
1089
+ <div class="hero-actions">
1090
+ <button class="cta-button" type="button"><i class="fa-solid fa-play"></i> Enter The World</button>
1091
+ <button class="cta-button-select-instance" type="button">
1092
+ <i class="fa-solid fa-globe"></i> Browse all worlds
1093
+ </button>
1094
+ </div>
1095
+ </div>
1096
+ <div class="hero-worlds">${worldSkeleton()}</div>
461
1097
  </div>
462
1098
  </section>
463
1099