117 Commits
Author SHA1 Message Date
miles f8b3d5c622 Update about page content and structure for clarity and detail 2026-04-23 20:36:11 -05:00
miles 4deab7e461 changing the LICENSE 2026-04-23 20:23:37 -05:00
miles 2fb319043b so code 2026-04-23 16:48:46 -05:00
miles 3b65631d5c Merge branch 'main' into code 2026-04-23 16:47:24 -05:00
miles 09f7af9413 Update v.txt 2026-04-23 16:46:31 -05:00
miles 898271dd7f Update version number from 1.26 to 2.26 2026-04-23 16:33:09 -05:00
miles 7c7f260138 Merge pull request #1 from mileswolfallen2/live
update
2026-04-23 16:31:25 -05:00
miles 2ece1d4bb8 update 2026-04-23 16:26:30 -05:00
miles 47c33b28a2 Now both login and signup check for Cloudflare Turnstile before allowing submission. The code checks window.turnstile.getResponse() - if it returns empty/falsy, the form submission is blocked with an error message. 2026-04-22 20:52:55 -05:00
miles a79c363cc6 tesing thing and added a robot.txt 2026-04-19 20:06:10 -05:00
miles 455a77b2c7 Update login and signup pages to include Cloudflare Turnstile for enhanced security 2026-04-18 02:16:34 -05:00
miles 0ed4d70680 added v.txt 2026-04-17 01:06:24 -05:00
miles 5f05b37887 Quick update 2026-04-17 00:38:10 -05:00
miles d7f0eb2c7b small chage 2026-04-17 00:17:55 -05:00
miles 3eeb4f7cb4 made the adman pane work agen 2026-04-16 23:41:12 -05:00
miles 52ff0fb0f5 Delete CNAME 2026-04-16 22:50:06 -05:00
miles 986cab3436 idk 2026-04-16 22:37:06 -05:00
miles dfb42e6393 Fix syntax error in server.js 2026-04-16 20:22:29 -05:00
miles 6ddb843e97 did a roll backe do to problems 2026-04-16 20:14:59 -05:00
miles 016d998263 Implement moderator management system with API endpoints for adding, removing, and checking moderators. Update server and documentation accordingly. 2026-04-14 19:26:16 -05:00
miles f27fcf7bf7 uodated the discored link to be perm 2026-04-13 18:17:01 -05:00
miles 07c69d1b30 Enhance home stats rendering by adding player ranking and recent runs information 2026-04-12 23:06:23 -05:00
miles 94a3dbda28 Refactor navigation links across multiple HTML files, adding "About" link and removing "Posts" link. Remove posts.json and related server functionality, indicating the removal of the posts feature. Add new "About" page detailing FEDL's mission, features, and community contributions. Update CSS for improved layout and animations. 2026-04-12 14:49:49 -05:00
miles cdb6e03560 Refactor code structure for improved readability and maintainability 2026-04-12 14:24:50 -05:00
miles c3c74863ef Add Google AdSense script to multiple HTML pages 2026-04-11 14:41:43 -05:00
miles 8581b01029 update 2026-04-11 14:35:36 -05:00
miles 29b8a4c132 Add support page with FAQs and resources 2026-04-11 14:34:22 -05:00
miles 85d73922c1 Updated discord link 2026-04-10 17:41:04 -05:00
miles 14004a21df Updated the web hooked logic to be better 2026-04-10 17:36:02 -05:00
miles 6bbaac2870 updated added webhooks to the server 2026-04-10 17:00:47 -05:00
miles 89f427e634 update 2026-04-10 00:08:23 -05:00
miles 14641723e2 hotfix 2026-04-10 00:03:48 -05:00
miles aee26eb540 Refactor code structure for improved readability and maintainability 2026-04-09 22:50:53 -05:00
miles f902c80dc6 Remove Cloudflare Workers types and related configurations; delete email worker implementation and add reset tokens JSON file. 2026-04-09 22:47:04 -05:00
miles 8bf6445cf6 updated bot 2026-04-09 22:18:13 -05:00
miles b53ab78028 testin email 2026-04-09 22:03:59 -05:00
miles 7b0fc6c7ad updated data.txt so you have more levels on the locel vershon 2026-04-09 20:56:12 -05:00
miles b87c3f8e94 update to new server 2026-04-09 16:55:12 -05:00
miles e2733d9d24 Refactor HTML files to use 'defer' attribute for script loading and update content for clarity and structure 2026-04-09 11:55:35 -05:00
miles 9db5e39a56 Update sitemap.html and sitemap.xml with new links and last modified dates 2026-04-08 00:01:37 -05:00
miles 22eeb8293c refine README.md for clarity and structure; update project details and backend features 2026-04-07 23:16:53 -05:00
miles f1ba614429 Create CNAME 2026-04-07 23:05:03 -05:00
miles 1b349062b4 Delete CNAME 2026-04-07 23:04:41 -05:00
miles 7395b78198 testing app 2026-04-05 23:25:50 -05:00
miles 7db049187a fixed messinging 2026-04-05 00:59:38 -05:00
miles eb1ca87277 test 2026-04-05 00:25:25 -05:00
miles 08a8a5bad1 updates 2026-04-05 00:02:50 -05:00
miles cf614feba6 hotfix 2026-04-04 23:10:28 -05:00
miles d11b47cb46 added the pontes to the levels 2026-04-04 20:53:07 -05:00
miles d16a6a3d68 added a page icon 2026-04-04 17:17:53 -05:00
miles b693c0205a uodated admen panel 2026-04-04 00:49:53 -05:00
miles 142fd5a6a0 added stuf 2026-04-03 23:53:33 -05:00
miles f5c26caeba hotfix for the post.html 2026-04-03 23:13:01 -05:00
miles 7086fbfbee added posting to the wedsite 2026-04-03 23:08:55 -05:00
miles 2e8070e442 fixed basic spelling problem 2026-04-03 14:52:51 -05:00
miles 477185ec17 updated text on runs.html 2026-04-03 00:30:49 -05:00
miles 426adc3043 Add player name input and default value handling; enhance loading shimmer effect 2026-04-03 00:28:14 -05:00
miles f012aa3dc4 Refactor navigation and add authentication features
- Updated navigation links across guess.html, index.html, lists.html, players.html, roulette.html, rules.html, and run.html for consistency and clarity.
- Enhanced index.html with a sign-up button for user registration.
- Modified lists.html to improve user guidance on video watching and added a "My %" column for tracking progress.
- Introduced login.html and signup.html for user authentication, including form validation and status messages.
- Implemented server-side user authentication in server.js, including user registration, login, and session management.
- Added user data handling for roulette progress and saved runs.
- Created a .gitignore file to exclude sensitive user data files from version control.
- Updated styles.css to accommodate new UI elements for authentication and improved layout.
2026-04-03 00:22:25 -05:00
miles aac09d2c8b Update app description and features for Gode Mod companion app 2026-04-02 23:23:38 -05:00
miles 453e6662e0 Add targeted import and bulk approval features with modals and API integration 2026-04-02 19:15:49 -05:00
miles 09fced106d fixed site map so we do not revel secrits 2026-04-02 18:17:54 -05:00
miles 751002d02a removed app from this project 2026-04-02 18:00:36 -05:00
miles 6bfe12cc42 Add React Router for navigation and update dependencies 2026-04-02 16:43:16 -05:00
miles 4ecf58dba3 put v1 test for app and made the app anansmet live 2026-04-02 16:31:56 -05:00
miles 3d6fccaf3c updated formating 2026-04-02 16:12:50 -05:00
miles 2d6c25dc83 Enhance SEO and accessibility by updating meta tags and titles in index and lists pages; add sitemap.xml and sitemap.html for better site navigation 2026-04-02 16:06:31 -05:00
miles 158c3f335c Remove old index page and add offline maintenance page with server status information 2026-04-01 20:09:18 -05:00
miles be4cc0bf1c testing stuf 2026-04-01 20:06:06 -05:00
miles da39fd4fd0 updated readme.md 2026-04-01 19:39:49 -05:00
miles b4c6c48958 Add Guess Game page and UI/JS support
Introduce a new Guess The Rank feature: add guess.html and associated styles and logic. app.js gains video modal utilities (extractYouTubeID, openVideoModal, renderApprovedRunsForLevel) and the full guessing-game flow (modes, round lifecycle, input handling). Refactor existing video-open usage to the new modal and enable showing approved runs in the modal. Update multiple navs and index highlights to link to the new Guess Game. Add CSS for guess UI and responsive tweaks, and adjust some UI copy (e.g. "View Video").
2026-04-01 16:47:22 -05:00
miles de621e50e9 Add percent column to run submissions table and update detail view 2026-04-01 00:13:32 -05:00
miles 52531abcc7 Add run percent and approved-runs modal
Introduce a percent field for runs and surface approved runs in the video modal. UI: add a numeric Percent input to the run submission form and show percent in submission lists and admin detail view (default 100%). Video modal: refactor openVideo to accept an item, add renderApprovedRunsForLevel to load and display approved runs for the same level with player, percent and video link, plus related modal HTML. Server: normalize and validate percent on run create/update (required), returning 400 when missing. Styles: add CSS for modal runs layout and responsive tweaks.
2026-04-01 00:11:13 -05:00
miles cdc65ec881 Update run submission instructions and remove raw footage link condition 2026-03-31 23:27:45 -05:00
miles 9d8906e94c Display approved runs count in hero
Replace the previous "last slot" display with an approved runs counter. Renamed the DOM variable to approvedRunsEl, added renderApprovedRuns to compute and render the number of runs whose status is "approved" (case-insensitive), and wired it to loadRuns() and onRunsUpdate(). Also updated the hero label in index.html from "Current last ranked slot" to "Approved runs". Handles missing elements and empty responses safely.
2026-03-31 22:55:42 -05:00
miles 06c6beb0b1 Add admin login overlay and gated UI
Introduce a client-side admin login screen that locks the admin page until the correct password is entered. admelist.html now contains a dedicated admin login card and wraps the actual admin shell content in a hidden section; the body receives an admin-locked class while locked. app.js adds form handling, verifyAdminPassword/unlockAdminShell logic, uses a DELETE /__authcheck__ request to validate the stored password, and shows/hides the admin UI accordingly; it also centralizes auth status messages. styles.css adds styles for the login overlay, locked state, and responsive tweaks. server/server.js removes the previous server-side requireAdmin guard for /admelist so the client can perform the auth check.
2026-03-31 22:47:43 -05:00
miles feb6dd2923 Add admin password UI and auth handling
Add an admin password field and session-backed auth handling for admin endpoints. admelist.html: introduce an admin password input and status text in the admin overview panel. app.js: store the password in sessionStorage (key: fedl_admin_password), populate the input on load, and persist changes; provide authHeaders() to inject a Basic Authorization header (fedl:password) into fetch requests; handle 401 responses by clearing the saved password and showing contextual error messages; wire password usage into save/update/delete run APIs. styles.css: add styling for the admin access panel and password field and adjust responsive rules. This enables browser-session admin authentication and clearer failure handling when admin credentials are missing or incorrect.
2026-03-31 22:39:16 -05:00
miles 7698b2c169 updated server 2026-03-31 22:29:24 -05:00
miles 11253b209e Always show live submissions status
Remove the canUseLiveServer conditional when setting the run list status so the message 'Live submissions are updating automatically.' is always shown. This simplifies the UI feedback and avoids showing a server-specific prompt while leaving error handling and rendering logic unchanged.
2026-03-31 21:43:28 -05:00
miles feb50ad389 make the thing work 2026-03-31 21:34:26 -05:00
miles c16f64aa08 remove data.txt from the git server 2026-03-31 21:26:59 -05:00
miles 90310fd671 Add run submission & admin moderation
Add a live run submission workflow and admin moderation UI.

- Add run.html: new page to submit runs (video, raw footage, notes) and show recent submissions.
- Update admelist.html: admin overview plus run queue table for reviewing/approving/rejecting/deleting submissions.
- Update app.js: client-side support for runs (load/refresh/caching, SSE "runs-update", admin review actions, run submission handling, and graceful fallback when no server is available).
- Server changes: add server/runs.json, implement /api/runs (GET, POST) and /api/runs/:id (PUT, DELETE) in server/server.js, ensure runs file, emit runs-update events, broaden CORS, set BASE path (/fedl) and default port 8090.
- Add styles for admin overview, run submission UI and submission cards in styles.css.
- Update README with run/admin pages and example URLs, adjust start-server.command to use PORT=8090, and minor note in LINUX_SERVICE_SETUP.txt.

Notes: live submission/review features require running the Node server; client falls back to static behavior when served from file:// or when server is unavailable.
2026-03-31 21:24:50 -05:00
miles 00baa8b204 Update players.html
made a small chage
2026-03-31 20:06:24 -05:00
miles 463fa222ff Redesign lists and roulette pages UI
Revamp the Lists and Roulette pages with new semantic panels and updated layout. lists.html: replace the simple list panel with a sidebar Quick Jump, a prominent hero section, dedicated controls (search + filter) and a separate table panel while preserving existing IDs (search, level-filter, list-area, list-title). roulette.html: reorganize into a roulette-shell with a main roulette panel (spin button, card) and a rules sidebar/card with concise rule items; interactive IDs (roulette-spin, roulette-card, roulette-open, etc.) remain intact. styles.css: add styles for the new components (.list-*, .control-block, .roulette-shell, .roulette-rules-card, .roulette-rule-list, .sidebar-kicker and more), tweak search-row spacing and responsive behaviors to support the new layout. No JS behavior changed in this diff (app.js left referenced but untouched).
2026-03-31 19:54:57 -05:00
miles 5acf920143 Add interactive homepage preview & styles
Replace the WIP homepage with a new interactive home-hero: updated copy, CTA labels, a stats area (total levels, #1 entry, last slot), a featured-list preview, spotlight and info panels, and a small Discord text tweak. Add CSS to style the new homepage components, featured cards, stat cards, spotlight layout and responsive rules for smaller screens. Update app.js to load and render list data on the index page: sort items, populate stats, render top 3 featured entries (with a fallback message), and hook into live updates via bindLiveUpdates/onLiveUpdate so the homepage stays current.
2026-03-31 19:48:11 -05:00
miles b39c334fdf Handle position edits on focusout
Stop performing item repositioning during the primary input handler and instead handle position changes on focusout. Non-position fields are updated live on input and mark the form as having unsaved changes. A new tbody focusout listener moves the item and re-renders the admin table when a position field loses focus, and also sets the unsaved status.
2026-03-31 19:16:46 -05:00
miles 96d2ca2d4a added back the admen pane 2026-03-31 19:07:02 -05:00
miles d29719131c Remove admin/server pages; consolidate live list
Delete admelist.html and serverlist.html and update the site to use lists.html as the main live-backed list. README.md was edited to reflect the consolidation (references to serverlist/admelist replaced with lists.html and roulette.html, wording adjusted). lists.html: remove admin/server links, change page title to “FEDL List”, and drop the level column in the table. app.js: update status text to mention the live server list, remove admin editor code and serverlist-specific branches, simplify list rendering (remove level column output) and wire live updates to listPage.applyItems. Miscellaneous: remove unused escapeAttr helper. Overall this simplifies the UI by consolidating live functionality into lists.html and removing the separate admin/server pages.
2026-03-31 19:00:35 -05:00
miles 906ef15e4b Use live server endpoints; add CORS/basePath
Point the client to a configurable live server (live API, events and data file) and accept both JSON and plain-text list formats. Update app.js to parse JSON or text responses, use live EventSource and PUT endpoints, and fall back to the live data file when needed.

On the server side, add BASE_PATH support (trim trailing slash) and normalize request paths so the app can be mounted under a subpath. Add setCorsHeaders and enable CORS (including handling OPTIONS preflight) for API, events and file responses, return proper headers on errors, and log the base path at startup. These changes enable cross-origin access and deployment under a base path while improving robustness of list parsing.
2026-03-31 18:47:49 -05:00
miles c239d8c062 Document live server-backed list and admin UI
Revise README to describe the new live server-backed list and admin editor. Adds/renames pages (lists.html = static, serverlist.html = live list, admelist.html = admin UI), clarifies shared data format for data.txt and server/data.txt, and documents the fallback order (/api/list → server/data.txt → data.txt). Includes instructions to run the Node server (server/server.js, HOST/PORT examples), a note on static serving for development, files added (server/server.js, server/data.txt, start-server.command, server/LINUX_SERVICE_SETUP.txt), and editor features (draft rows, numbering/shift behavior, delete flow, live refresh). Also clarifies where live vs static data is stored and that no build step is required.
2026-03-31 17:54:51 -05:00
miles 3d1005a72c made percoshons for the server list just in cas my mom umplugs my server 2026-03-31 17:51:45 -05:00
miles 63d4ec0009 added a server so you do not need to update a text file mauly
yaaaaa
2026-03-31 17:48:50 -05:00
Lux, Righ? 46a95d3a19 added 202-250 2026-03-31 13:02:56 -04:00
Lux, Righ? fddc92ae72 added 136-201 2026-03-31 11:35:50 -04:00
miles 98668db533 put a comming soon to the mane page 2026-03-30 23:48:41 -05:00
miles b1a6873db3 Remove 'Errors' button from navigation
Removed the 'Errors' button from the navigation.
2026-03-30 23:37:30 -05:00
miles cc68d81440 Remove 'Errors' link from navigation
Removed the link to the 'Errors' page from the navigation.
2026-03-30 23:23:36 -05:00
Lux, Righ? e79d3b0a0d Added 51-135 2026-03-31 00:06:06 -04:00
miles 4c2f6ed5a0 added 404 etc 2026-03-30 23:01:59 -05:00
Lux, Righ? df238b1448 Fixed combustion 2026-03-30 23:21:08 -04:00
miles 10f3639c75 made gd rulet better 2026-03-30 19:35:31 -05:00
miles 93a3f497c0 added demon rulet v1 not good 2026-03-30 19:04:30 -05:00
miles fe65e66d16 fixed discord coz i suck 2026-03-30 02:51:50 -05:00
miles 60aa221e47 added discord 2026-03-30 02:50:31 -05:00
miles fdc700c558 Create CNAME 2026-03-30 01:00:21 -05:00
miles 349756891e Delete CNAME 2026-03-30 01:00:17 -05:00
miles 8009102cd4 updated redme 2026-03-30 00:54:43 -05:00
miles 23a3235afc uptaded the websit
see https://raspberrypi-1.tail46eacb.ts.net/git/miles/gd-list/commit/f28abb272d9e837c2c462c0552c57b9d40f046a7 for more info
2026-03-30 00:42:41 -05:00
miles 55b8c1a7d4 Create CNAME 2026-03-30 01:41:18 +00:00
miles e5691e95cf Delete CNAME 2026-03-30 01:41:06 +00:00
miles edf2fae76c Create CNAME 2026-03-30 01:35:02 +00:00
miles 52d8d60760 Delete CNAME 2026-03-30 01:34:50 +00:00
miles d5b6ba26dc Update CNAME 2026-03-30 01:17:50 +00:00
miles f1f040aaa0 Create CNAME 2026-03-30 01:17:28 +00:00
Lux, Righ? 8940f7cec8 Fixed spelling of The Hallucination 2026-03-29 20:49:29 -04:00
miles 58da99b91f updated spelling 2026-03-29 23:49:55 +00:00
miles 34d34fc890 updated the name of the list so it is feel 2026-03-29 17:26:10 -05:00
miles b3d92b5f6e put it on live 2026-03-29 17:22:14 -05:00
49 changed files with 9634 additions and 1150 deletions
Vendored
BIN
View File
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
# Server-side auth and user data (local only)
server/users.json
server/sessions.json
server/userdata.json
server/data.txt
server/runs.json
server/messages.json
# Environment variables
server/.env
server/config.json
+3
View File
@@ -1,10 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>401 - Unauthorized</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body class="error-shell">
<header>
+3
View File
@@ -1,10 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>403 - Forbidden</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body class="error-shell">
<header>
+3
View File
@@ -1,10 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>404 - Not Found</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body class="error-shell">
<header>
+3
View File
@@ -1,10 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>429 - Too Many Requests</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body class="error-shell">
<header>
+3
View File
@@ -1,10 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>500 - Internal Server Error</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body class="error-shell">
<header>
+3
View File
@@ -1,10 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>502 - Bad Gateway</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body class="error-shell">
<header>
+3
View File
@@ -1,10 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>503 - Service Unavailable</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body class="error-shell">
<header>
-18
View File
@@ -1,18 +0,0 @@
MIT License
Copyright (c) 2026 miles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
+116 -46
View File
@@ -1,37 +1,45 @@
# GD FEDL
A lightweight Geometry Dash FEDL-style website with a built-in Node.js server and browser-based list manager.
GD FEDL is a Geometry Dash list site built as a multi-page static frontend with a lightweight Node.js backend. It supports live list data, run submissions, admin moderation, account login, synced user state, community posts, bug reports, and direct messages.
It includes:
## What It Includes
- a home page
- a list page for ranked level videos
- a manage page for editing the list in the browser
- a players page
- a rules page
- a Node.js server with a JSON API
- `index.html` landing page
- `lists.html` ranked list browser with search, filters, and video playback
- `run.html` run submission page
- `admelist.html` admin page for list editing and run moderation
- `players.html` player progress tracker
- `guess.html` rank guessing game
- `roulette.html` demon roulette picker
- `rules.html` rules page
- `signup.html` and `login.html` account pages
- `post.html` and `messages.html` community features
- `contact.html` bug report / contact page
- `errors.html` plus standalone HTTP-style error pages
## Project Files
## Tech Stack
- `index.html` - home page
- `lists.html` - main list view
- `manage.html` - browser UI for adding, editing, and deleting list entries
- `players.html` - player manager
- `rules.html` - submission rules and mod guidelines
- `app.js` - client-side logic for the list and players pages
- `styles.css` - site styling
- `server.js` - Node.js server and API
- `data.json` - primary list storage generated by the server
- `data.txt` - compatibility export rewritten by the server
- `package.json` - start script for the Node server
- Plain HTML, CSS, and JavaScript
- No build step for the main site
- Node.js `http` server
- Server-Sent Events for live updates
- Browser `localStorage` / `sessionStorage`
- JSON and text files for simple local persistence
## How The List Data Works
## Project Structure
The site now serves list data through `GET /api/levels`.
- `app.js` shared frontend logic
- `styles.css` shared site styling
- `data.txt` static fallback list data
- `level-ids.txt` optional local level ID lookup data for roulette
- `server/server.js` backend for list data, auth, posts, messages, bug reports, and run submissions
- `server/README.md` backend API details
- `server/LINUX_SERVICE_SETUP.txt` example `systemd` setup
- `start-server.command` macOS launcher for the Node server
The server stores entries in `data.json` and also keeps `data.txt` updated automatically for compatibility.
## Data Files
Legacy `data.txt` lines still use this format:
The main list format is one entry per line:
```txt
category|position|title|url
@@ -43,45 +51,107 @@ Example:
new|1|Flamewall|https://youtu.be/x4Io4zkWVRw
```
## Running The Site
The server also stores runtime data in `server/`:
Start the Node server:
- `data.txt` live list source
- `runs.json` submitted runs
- `users.json` registered accounts
- `sessions.json` login sessions
- `userdata.json` synced user state
- `posts.json` community posts
- `bugreports.json` bug reports
- `messages.json` private messages
Most JSON files are created automatically when first needed. `server/data.txt` is the one file you should create yourself before first run.
## Main Backend Features
- `GET /api/list` and `PUT /api/list` for live list data
- `GET /api/runs`, `POST /api/runs`, `PUT /api/runs/:id`, `DELETE /api/runs/:id`
- `POST /api/runs/bulk-approve` for admin moderation
- `POST /api/auth/signup`, `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/me`
- `GET /api/user/state`, `PUT /api/user/state` for synced client state
- `GET /api/posts`, `POST /api/posts`, like/comment routes, and post deletion
- `GET /api/bugreports`, `POST /api/bugreports`, admin update/delete routes
- `GET /api/messages`, `POST /api/messages`, conversation lookup, and user search
- `GET /events` for SSE updates
- Pointercrate and AREDL import routes for admins
See [server/README.md](/Users/miles/Documents/GitHub/fedl/server/README.md) for the fuller API reference.
## Local Setup
1. Make sure Node.js is installed.
2. Create the live list data file if it does not exist yet:
```bash
npm start
printf "new|1|Example Level|https://youtu.be/example\n" > server/data.txt
```
Then open:
3. Start the server:
```bash
node server/server.js
```
4. Open the site at:
```txt
http://localhost:8000
http://127.0.0.1:8090/fedl/
```
## Editing The List
To expose it on your local network:
Open:
```bash
HOST=0.0.0.0 PORT=8090 node server/server.js
```
On macOS you can also run:
```txt
http://localhost:8000/manage.html
./start-server.command
```
From there you can:
## Environment Variables
- add entries
- edit existing entries
- delete entries
- search the current list
- `HOST` server bind host, default `127.0.0.1`
- `PORT` server port, default `8090`
- `ADMIN_PASSWORD` admin password for protected write routes
- `AREDL_ACCESS_TOKEN` bearer token for AREDL imports
- `AREDL_API_KEY` API key for AREDL imports
The server also exposes these API routes:
Example:
- `GET /api/levels`
- `POST /api/levels`
- `PUT /api/levels/:id`
- `DELETE /api/levels/:id`
```bash
ADMIN_PASSWORD=changeme HOST=0.0.0.0 PORT=8090 node server/server.js
```
## Frontend Live Server Note
`app.js` uses a hardcoded live server base:
```txt
https://raspberrypi-1.tail46eacb.ts.net/fedl
```
That means:
- the frontend will try the remote live server first
- if the live server probe fails, the app redirects to `offlineindex.html`
- if you want local development against your own server, update `TESTING_MODE` or `liveServerBase` in `app.js`
## Notes
- On first run, the server will migrate the current `data.txt` into `data.json`
- Player data is stored in the browser with `localStorage`
- The players list is local to each browser/device
- No external npm packages are required
- `players.html` stores local player progress in the browser, with optional server sync when signed in
- admin-protected routes use HTTP Basic auth
- account auth uses bearer tokens from the backend
- `roulette.html` can use `level-ids.txt` before falling back to external lookups
- the backend serves the repo root as static files under the `/fedl` base path
## Linux Service
For `systemd` setup notes, see [server/LINUX_SERVICE_SETUP.txt](/Users/miles/Documents/GitHub/fedl/server/LINUX_SERVICE_SETUP.txt).
## License
See [LICENSE](/Users/miles/Documents/GitHub/fedl/LICENSE).
+102
View File
@@ -0,0 +1,102 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="About FEDL - Fair Extreme Demon List, your hub for the Geometry Dash demon list">
<meta name="keywords" content="demon list, about, geometry dash, fedl, ranked demons">
<title>About — GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="about">
<header>
<h1>About</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main class="rules-page">
<section class="rule-section">
<h2>What is FEDL?</h2>
<p><strong>FEDL</strong> stands for <strong>Fair Extreme Demon List</strong>. We are a community-driven Geometry Dash demon list focused on providing the most up-to-date, fair, and least-restrictive rankings in the GD demon community.</p>
<p>Our goal is to make demon verification accessible to everyone while maintaining accurate and current rankings that reflect the ever-evolving state of the game.</p>
</section>
<section class="rule-section">
<h2>How It Works</h2>
<p>Players submit their demon completions through our run submission system. Each submission is reviewed by our moderation team to verify the run meets our guidelines. Once approved, the run is linked to the corresponding demon on our list.</p>
<p>The list is updated in real-time as new demons are verified, ensuring players always have access to the most current rankings.</p>
</section>
<section class="rule-section">
<h2>Features</h2>
<ul class="rules-list">
<li><strong>Browse the List</strong> - Search and filter through all ranked demons by title, level ID, or rank position. Jump between rank ranges for quick navigation.</li>
<li><strong>Player Rankings</strong> - Track the top players based on their verified runs and total points earned.</li>
<li><strong>Submit Runs</strong> - Submit your own completions with video links and notes for review by our moderators.</li>
<li><strong>Roulette</strong> - Feeling adventurous? Spin the roulette to get a random demon from the list as your next challenge.</li>
<li><strong>Guess Game</strong> - Test your demon knowledge by guessing if a level is ranked higher or lower than shown.</li>
<li><strong>Messages</strong> - Receive updates on your submissions and communicate with the team.</li>
<li><strong>Themes</strong> - Customize your experience with 14 different color themes including Light, Dark, Ocean, Cyberpunk, Fire, Galaxy, and more.</li>
</ul>
</section>
<section class="rule-section">
<h2>Moderation</h2>
<p>Our moderation team reviews all submissions to ensure fairness and accuracy. We verify completion videos, check for compliance with our rules, and provide feedback when runs are rejected.</p>
<p>Moderators include <strong>wolf_reaper90</strong>, <strong>dioxyx</strong>, and <strong>steve</strong>. Contact us through the <a href="contact.html" style="color:var(--accent)">Contact page</a> for any moderation-related questions.</p>
</section>
<section class="rule-section">
<h2>Keyboard Shortcuts</h2>
<ul class="rules-list">
<li><strong>/</strong> - Focus the search bar</li>
<li><strong>H</strong> - Go to Home</li>
<li><strong>L</strong> - Go to Lists</li>
<li><strong>R</strong> - Go to Run submission</li>
<li><strong>?</strong> - Go to Rules</li>
<li><strong>Shift+W / Shift+E</strong> - Go to Admin Panel (mods only)</li>
<li><strong>Esc</strong> - Close modals</li>
</ul>
</section>
<section class="rule-section">
<h2>Community</h2>
<p>Join our Discord server to connect with other demon hunters, ask questions about rules, discuss level placements, share your progress, and get support from our team.</p>
<p><a href="https://discord.gg/5HyUKQrN" target="_blank" rel="noopener noreferrer" class="btn" style="display:inline-block;margin-top:12px">Join Our Discord</a></p>
</section>
<section class="rule-section">
<h2>Theme Options</h2>
<p>FEDL includes 14 different themes to customize your viewing experience. Visit the <a href="account.html" style="color:var(--accent)">Account page</a> to choose from themes like Default, Light, Ocean, Cyberpunk, Matrix, Fire, Galaxy, and more!</p>
</section>
<section class="rule-section">
<h2>Coming Soon</h2>
<ul class="rules-list">
<li><strong>Geode Mod</strong> - A dedicated FEDL companion mod for Geometry Dash with quick list access, roulette tools, queue tracking, and built-in validation help.</li>
<li><strong>Mobile App</strong> - A dedicated app for FEDL is on the way. Stay tuned for updates.</li>
<li><strong>OAuth Sign-in</strong> - Sign in with Google, Discord, or other platforms for easier access.</li>
</ul>
</section>
<section class="rule-section">
<h2>Credits</h2>
<p>FEDL is built and maintained by the community. Thanks to all the moderators who review submissions, the players who submit runs, and everyone who helps keep the demon list up to date.</p>
<p class="muted">Version 1.0 | Last updated April 2026</p>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
+158
View File
@@ -0,0 +1,158 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Account — GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="account">
<header>
<h1>Account</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main class="account-main">
<section class="panel account-hero">
<p class="hero-kicker">FEDL account</p>
<h2>Manage your account</h2>
<p class="muted">Change your password and view your account details.</p>
<p id="account-overview-status" class="muted" role="status"></p>
<div class="account-summary-grid">
<article class="account-summary-card">
<span class="account-summary-label">Username</span>
<strong id="account-username">Loading…</strong>
</article>
<article class="account-summary-card">
<span class="account-summary-label">Joined</span>
<strong id="account-created">Loading…</strong>
</article>
</div>
</section>
<section class="panel account-panel">
<h3>Theme</h3>
<p class="muted">Choose your preferred color scheme.</p>
<div class="theme-options">
<label class="theme-option">
<input type="radio" name="theme" value="dark" checked>
<span class="theme-preview theme-preview-dark"></span>
<span>Default</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="light">
<span class="theme-preview theme-preview-light"></span>
<span>Light</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="blue">
<span class="theme-preview theme-preview-blue"></span>
<span>Ocean</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="midnight">
<span class="theme-preview theme-preview-midnight"></span>
<span>Midnight</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="cyberpunk">
<span class="theme-preview theme-preview-cyberpunk"></span>
<span>Cyberpunk</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="earth">
<span class="theme-preview theme-preview-earth"></span>
<span>Earth</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="retro">
<span class="theme-preview theme-preview-retro"></span>
<span>Retro</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="matrix">
<span class="theme-preview theme-preview-matrix"></span>
<span>Matrix</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="synthwave">
<span class="theme-preview theme-preview-synthwave"></span>
<span>Synthwave</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="fire">
<span class="theme-preview theme-preview-fire"></span>
<span>Fire</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="galaxy">
<span class="theme-preview theme-preview-galaxy"></span>
<span>Galaxy</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="candy">
<span class="theme-preview theme-preview-candy"></span>
<span>Candy</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="highcontrast">
<span class="theme-preview theme-preview-highcontrast"></span>
<span>High Contrast</span>
</label>
<label class="theme-option">
<input type="radio" name="theme" value="original">
<span class="theme-preview theme-preview-original"></span>
<span>Original</span>
</label>
</div>
<p id="account-theme-status" class="muted" role="status"></p>
</section>
<section class="panel account-panel">
<h3>Password reset</h3>
<p class="muted">Forgot your password? Request a reset code which will be sent to your messages.</p>
<p id="account-reset-status" class="muted" role="status"></p>
<div class="auth-actions">
<button type="button" class="btn" id="account-reset-email-btn">Send reset to my messages</button>
<a class="text-link" href="reset-password.html">Open reset page</a>
</div>
</section>
<section class="panel account-panel">
<h3>Change password</h3>
<p class="muted">Use this if you already know your current password and just want to swap it.</p>
<form id="account-password-form" class="auth-form">
<label class="admin-password-field" for="account-current-password">
<span>Current password</span>
<input id="account-current-password" name="currentPassword" type="password" autocomplete="current-password" required />
</label>
<label class="admin-password-field" for="account-new-password">
<span>New password</span>
<input id="account-new-password" name="newPassword" type="password" autocomplete="new-password" required minlength="8" />
</label>
<label class="admin-password-field" for="account-confirm-password">
<span>Confirm new password yaa</span>
<input id="account-confirm-password" name="confirmPassword" type="password" autocomplete="new-password" required minlength="8" />
</label>
<p id="account-password-status" class="muted" role="status"></p>
<div class="auth-actions">
<button type="submit" class="btn" id="account-password-submit">Update password</button>
</div>
</form>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
+247
View File
@@ -0,0 +1,247 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Edit List - GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="admelist" class="admin-locked">
<header id="admin-page-header">
<h1>FEDL Admin</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main class="admin-shell">
<section id="admin-login-screen" class="admin-login-screen">
<div class="admin-login-card">
<p class="hero-kicker">Admin Access</p>
<h2>Enter the admin password</h2>
<p class="muted">Submit the server password to unlock the list editor and run moderation tools.</p>
<form id="admin-login-form" class="admin-login-form">
<label class="admin-password-field" for="admin-password">
<span>Admin password</span>
<input id="admin-password" type="password" placeholder="Enter server password" autocomplete="current-password" />
</label>
<div class="admin-login-actions">
<button id="admin-login-submit" type="submit" class="btn">Submit</button>
<p id="admin-auth-status" class="muted">Saved only in this browser session.</p>
</div>
</form>
</div>
</section>
<section id="admin-shell-content" class="admin-shell-content" hidden>
<section class="admin-tabs">
<button type="button" class="admin-tab active" data-tab="stats">Stats</button>
<button type="button" class="admin-tab" data-tab="list">List Editor</button>
<button type="button" class="admin-tab" data-tab="runs">Run Queue</button>
<button type="button" class="admin-tab" data-tab="mods">Mods</button>
<button type="button" class="admin-tab" data-tab="bugs">Bug Reports</button>
<button type="button" class="admin-tab" data-tab="import">Import</button>
</section>
<section class="panel admin-panel admin-tab-panel" id="tab-stats">
<div class="admin-toolbar">
<div>
<p class="hero-kicker">Statistics</p>
<h2>Overview</h2>
<p id="admin-stats-status" class="muted">Loading stats...</p>
</div>
</div>
<div class="stats-grid">
<div class="stat-box">
<strong id="stat-total-runs">--</strong>
<span>Total Runs</span>
</div>
<div class="stat-box">
<strong id="stat-pending-runs">--</strong>
<span>Pending</span>
</div>
<div class="stat-box">
<strong id="stat-levels-count">--</strong>
<span>Levels</span>
</div>
<div class="stat-box">
<strong id="stat-bug-reports">--</strong>
<span>Open Reports</span>
</div>
</div>
</section>
<section class="panel admin-panel admin-tab-panel" id="tab-list" hidden>
<div class="admin-toolbar">
<div>
<p class="hero-kicker">List Editor</p>
<h2>Manage the live list</h2>
<p id="admin-status" class="muted">Loading list data...</p>
</div>
<div class="admin-actions">
<input id="admin-search" type="text" placeholder="Search rows..." />
<button id="add-row" type="button" class="btn ghost-btn">Add Row</button>
<button id="save-list" type="button" class="btn">Save List</button>
</div>
</div>
<div class="table-wrap">
<table class="levels-table admin-table">
<thead>
<tr>
<th style="width:90px">#</th>
<th style="width:140px">Level</th>
<th>Title</th>
<th>Video URL</th>
<th style="width:120px">Delete</th>
</tr>
</thead>
<tbody id="admin-list-body"></tbody>
</table>
</div>
<div style="padding:16px">
<button id="add-row-bottom" type="button" class="btn ghost-btn">Add Row</button>
</div>
</section>
<section class="panel admin-panel admin-tab-panel" id="tab-runs" hidden>
<div class="admin-toolbar">
<div>
<p class="hero-kicker">Run Queue</p>
<h2>Review submitted runs</h2>
<p id="runs-admin-status" class="muted">Loading run submissions...</p>
</div>
<div class="admin-actions">
<input id="run-search" type="text" placeholder="Search submissions..." />
</div>
</div>
<div class="table-wrap">
<table class="levels-table admin-table">
<thead>
<tr>
<th style="width:130px">Status</th>
<th style="width:160px">Player</th>
<th style="width:110px">Percent</th>
<th>Level</th>
<th style="width:180px">Submitted</th>
<th style="width:240px">Actions</th>
</tr>
</thead>
<tbody id="run-admin-body"></tbody>
</table>
</div>
</section>
<section class="panel admin-panel admin-tab-panel" id="tab-bugs" hidden>
<div class="admin-toolbar">
<div>
<p class="hero-kicker">Reports</p>
<h2>View and manage bug reports</h2>
<p id="bug-reports-admin-status" class="muted">Loading bug reports...</p>
</div>
<div class="admin-actions">
<input id="bug-report-search" type="text" placeholder="Search reports..." />
</div>
</div>
<div class="table-wrap">
<table class="levels-table admin-table">
<thead>
<tr>
<th style="width:100px">Category</th>
<th style="width:100px">Status</th>
<th style="width:160px">Submitted By</th>
<th>Subject</th>
<th style="width:180px">Submitted</th>
<th style="width:180px">Actions</th>
</tr>
</thead>
<tbody id="bug-reports-body"></tbody>
</table>
</div>
</section>
<section class="panel admin-panel admin-tab-panel" id="tab-import" hidden>
<div class="admin-toolbar">
<div>
<p class="hero-kicker">Import tool</p>
<h2>External lists & bulk approve</h2>
<p id="import-status" class="muted">Pull records from Pointercrate or AREDL (AREDL needs a server API key or token). Targeted import loads victories from the API by player or level, adds them as approved runs with notes set to "Valid run". Bulk approve approves pending FEDL submissions only. Full imports run one page at a time to limit rate usage.</p>
</div>
<div class="admin-actions admin-import-actions">
<button id="import-pointercrate" type="button" class="btn ghost-btn">Import Pointercrate</button>
<button id="import-aredl" type="button" class="btn ghost-btn">Import AREDL</button>
<button id="import-targeted-open" type="button" class="btn ghost-btn">Import by player or level…</button>
<button id="bulk-approve-open" type="button" class="btn ghost-btn">Bulk-approve by player…</button>
</div>
</div>
</section>
<div id="import-targeted-modal" class="video-modal bulk-approve-modal" hidden>
<div class="inner bulk-approve-modal-card" role="dialog" aria-modal="true" aria-labelledby="import-targeted-title">
<p class="hero-kicker">Import tool</p>
<h2 id="import-targeted-title">Import victories from API</h2>
<p class="muted">Choose a list, match by player or level, then import. New runs are <strong>approved</strong> with submission and review notes <strong>Valid run</strong>. Duplicates (same video URL) are skipped.</p>
<form id="import-targeted-form" class="admin-login-form bulk-approve-form">
<label class="admin-password-field" for="import-targeted-source">
<span>List API</span>
<select id="import-targeted-source" required>
<option value="pointercrate">Pointercrate</option>
<option value="aredl">AREDL</option>
</select>
</label>
<div class="import-targeted-mode">
<span class="import-targeted-mode-label">Match</span>
<label class="import-targeted-radio"><input type="radio" name="import-targeted-filter" value="player" checked /> Player</label>
<label class="import-targeted-radio"><input type="radio" name="import-targeted-filter" value="level" /> Level</label>
</div>
<label class="admin-password-field" for="import-targeted-query">
<span id="import-targeted-query-label">Player name</span>
<input id="import-targeted-query" type="text" placeholder="Name as on the list" autocomplete="off" required />
</label>
<p class="muted small import-targeted-hint">Pointercrate: use the <strong>exact</strong> player or demon name from the list, or a numeric id (player id / demon id). The old API filters were unreliable; names are resolved correctly now. AREDL prefers an exact name match, then substring, across scanned pages.</p>
<div class="admin-login-actions bulk-approve-actions">
<button type="submit" class="btn" id="import-targeted-submit">Import as valid runs</button>
<button type="button" class="btn ghost-btn" id="import-targeted-cancel">Cancel</button>
</div>
</form>
</div>
</div>
<div id="bulk-approve-modal" class="video-modal bulk-approve-modal" hidden>
<div class="inner bulk-approve-modal-card" role="dialog" aria-modal="true" aria-labelledby="bulk-approve-title">
<p class="hero-kicker">Import tool</p>
<h2 id="bulk-approve-title">Bulk-approve by player</h2>
<p class="muted">Only submissions still marked <strong>pending</strong> are updated. Approved and rejected runs are left unchanged.</p>
<form id="bulk-approve-form" class="admin-login-form bulk-approve-form">
<label class="admin-password-field" for="bulk-approve-player">
<span>Player name</span>
<input id="bulk-approve-player" type="text" placeholder="Exact name from the run queue" autocomplete="off" required />
</label>
<p id="bulk-approve-preview" class="muted" aria-live="polite"></p>
<label class="admin-password-field" for="bulk-approve-notes">
<span>Review notes (optional)</span>
<input id="bulk-approve-notes" type="text" placeholder="Shown on each approved run" autocomplete="off" />
</label>
<div class="admin-login-actions bulk-approve-actions">
<button type="submit" class="btn" id="bulk-approve-submit">Approve all pending</button>
<button type="button" class="btn ghost-btn" id="bulk-approve-cancel">Cancel</button>
</div>
</form>
</div>
</div>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
+3469 -265
View File
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Contact Us - GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="contact">
<header>
<h1>Contact Us</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main>
<section class="run-shell">
<section class="panel run-panel">
<div class="run-head">
<div>
<p class="hero-kicker">FEDL Contact</p>
<h2>Report a bug or send feedback</h2>
<p class="muted run-intro">Found a bug? Have a suggestion? Fill out the form below and the admins will review it. If you <a href="login.html">log in</a>, your username will be attached to the report.</p>
</div>
</div>
<form id="contact-form" class="run-form">
<label class="control-block" for="contact-category">
<span>Category</span>
<select id="contact-category" name="category" required>
<option value="bug">Bug Report</option>
<option value="suggestion">Suggestion</option>
<option value="question">Question</option>
<option value="other">Other</option>
</select>
</label>
<label class="control-block" for="contact-subject">
<span>Subject</span>
<input id="contact-subject" name="subject" type="text" placeholder="Brief summary of the issue" required />
</label>
<label class="control-block" for="contact-description">
<span>Description</span>
<textarea id="contact-description" name="description" rows="8" placeholder="Describe the bug, include steps to reproduce if applicable, and any screenshots or links that might help." required></textarea>
</label>
<label class="control-block" for="contact-email">
<span>Email (optional)</span>
<input id="contact-email" name="email" type="email" placeholder="Your email if you'd like a response" />
</label>
<div class="run-actions">
<button id="contact-submit" type="submit" class="btn">Submit Report</button>
<p id="contact-form-status" class="muted">The server needs to be running for submissions.</p>
</div>
</form>
</section>
<aside class="panel run-side-panel">
<p class="status-label">What to report</p>
<h2>Before you submit</h2>
<div class="run-checklist">
<article>
<strong>Bug details</strong>
<p>Include what you were doing when the bug occurred and what you expected to happen.</p>
</article>
<article>
<strong>Steps to reproduce</strong>
<p>List the steps needed to trigger the bug so we can try to replicate it.</p>
</article>
<article>
<strong>Screenshots or videos</strong>
<p>Links to screenshots or videos can help us understand the issue better.</p>
</article>
</div>
</aside>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
-352
View File
@@ -1,352 +0,0 @@
[
{
"id": "legacy-1",
"level": "new",
"position": "1",
"title": "Flamewall",
"url": "https://youtu.be/x4Io4zkWVRw?si=tf0DBaWahOPPEWxO"
},
{
"id": "legacy-2",
"level": "new",
"position": "2",
"title": "Thinking Space II",
"url": "https://youtu.be/CELNmHwln_c?si=soP6SzRn92G1vKbl"
},
{
"id": "legacy-3",
"level": "new",
"position": "4",
"title": "Amethyst",
"url": "https://youtu.be/4lfkzz1VCbA?si=6NNeWiH-nnwu21Cb"
},
{
"id": "legacy-4",
"level": "new",
"position": "3",
"title": "Tidal Wave",
"url": "https://youtu.be/9fsZ014qB3s?si=oj4E4ceyNtRVcDvd"
},
{
"id": "legacy-5",
"level": "new",
"position": "5",
"title": "Orbit",
"url": "https://youtu.be/QKcv8DkNPd0?si=cKBFqp7GRqmfupgJ"
},
{
"id": "legacy-6",
"level": "new",
"position": "6",
"title": "BOOBAWAMBA",
"url": "https://youtu.be/20fYiqLAo_E?si=QSSixz31rWeeQ_8B"
},
{
"id": "legacy-7",
"level": "new",
"position": "7",
"title": "Nullscapes",
"url": "https://youtu.be/EztneTPp5CU?si=ljn-S_bnGYwOoUyV"
},
{
"id": "legacy-8",
"level": "new",
"position": "8",
"title": "Quantuese Processing",
"url": "https://youtu.be/j5NC0u1Q91Q?si=FvUEY2kdDtNMqWSD"
},
{
"id": "legacy-9",
"level": "new",
"position": "9",
"title": "Subsuming Vortex",
"url": "https://youtu.be/0eYG1ogJpIQ?si=7XRxfDsmMVb0G9Si"
},
{
"id": "legacy-10",
"level": "new",
"position": "10",
"title": "Andromeda",
"url": "https://youtu.be/mk3TDemdkC0?si=va13_RKAJUyFvhcv"
},
{
"id": "legacy-11",
"level": "new",
"position": "11",
"title": "Every End",
"url": "https://youtu.be/AO--mVVFtKI?si=gIT5vxyc1FZ4_8Jj"
},
{
"id": "legacy-12",
"level": "new",
"position": "12",
"title": "Silent Clubstep",
"url": "https://youtu.be/GR4OMkS3SN8?si=CMNz8bPBaUU94wiy"
},
{
"id": "legacy-13",
"level": "new",
"position": "13",
"title": "Anathema",
"url": "https://youtu.be/_uKwmjHmySI?si=PgXwX7Z--wCay34b"
},
{
"id": "legacy-14",
"level": "new",
"position": "14",
"title": "Acheron",
"url": "https://youtu.be/sBKR6aUorzA?si=0A9S2_W8gi1awZ81"
},
{
"id": "legacy-15",
"level": "new",
"position": "15",
"title": "Ashley Wave Trials",
"url": "https://youtu.be/aTxt76U3e2Q?si=RVcEcOTOfs7NGWRi"
},
{
"id": "legacy-16",
"level": "new",
"position": "16",
"title": "Avernus",
"url": "https://youtu.be/16Zh8jssanc?si=0o6d2dH4p_J7UjKB"
},
{
"id": "legacy-17",
"level": "new",
"position": "17",
"title": "Menace",
"url": "https://youtu.be/nnkgghxxsEE?si=OuUQfD7WbXVooYyX"
},
{
"id": "legacy-18",
"level": "new",
"position": "18",
"title": "Spectre",
"url": "https://youtu.be/MzsSLKJrLSI?si=-bXtr_jG96kTCzy4"
},
{
"id": "legacy-19",
"level": "new",
"position": "19",
"title": "Abyss Of Darkness",
"url": "https://youtu.be/ejJkpqcMMCY?si=Rpp4HEi1kOMh-p0h"
},
{
"id": "legacy-20",
"level": "new",
"position": "20",
"title": "Defeated Circles",
"url": "https://youtu.be/nU5AQPzd2YA?si=6eKDMPh-2QjDBBOF"
},
{
"id": "legacy-21",
"level": "new",
"position": "21",
"title": "Tunnel Of Despair",
"url": "https://youtu.be/LpS4JREhW98?si=Z6_DRV5YZTEA0TeQ"
},
{
"id": "legacy-22",
"level": "new",
"position": "22",
"title": "Subterminal Point",
"url": "https://youtu.be/h2wmRMgACH4?si=nDD0-bmLqPgcdVJP"
},
{
"id": "legacy-23",
"level": "new",
"position": "23",
"title": "KOCMOC",
"url": "https://youtu.be/2CxE-UWCIG4?si=9xz4hBjtNKe8Rcbs"
},
{
"id": "legacy-24",
"level": "new",
"position": "24",
"title": "Slaughterhouse",
"url": "https://youtu.be/kpcF1-QAHQc?si=7ybF9EbcjqHzloLM"
},
{
"id": "legacy-25",
"level": "new",
"position": "25",
"title": "Kyouki",
"url": "https://youtu.be/KDa5c0CJTHs?si=WU0sPusFLB7Jtkam"
},
{
"id": "legacy-26",
"level": "new",
"position": "26",
"title": "The Lightning Rod",
"url": "https://youtu.be/nQDTi077O6M?si=Kqp2Od3lJx1YWTDB"
},
{
"id": "legacy-27",
"level": "new",
"position": "27",
"title": "Based After Based",
"url": "https://youtu.be/yQBFyUvB3lY?si=Cmxbc5jrTrNlcmG5"
},
{
"id": "legacy-28",
"level": "new",
"position": "28",
"title": "CHIL",
"url": "https://youtu.be/DROMiCc2ZRM?si=_bR8CCnqJGVCex9F"
},
{
"id": "legacy-29",
"level": "new",
"position": "29",
"title": "Sakupen Circles",
"url": "https://youtu.be/ofG2mJi9kEA?si=3yv78_jxidKPhtL5"
},
{
"id": "legacy-30",
"level": "new",
"position": "30",
"title": "Deimos (ItsHybrid)",
"url": "https://youtu.be/b2yHaIk5zio?si=AMIuVFR-EW4CK8_P"
},
{
"id": "legacy-31",
"level": "new",
"position": "31",
"title": "Eyes In The Water",
"url": "https://youtu.be/yvLwiOy3KEA?si=1991xs25WTEoTbE0"
},
{
"id": "legacy-32",
"level": "new",
"position": "32",
"title": "Voltage",
"url": "https://youtu.be/wBRvBN9tmlc?si=GU2md9I7ZnUV7Rfr"
},
{
"id": "legacy-33",
"level": "new",
"position": "33",
"title": "Firework",
"url": "https://youtu.be/QBe5x2o9v2w?si=dhPg9YcCRjwngxU9"
},
{
"id": "legacy-34",
"level": "new",
"position": "34",
"title": "Silentlocked",
"url": "https://youtu.be/O-IQeUdEGvI?si=KeSHH6kwZoSVGINT"
},
{
"id": "legacy-35",
"level": "new",
"position": "35",
"title": "poocubed",
"url": "https://youtu.be/fzL31vai1ms?si=9Gwk7RDtbEFxOJ6t"
},
{
"id": "legacy-36",
"level": "new",
"position": "36",
"title": "KOSETSU",
"url": "https://youtu.be/hZ8vFX8z_BU?si=P6fFal0yazJbje2O"
},
{
"id": "legacy-37",
"level": "new",
"position": "37",
"title": "Through The Gates",
"url": "https://youtu.be/4yHA6jux5UI?si=nstk3A6FDN3QR-qm"
},
{
"id": "legacy-38",
"level": "new",
"position": "38",
"title": "Saul Goodman",
"url": "https://youtu.be/hjs5PjUaw9k?si=qJpGrru21d50gO5R"
},
{
"id": "legacy-39",
"level": "new",
"position": "39",
"title": "The Salt Factory",
"url": "https://youtu.be/lQ7M-Sgov24?si=nXYVL0XGy5NOJLXn"
},
{
"id": "legacy-40",
"level": "new",
"position": "40",
"title": "Snowbound",
"url": "https://youtu.be/cjHwgbtAkXU?si=ghJbRJCkN-m0zu60"
},
{
"id": "legacy-41",
"level": "new",
"position": "41",
"title": "CONVOLSION",
"url": "https://youtu.be/qeRKuyU3eGI?si=UPaem0b3V8fX0L1r"
},
{
"id": "legacy-42",
"level": "new",
"position": "42",
"title": "The Apocalyptic Trilogy",
"url": "https://youtu.be/RUBbpsTR5eU?si=mEsPtyMLrQgP5fWl"
},
{
"id": "legacy-43",
"level": "new",
"position": "43",
"title": "MINUSdry",
"url": "https://youtu.be/YvA8ehhzz0Q?si=DT6DfrXOQ41swSIR"
},
{
"id": "legacy-44",
"level": "new",
"position": "44",
"title": "Sevvend Clubstep",
"url": "https://youtu.be/TvA8EJTJFCc?si=Rm9HyExGYpeGn4qs"
},
{
"id": "legacy-45",
"level": "new",
"position": "45",
"title": "Edge Of Destiny",
"url": "https://youtu.be/rUphe3H59yU?si=dZ8LgDyqif1ET_k1"
},
{
"id": "legacy-46",
"level": "new",
"position": "46",
"title": "The Halucination",
"url": "https://youtu.be/tYbXsNkO9HE?si=b8hmXS98BAjDlPzF"
},
{
"id": "legacy-47",
"level": "new",
"position": "47",
"title": "CONVULSION",
"url": "https://youtu.be/qeRKuyU3eGI?si=qJN026t6RcGjU3iW"
},
{
"id": "legacy-48",
"level": "new",
"position": "48",
"title": "Solar Flare",
"url": "https://youtu.be/eHQNgty8ypY?si=ZdUIODdeDYLAXeHI"
},
{
"id": "legacy-49",
"level": "new",
"position": "49",
"title": "LIMBO",
"url": "https://youtu.be/kXYMbaMVOZg?si=fWm4ngeSxBhFZadA"
},
{
"id": "legacy-50",
"level": "new",
"position": "50",
"title": "The Catacombs",
"url": "https://youtu.be/T9-75lfKVQg?si=zEH6vP1617n5666I"
}
]
+316 -31
View File
@@ -1,14 +1,14 @@
new|1|Flamewall|https://youtu.be/x4Io4zkWVRw?si=tf0DBaWahOPPEWxO
new|2|Thinking Space II|https://youtu.be/CELNmHwln_c?si=soP6SzRn92G1vKbl
new|4|Amethyst|https://youtu.be/4lfkzz1VCbA?si=6NNeWiH-nnwu21Cb
new|3|Tidal Wave|https://youtu.be/9fsZ014qB3s?si=oj4E4ceyNtRVcDvd
new|4|Amethyst|https://youtu.be/4lfkzz1VCbA?si=6NNeWiH-nnwu21Cb
new|5|Orbit|https://youtu.be/QKcv8DkNPd0?si=cKBFqp7GRqmfupgJ
new|6|BOOBAWAMBA|https://youtu.be/20fYiqLAo_E?si=QSSixz31rWeeQ_8B
new|7|Nullscapes|https://youtu.be/EztneTPp5CU?si=ljn-S_bnGYwOoUyV
new|8|Quantuese Processing|https://youtu.be/j5NC0u1Q91Q?si=FvUEY2kdDtNMqWSD
new|9|Subsuming Vortex|https://youtu.be/0eYG1ogJpIQ?si=7XRxfDsmMVb0G9Si
new|10|Andromeda|https://youtu.be/mk3TDemdkC0?si=va13_RKAJUyFvhcv
new|11|Every End|https://youtu.be/AO--mVVFtKI?si=gIT5vxyc1FZ4_8Jj
new|10|Every End|https://youtu.be/AO--mVVFtKI?si=gIT5vxyc1FZ4_8Jj
new|11|Andromeda|https://youtu.be/mk3TDemdkC0?si=va13_RKAJUyFvhcv
new|12|Silent Clubstep|https://youtu.be/GR4OMkS3SN8?si=CMNz8bPBaUU94wiy
new|13|Anathema|https://youtu.be/_uKwmjHmySI?si=PgXwX7Z--wCay34b
new|14|Acheron|https://youtu.be/sBKR6aUorzA?si=0A9S2_W8gi1awZ81
@@ -20,31 +20,316 @@ new|19|Abyss Of Darkness|https://youtu.be/ejJkpqcMMCY?si=Rpp4HEi1kOMh-p0h
new|20|Defeated Circles|https://youtu.be/nU5AQPzd2YA?si=6eKDMPh-2QjDBBOF
new|21|Tunnel Of Despair|https://youtu.be/LpS4JREhW98?si=Z6_DRV5YZTEA0TeQ
new|22|Subterminal Point|https://youtu.be/h2wmRMgACH4?si=nDD0-bmLqPgcdVJP
new|23|KOCMOC|https://youtu.be/2CxE-UWCIG4?si=9xz4hBjtNKe8Rcbs
new|24|Slaughterhouse|https://youtu.be/kpcF1-QAHQc?si=7ybF9EbcjqHzloLM
new|25|Kyouki|https://youtu.be/KDa5c0CJTHs?si=WU0sPusFLB7Jtkam
new|26|The Lightning Rod|https://youtu.be/nQDTi077O6M?si=Kqp2Od3lJx1YWTDB
new|27|Based After Based|https://youtu.be/yQBFyUvB3lY?si=Cmxbc5jrTrNlcmG5
new|28|CHIL|https://youtu.be/DROMiCc2ZRM?si=_bR8CCnqJGVCex9F
new|29|Sakupen Circles|https://youtu.be/ofG2mJi9kEA?si=3yv78_jxidKPhtL5
new|30|Deimos (ItsHybrid)|https://youtu.be/b2yHaIk5zio?si=AMIuVFR-EW4CK8_P
new|31|Eyes In The Water|https://youtu.be/yvLwiOy3KEA?si=1991xs25WTEoTbE0
new|32|Voltage|https://youtu.be/wBRvBN9tmlc?si=GU2md9I7ZnUV7Rfr
new|33|Firework|https://youtu.be/QBe5x2o9v2w?si=dhPg9YcCRjwngxU9
new|34|Silentlocked|https://youtu.be/O-IQeUdEGvI?si=KeSHH6kwZoSVGINT
new|35|poocubed|https://youtu.be/fzL31vai1ms?si=9Gwk7RDtbEFxOJ6t
new|36|KOSETSU|https://youtu.be/hZ8vFX8z_BU?si=P6fFal0yazJbje2O
new|37|Through The Gates|https://youtu.be/4yHA6jux5UI?si=nstk3A6FDN3QR-qm
new|38|Saul Goodman|https://youtu.be/hjs5PjUaw9k?si=qJpGrru21d50gO5R
new|39|The Salt Factory|https://youtu.be/lQ7M-Sgov24?si=nXYVL0XGy5NOJLXn
new|40|Snowbound|https://youtu.be/cjHwgbtAkXU?si=ghJbRJCkN-m0zu60
new|41|CONVOLSION|https://youtu.be/qeRKuyU3eGI?si=UPaem0b3V8fX0L1r
new|42|The Apocalyptic Trilogy|https://youtu.be/RUBbpsTR5eU?si=mEsPtyMLrQgP5fWl
new|43|MINUSdry|https://youtu.be/YvA8ehhzz0Q?si=DT6DfrXOQ41swSIR
new|44|Sevvend Clubstep|https://youtu.be/TvA8EJTJFCc?si=Rm9HyExGYpeGn4qs
new|45|Edge Of Destiny|https://youtu.be/rUphe3H59yU?si=dZ8LgDyqif1ET_k1
new|46|The Halucination|https://youtu.be/tYbXsNkO9HE?si=b8hmXS98BAjDlPzF
new|47|CONVULSION|https://youtu.be/qeRKuyU3eGI?si=qJN026t6RcGjU3iW
new|48|Solar Flare|https://youtu.be/eHQNgty8ypY?si=ZdUIODdeDYLAXeHI
new|49|LIMBO|https://youtu.be/kXYMbaMVOZg?si=fWm4ngeSxBhFZadA
new|50|The Catacombs|https://youtu.be/T9-75lfKVQg?si=zEH6vP1617n5666I
new|23|Penumbral|https://youtu.be/nVHPFf13M-I?si=1TpoGSwOX63uOtj1
new|24|KOCMOC|https://youtu.be/2CxE-UWCIG4?si=9xz4hBjtNKe8Rcbs
new|25|Slaughterhouse|https://youtu.be/kpcF1-QAHQc?si=7ybF9EbcjqHzloLM
new|26|Kyouki|https://youtu.be/KDa5c0CJTHs?si=WU0sPusFLB7Jtkam
new|27|The Lightning Rod|https://youtu.be/nQDTi077O6M?si=Kqp2Od3lJx1YWTDB
new|28|Based After Based|https://youtu.be/yQBFyUvB3lY?si=Cmxbc5jrTrNlcmG5
new|29|CHIL|https://youtu.be/DROMiCc2ZRM?si=_bR8CCnqJGVCex9F
new|30|Sakupen Circles|https://youtu.be/ofG2mJi9kEA?si=3yv78_jxidKPhtL5
new|31|Deimos (ItsHybrid)|https://youtu.be/b2yHaIk5zio?si=AMIuVFR-EW4CK8_P
new|32|Eyes In The Water|https://youtu.be/yvLwiOy3KEA?si=1991xs25WTEoTbE0
new|33|Voltage|https://youtu.be/wBRvBN9tmlc?si=GU2md9I7ZnUV7Rfr
new|34|Firework|https://youtu.be/QBe5x2o9v2w?si=dhPg9YcCRjwngxU9
new|35|Silentlocked|https://youtu.be/O-IQeUdEGvI?si=KeSHH6kwZoSVGINT
new|36|poocubed|https://youtu.be/fzL31vai1ms?si=9Gwk7RDtbEFxOJ6t
new|37|KOSETSU|https://youtu.be/hZ8vFX8z_BU?si=P6fFal0yazJbje2O
new|38|Through The Gates|https://youtu.be/4yHA6jux5UI?si=nstk3A6FDN3QR-qm
new|39|Saul Goodman|https://youtu.be/hjs5PjUaw9k?si=qJpGrru21d50gO5R
new|40|The Salt Factory|https://youtu.be/lQ7M-Sgov24?si=nXYVL0XGy5NOJLXn
new|41|Solar Flare|https://youtu.be/eHQNgty8ypY?si=ZdUIODdeDYLAXeHI
new|42|Snowbound|https://youtu.be/cjHwgbtAkXU?si=ghJbRJCkN-m0zu60
new|43|CONVOLSION|https://youtu.be/qeRKuyU3eGI?si=UPaem0b3V8fX0L1r
new|44|The Apocalyptic Trilogy|https://youtu.be/RUBbpsTR5eU?si=mEsPtyMLrQgP5fWl
new|45|MINUSdry|https://youtu.be/YvA8ehhzz0Q?si=DT6DfrXOQ41swSIR
new|46|Sevvend Clubstep|https://youtu.be/TvA8EJTJFCc?si=Rm9HyExGYpeGn4qs
new|47|Edge Of Destiny|https://youtu.be/rUphe3H59yU?si=dZ8LgDyqif1ET_k1
new|48|The Halucination|https://youtu.be/tYbXsNkO9HE?si=b8hmXS98BAjDlPzF
new|49|CONVULSION|https://youtu.be/qeRKuyU3eGI?si=qJN026t6RcGjU3iW
new|50|LIMBO|https://youtu.be/kXYMbaMVOZg?si=fWm4ngeSxBhFZadA
new|51|The Catacombs|https://youtu.be/T9-75lfKVQg?si=zEH6vP1617n5666I
new|52|Gaggatronda|https://youtu.be/A4G41vw6Cf0?si=0B8vDSj8vPElRa4i
new|53|Codependence (Solo)|https://youtu.be/IKGk41hSnDo?si=1hmZYSDTYOxjWW6u
new|54|Belladonna|https://youtu.be/saHSgoU1JQQ?si=SyIGkLNhmE1D8So7
new|55|Collapse (Nexel)|https://youtu.be/RCFpXyf6bn0?si=WMfEkJJRqN6xaLxZ
new|56|Mayhem|https://youtu.be/U6e6DD32noM?si=YCWNMvQ34ohce3H3
new|57|Walter White|https://youtu.be/KzK62PkWOJ0?si=UQdOGms7z-pG-XPp
new|58|CROMACAVE|https://youtu.be/tfpsMZI6z-o?si=OG_8nmb3MUpR-EBz
new|59|The Plunge|https://youtu.be/8gW1-_u1K0Y?si=Sei6SdC7G-QzrKdy
new|60|Infinite Chaos|https://youtu.be/wOMVERxU5IA?si=xig-kQM3x3e5fH1k
new|61|Damascus|https://youtu.be/MWQyCUGt76Y?si=p3xkwoLEbK0p-ucw
new|62|Operation Evolution|https://youtu.be/WsVF-ayipvY?si=SGz8C0T9IXeu1YeR
new|63|SARYYX NEVER CLEAR|https://youtu.be/rRlyvmjcz_4?si=v6Eva9LHV2iLzNaf
new|64|The Yangire|https://youtu.be/V3FjIorIfWg?si=5DR0YRTHFMw8dZ7z
new|65|Decks Dark|https://youtu.be/l52ApE4HWkY?si=85C832hCxG0PUThq. hi
new|66|Climax|https://youtu.be/E4ppq2WeAAs?si=dOWHqoBCSC_3liFu
new|67|ORDINARY|https://youtu.be/4QiHP3jpGnk?si=0plaftXpA8nOrUf8
new|68|wavterminal|https://youtu.be/dlvsCE3xYkQ?si=ihNU2fqTufiJ3t7N
new|69|Loops Of Fury|https://youtu.be/9HBG3i5zbw8?si=7SnwcoJkF5Wke-im
new|70|Sinister Silence|https://youtu.be/dJCdTMQxHMo?si=eXhBEf1AiZRxnwr_
new|71|arcturus|https://youtu.be/a_Bqa8l9Xtc?si=Tz6FaY_wAld1xjGq
new|72|Cimmerian Shade|https://youtu.be/lUNvV1k19HA?si=P9_B0sDua9WZ_bQj
new|73|PSYCHOPATH|https://youtu.be/1luL7tTE8p8?si=YKhXZzpUsFUntBQ2
new|74|Midnight|https://youtu.be/NhkN_a9cNi8?si=kxmLQdZfSmiXjV6s
new|75|Tartarus|https://youtu.be/JptG7rwti1U?si=HCtGcPUmKQEtJlB9
new|76|Sonic Wave Infinity|https://youtu.be/C3-l_fFikpQ?si=-tbOdlhQ49L9WeOe
new|77|BEELINE (solo)|https://youtu.be/nt6T9YDb6gw?si=isN4Q0jDZnuMsldv
new|78|Jigsaw|https://youtu.be/FPfSqlI0T30?si=xb8vXJ2girhi-dbs
new|79|Waterfall|https://youtu.be/VKuVdqneG9Y?si=8CRjd1ttXRM8mWLh
new|80|THE JET ENGINE|https://youtu.be/8Q20XU__LoQ?si=NKm0fq3qFgiX53e9
new|81|Coalescence|https://youtu.be/do-a2EzZ4R8?si=zEIRXm5OR97WKgX1
new|82|BPATA MPAKA|https://youtu.be/sSbHYzYgYfA?si=7g9yQ1XnoZtQrqgP
new|83|The Wonder Of You|https://youtu.be/iQ3RzYCvLvA?si=JU5AW_dP1Mgmxq7A
new|84|Delta|https://youtu.be/_rr3J8DkIm4?si=BU9VW8h-wKoWAcRJ
new|85|The Golden|https://youtu.be/wpS5PSHmji0?si=J7dT2Srdo_LqTrd7
new|86|Terminal Rampancy|https://youtu.be/29hRhhqQTLc?si=5GGOrtxUootbEY-F
new|87|Natural Disaster|https://youtu.be/_0GqdSFmG_8?si=4kUa8hJqK-bT70nI
new|88|Viprin UFO|https://youtu.be/LTMW9Py-nHE?si=vIBNIDOhvQi9FpVd
new|89|Oblivion|https://youtu.be/ZcmDAm50BB0?si=otqXWvzIRzL1L3wq
new|90|NETWORK|(Agat3) https://youtu.be/K7wJi01J5D8?si=hEJIwYn1J9RY_-XG
new|91|UNKNOWN|https://youtu.be/a1sQxQqU6EM?si=737UNiYWoU5CM0zQ
new|92|Verdant Landscape|https://youtu.be/wjZxkS5KC0A?si=WFTg6aixL7Z0YAGp
new|93|Levigo|https://youtu.be/AROmUo7DNkE?si=eHXmi5PmBW94sXRl
new|94|ATOMIC CANNON Mk III|https://youtu.be/xlAI_wVvnx8?si=3UJXMPHYGKH-qDyC
new|95|Shukketsu|https://youtu.be/nkG4hKJIz2g?si=KNly0dPd063HIlKU
new|96|Checked Steam|https://youtu.be/cq6047eYGXs?si=yE7kCbjdAp_7iMlM
new|97|WOBBLING MACHINE (Solo)|https://youtu.be/RKX6VOsICOg?si=QjLbBucxWJhI3TW0
new|98|Critical Heat|https://youtu.be/UjYIt2jy_BY?si=5Jl3FoAOEPSGL1t-
new|99|Crystal Crusher (Solo)|https://youtu.be/RKX6VOsICOg?si=ZPPM3qjbx2KraxYW
new|100|limbo but uwu ig idk|https://youtu.be/3B36N-qfOD8?si=NshDR7CO9whAkga9
new|101|The Paroxysm of Rage|https://youtu.be/nq3i1qJQeEk?si=-xey2Qd9hrtu_AZU
new|102|paranoia (amplitron)|https://youtu.be/Aw83i8Zmh_0?si=0GwEgBHuexCHsuhk
new|103|Blood Echo|https://youtu.be/fSvk4tLlQzQ?si=a6pIZkJ58uM2y5gs
new|104|Aerial Gleam|https://youtu.be/eyZjKtrGVYw?si=qmsOlWAezCCgYeGb
new|105|DISCONNECT|https://youtu.be/HxHo-LzwSao?si=PsuqKQi9DL-iYtUW
new|106|VOID|https://youtu.be/kN1FUoucoSc?si=NjE2E12FF9urJL45
new|107|Starlit Stroll|https://youtu.be/U7MM74dTXuI?si=WofCw6_u86gVLFSb
new|108|Trueffet|https://youtu.be/yHESkazVD4Q?si=Wo04Qh1dOGEDb3JH
new|109|Henken|https://youtu.be/M--2oQKrV68?si=WsNcHkxpVO3BGW6T
new|110|Graceful|https://youtu.be/5kOD37vQHKE?si=VXPDbYRAZstaq_Oy
new|111|Kenos|https://youtu.be/Bs1kVySdUtI?si=4DAwPGeehZX6YzEJ
new|112|azure blast|https://youtu.be/TN0FQ3KBv4w?si=JVSf1iXaez8sSOw0
new|113|Fragile|https://youtu.be/XfXzGv0oALw?si=0Blg4XOudYEomR2A
new|114|Starlight Summit|https://youtu.be/LB9xx41M4Ns?si=xC9MsTIzlM44AdhE
new|115|chrome hearts|https://youtu.be/GK0Z8rgziH4?si=orYnpQGMPKPQTW_O
new|116|Esfera|https://youtu.be/GK0Z8rgziH4?si=1km0bCL22Egbl-Xk
new|117|Destruction 19|https://youtu.be/UUyzGg_4mqs?si=uJw74R5zv3GwYiag
new|118|NEUTRA|https://youtu.be/0fNBFwNS1qg?si=sqeqmMqFotN6jgGh
new|119|Time Lapse|https://youtu.be/MnI1mM_wjzc?si=FEmB2fWSeNqJDIwv
new|120|Dark Dimension|https://youtu.be/g2DrT9bHjOI?si=TuVvUXo1XDGlNB5_
new|121|Guideless Goobering|https://youtu.be/pFxdGKYQrcg?si=nQ3kVcqOq8UEt6uB
new|122|Hard Machine|https://youtu.be/z3grmsqAaas?si=xBvAriHwGl-IneUd
new|123|Swing Swing|https://youtu.be/_hL9hFU3WI4?si=4ihgAE4n8VrN4lsk
new|124|DISSONANCE|https://youtu.be/IfFser-f9c0?si=P2D23zMCX96QPEL8
new|125|Zodiac|https://youtu.be/N4QjElo58_o?si=0PMsj7NgBfUUIzLZ
new|126|Goober Rage Stage|https://youtu.be/1rrouUCPXhY?si=2o6cHwApb4o-08jL
new|127|Crackhead Circles|https://youtu.be/_4MDq8Us5gM?si=KO4JZZNs5mYpzAg3
new|128|Lotus Flower|https://youtu.be/pTgOHcwJhd0?si=k92uqgcP9o-Ovvxr
new|129|Scream Machine|https://youtu.be/n-KeSGEX7Jg?si=dY8omM-22Iju1uck
new|130|Axinie|https://youtu.be/LjfO2fqnozI?si=KuTXmwjvM6_t45rS
new|131|Widestep|https://youtu.be/TN8fHN_XV3o?si=UdbpxVI9fvujr4RU
new|132|Judgement Knights|https://youtu.be/MzzY8nWfxnk?si=zAXlxJZFiKCGlu9A
new|133|Keres|https://youtu.be/MFFIjZP7PxQ?si=lkc9tRawW-XNv6-7
new|134|Lithium|https://youtu.be/IfVM56qNVdA?si=AIEw5JbV9OQW60eJ
new|135|IRIS|https://youtu.be/WiFwAU8pgr4?si=Q0fTY2Hd2oQLBLaW
new|136|Cold Sweat|https://youtu.be/tBpEPXGhQug?si=0s6Mh429yBF_dkea
new|137|Galeforce|https://youtu.be/G6VF2Gvmpz0?si=WzSIerGhoz3u0fcV
new|138|in this|https://youtu.be/gQiOdxOyNGg?si=gTmiy_pThUKYLcb0
new|139|Frost Spirit|https://youtu.be/KXbaRnRpm58?si=LHfUVqpxgRYtEmkH
new|140|Thinking Space|https://youtu.be/iHf2nanWjvE?si=stgikKc4PPw8F7vp
new|141|Promethean|https://youtu.be/19kE_Yo8puE?si=0AcDFf0OjghIgdpX
new|142|Ascent|https://youtu.be/sII8zQZIJlg?si=gHbK7-s-CLho4vM0
new|143|Dry Out Copyable 2|https://youtu.be/FXW9J9CaREU?si=6Be51250LT0SQLHy
new|144|Renevant|https://youtu.be/2Y8kBw8YxO8?si=UQS31gMvOew6fsrc
new|145|ConClusion|https://youtu.be/wpuPFFqnJFY?si=CEHAmw_k9_WjadYw
new|146|Trotil|https://youtu.be/kblnO7LfQn4?si=Lq_V_8BrvzGo-KAH
new|147|We Are Not The Same (Solo)|https://youtu.be/yLLd4mpeYC4?si=5Cugl6UYnBcx8A0a
new|148|Disconnected Descent|https://youtu.be/TP7OAAVH-uA?si=2_eKfx19pc3xyZFt
new|149|Instinct (KrmaL)|https://youtu.be/6W3Gvi9ft0w?si=o-AAu2Yz7G5Ytm2Y
new|150|Calculator Core|https://youtu.be/VrgNFsF2NGw?si=VxW5clIFp8YLf0nv
new|151|Sky Shredder|https://youtu.be/Z-69-4RKRgk?si=O96OhtDsQ1BFgJLG
new|152|Crimson Planet|https://www.youtube.com/live/euMInLjyG6k?si=09vneTJfThjnpACw
new|153|shimmer (amplitron)|https://youtu.be/J89j-_l7ezg?si=fMAyAXx2w028BXR8
new|154|DIRECTIONS|https://youtu.be/-SEgSgxpRxo?si=ZLxK5XD8B8pPiVsp
new|155|ATOMIC CANNON Mk II|https://youtu.be/4-lOnZsfhNQ?si=ON-czwtJawFMgZc_
new|156|Ringy Paracosm|https://youtu.be/RIDuh4IVGiw?si=9s892WxqybVKVYlP
new|157|Cognition|https://youtu.be/enl0GOyskPs?si=qe18-qXwxoUkokyt
new|158|Axiom Asterism|https://youtu.be/LnFdka3HekA?si=ehLne5DftKG33cQH
new|159|Indivine|https://youtu.be/g2CVts-tX70?si=XfKVwmDp5HoTj_--
new|160|Neon Skyline|https://youtu.be/HvGjIAeLv_8?si=sEu3tWgtJJrcbHxd
new|161|Cosmic Cyclone|https://youtu.be/0I9xCfTOpXg?si=2Q1_twV5HH3zLOMN
new|162|CORRODERE|https://youtu.be/-V87Bh44lAU?si=zIbxyCYezyK_dAna
new|163|SARY NEVER CLEAR|https://youtu.be/23u3SlwrOMw?si=GMUrk4CONI8n-RHy
new|164|qoUEO|https://youtu.be/PJbfroH0sok?si=lQM4JwbyXlwc85td
new|165|Rigel|https://youtu.be/liJFeLHcW_Q?si=UaqSqF1aJLB6kt6t
new|166|Scrubbabingo force|https://youtu.be/eT_izuCxGUU?si=_VzD8E46NCZMx11v
new|167|RUST|https://youtu.be/mEXGlrNMyR4?si=HQuHo3l0QAtY4hPO
new|168|Call Me Maybe|https://youtu.be/Jc-98ND5OJk?si=3EsBQ7paobUcgnel
new|169|ta1LSD0ll|https://youtu.be/w-NH14vgMb8?si=xocgmpg6Jn92_Evs
new|170|SAND SAILOR|https://youtu.be/QPmzn9X77Uo?si=vWrkuwERNWZbYcTb
new|171|Gloxinia|https://youtu.be/MJh_6Z5v6bM?si=Lyzx4IoWnWQfZELk
new|172|Akashic Records|https://youtu.be/qnZnV6q-mt4?si=7n69Q8erUDa58c0H
new|173|Cobwebs|https://youtu.be/bgZ85rCaEGY?si=gEsYkSGnk6G5qP_r
new|174|meow hard|https://youtu.be/jOhGH2TckDA?si=ZhjMOsVjzFxDPFZm
new|175|Lucid Nightmares|https://youtu.be/G7hBE5qWx6M?si=D8M1hqmsA9k3oVOU
new|176|RUTHLESS|https://youtu.be/9jwVeYtc6H4?si=meyxbaKJqCe5Fums
new|177|Coral Cave|https://youtu.be/kTOM1nSEF3I?si=XpkVW2-qTylokjI-
new|178|SUPERHATEMEWORLD|https://youtu.be/fhmgZYCJ29M?si=yjgUX5sohJ0ej1uW
new|179|Launchpad Labyrinth|https://youtu.be/XELUzw7UbNU?si=rxyVGXKE74rHb8DT
new|180|Tenkai (Solo)|https://youtu.be/AM2KHSZkjcc?si=bkHkgj1CO1yv31vv
new|181|Sharpscapes|https://youtu.be/wM-J6fLIs14?si=4njb0wT6coAwDDZB
new|182|obsession|https://youtu.be/OjXKjQXnhjA?si=5QdHmozIHvdl4I4-
new|183|Horros (Solo)|https://youtu.be/OS46C-zvqDA?si=2DKf_LRA2PwIjAtG
new|184|Omega Interface|https://youtu.be/sVngEgj0who?si=CyRbOzs4OKeB4StS
new|185|Amalgam|https://youtu.be/82DDR2BjXSI?si=xm-EDHngoNovfmzf
new|186|Ragnarok|https://youtu.be/aGtXqn9HL2o?si=Hhkr5oZGdAF4iMgZ
new|187|Sides Of My Mind (Solo)|https://youtu.be/d3d2KPVgCAk?si=L-zr7NZGRrWqiDuJ
new|188|Ykds1479ymdppr|https://youtu.be/Wom_gMcauUI?si=UVV5h9rfRPws1Wr5
new|189|The Art of the Blade|https://youtu.be/KSJl4A9eUW4?si=PmbP4G67aQEaeyyh
new|190|NYUTOPIA|https://youtu.be/TPyZKiTOUQA?si=5_D_ddUqaz7xSDpS
new|191|me when machine gang (Solo)|https://youtu.be/C8uSFs01qwc?si=dgFXyQFS1rKk3ug9
new|192|Deep Bass|https://youtu.be/5uu8xXgz_4s?si=jnpzgqcYgIxNS5m6
new|193|Pandemonium (Cersia)|https://youtu.be/OKEHsJPaeQY?si=cv5MRfDMZjs4nwno
new|194|Silent Club|https://youtu.be/NjEiHIokTGM?si=wmVPS04r8wi_jgNG
new|195|Descent Into Exile|https://youtu.be/whtt_WkG_20?si=Ly3S1-pgtmJfDgpI
new|196|The Rupture|https://youtu.be/SaU454afkKY?si=QcXiCR2boQsd8iWg
new|197|no jokes|https://youtu.be/oVhGYiQ8fBc?si=gtjiunr2HOryqsGn
new|198|Jupiter My Favourite|https://youtu.be/adVQ_yflcGM?si=9l3Eo1YxpL7SQ5o1
new|199|Kappa|https://youtu.be/eIfc1zUuOPU?si=OETCzxDyPm4VtXhI
new|200|FARBIDI THEORY|https://youtu.be/MQD6klwhcsM?si=-8ZQ1fakCN5HVqAw
new|201|LD50|https://youtu.be/2OiS9mVDlmQ?si=XvEHVQlMZvYv7eUM
new|202|Bloom|https://youtu.be/oZTyTgeb7q4?si=MC61WABnqDPSl377
new|203|the ocean of fish|https://youtu.be/dNHUg49I3sM?si=chQLiCsZmoPjhh6E
new|204|BarbarosFinaleFinale|https://youtu.be/F32ekXnS3a4?si=UW9apT8TfrCh6NEL
new|205|Nabil Let Go|https://youtu.be/IRYire6RkUg?si=h50D6hCRKxr8O4wR
new|206|Bloodlust|https://youtu.be/5SzKetF2btw?si=hs8FajAMmW52a2DD
new|207|Asterios|https://youtu.be/YG3taML8WjA?si=p68EsJTC4tJ58g4q
new|208|Sank|https://youtu.be/6ZuEWp1dJ0c?si=eR2pCKpXj_JPVDUEz
new|209|Escape Room|https://youtu.be/sZebhp6gFuI?si=HrzmtVkFY9OZtJBa
new|210|Jesse Pinkman|https://youtu.be/fYcBtp4X8GA?si=HlxPFd0TEfufE5Qz
new|211|Moving Forward|https://youtu.be/2wYnXim6TYY?si=z66XmjVqWN3w5Xo0
new|212|X84|https://youtu.be/ZzGVEmS5D8s?si=WRnQYxFWB9uFAmJi
new|213|Excruciation Chamber|https://youtu.be/BdkuNdFfY-o?si=QeVwcyv4Sx27exQI
new|214|Terminux|https://youtu.be/-UgocJ3WYn4?si=uSiVYs6waOj5y8pz
new|215|Twilight|https://youtu.be/5bZ3RQP7fjs?si=EqntxnyH46JPb4tM
new|216|IthacropoliX|https://youtu.be/n4nbu_fZcf4?si=3AojnEZYAGP3BBPQ
new|217|CITRA|https://youtu.be/O_kYXvwqrWo?si=EjQHq04G3caSRj5H
new|218|Nhelv|https://youtu.be/k117h9HUt6s?si=O-tw91m3_FFR0mYN
new|219|Awedsy|https://youtu.be/hx5ebE8e6Eo?si=aoXlzm82otqvYXmJ
new|220|TORN|https://youtu.be/QUpzux7AoTI?si=5BBH3x-j2x62ttNl
new|221|MewneI|https://youtu.be/yWfbbEbTK9w?si=LHHUHaA3zc7Yy_uY
new|222|Deimos (EndLevel)|https://youtu.be/FgxsNzQnF5o?si=yalVUa7cMm-_KVXC
new|223|Congregation|https://youtu.be/KVlcdvGYcj0?si=uwMTvD45yMSST6jW
new|224|Gracefully|https://youtu.be/5RBn1nk5wb4?si=YzBymZ_FrG1fGeK0
new|225|DsinK|https://youtu.be/EsQAFvKqo3U?si=9DApZCy2SmlFOnLo
new|226|Sigma|https://youtu.be/3INdO0esQRw?si=LTYmo4jqmiNECG4W
new|227|Sazerix|https://youtu.be/l8aBStUgYOs?si=xxfUBcsVHJDeiutP
new|228|Pootis Engage XTREME|https://youtu.be/2SqKRRphIVI?si=e__iWF4KfDo_QrsR
new|229|Aronia|https://youtu.be/eJe3i5KyKOk?si=LpTBN7qyPb4VIYDq
new|230|Exosphere|https://youtu.be/HRbBub341W4?si=tPr0acTWVgsYiSfj
new|231|untitled unmastered|https://youtu.be/zzPsYgyU4n4?si=GhsZ-FF2GtgI-E9f
new|232|Pagoda|https://youtu.be/KVomsNjGalQ?si=gOl163Fh0uJgbxpxhttps://youtu.be/npIZ8TGMDvk?si=4HY9BHGn-5jf8JXH
new|233|Fog|https://youtu.be/Kt_fxnOYeco?si=LnB8xdFEpvNvEUED
new|234|Knights of Thunder|https://youtu.be/npIZ8TGMDvk?si=A7qhkz-Y7-Vif1l2
new|235|ATOMIC CANNON|https://youtu.be/VZS5Yn9Y-OI?si=PTnl4Z7QFIUx1YNI
new|236|Cicatrize|https://youtu.be/QfF3jgPQZk0?si=h5Ov2QbMoNCo0aVk
new|237|Dust Bowl|https://youtu.be/EXuxHOAAVrI?si=uOOvtL7gLoZ0odwr
new|238|Gustavo Fring|https://youtu.be/OB0YiqyEp2E?si=7pkhUZ6Fzuu4HVVi
new|239|Ploink|https://youtu.be/BV19Dg09LN0?si=kTEGTO_tyhpA_SoZ
new|240|Fragmented|https://youtu.be/RnLoliyy4k0?si=_Vuwadp_AEsTKuDX
new|241|Ouroboros|https://youtu.be/hLTnewDC2Hs?si=OnPDUTPAQAQCcU0W
new|242|FRIDAY|https://youtu.be/YUoFhrN3aKw?si=ma7I1IAtU0UbrSDu
new|243|CONNECT|https://youtu.be/oo_PqnEHZvo?si=q1jV7efm_cq_XPYM
new|244|Ourwa|https://youtu.be/BImzHJnr5Ks?si=_ZT5WifsgFTBMK9O
new|245|XRAY|https://youtu.be/zk1G8Spw1hg?si=YL20_J-29p0x-iD2
new|246|Visible Ray|https://youtu.be/hHpaB752peM?si=syGmQ7hSd536vRCp
new|247|the wiener|https://youtu.be/rgMruLorH1c?si=hQ6-z1GvwBCNzk8A
new|248|Hardry|https://youtu.be/59JlNpDlhH8?si=fXzYUMwfmEjMPmQa
new|249|Ghoul|https://youtu.be/W1kgdxV_8E0?si=9ULxBYkdf5TaoHX6
new|250|kowareta|https://youtu.be/wxWBFaZYHPM?si=7-9C-9-oa5jrNIrC
new|251|Apotheosis|https://youtu.be/X261xjwc3HQ?si=7-DnqQMviuTWtoOG
new|252|EGO KILLING|https://www.youtube.com/watch?v=xnCsfVFygC4
new|253|Frozen Cave|https://www.youtube.com/watch?v=BFaqgkw-Hg0
new|254|Dump|https://www.youtube.com/watch?v=UDrCb0rzuQo
new|255|SZYSLAK|https://www.youtube.com/watch?v=CUov9IB5wjg
new|256|Th3Dev0n|https://www.youtube.com/watch?v=f7s6i0M1n-g
new|257|The Yandere|https://www.youtube.com/watch?v=MN6Dra6yAys
new|258|2 Comme le vent|https://www.youtube.com/watch?v=P7tLT3QEHw4
new|259|Death Corridor Z|https://www.youtube.com/watch?v=9799msdH394
new|260|Arctic Lights|https://www.youtube.com/watch?v=yd5cH9nvDvg
new|261|PROTON LAUNCHER|https://www.youtube.com/watch?v=8stjtuvzfyk
new|262|Dedohexdragon|https://www.youtube.com/watch?v=z_zegPqfqr4
new|263|ROCKY ROAD|https://www.youtube.com/watch?v=uQqB3cVh_AM
new|264|Plasma Pulse Finale|https://www.youtube.com/watch?v=xqzx9cu6XoA
new|265|CHROMA FINALE|https://www.youtube.com/watch?v=ByJ9rz4-WUI
new|266|Spectrum Cyclone|https://www.youtube.com/watch?v=IN0XeMo2SmM
new|267|Icefall|https://www.youtube.com/watch?v=9dy1afi-fo4
new|268|Storming Summit|https://www.youtube.com/watch?v=ymmMnZLpfQU
new|269|CRIDIUM|https://www.youtube.com/watch?v=IS3AcckWUT8
new|270|REPENTLESS|https://www.youtube.com/watch?v=-lexD6huBlg
new|271|Specthys (Solo)|https://www.youtube.com/watch?v=QEhqvQvEfNM
new|272|Alpha|https://www.youtube.com/watch?v=x9s3nSRUM7A
new|273|Fever Dream (icedcave)|https://www.youtube.com/watch?v=Mc3vZeM_VYw
new|274|Shutdown|https://www.youtube.com/watch?v=A-A6_ybDY2I
new|275|Calamity|https://www.youtube.com/watch?v=PwOPRxp6KjY
new|276|Aquatic Auroras|https://www.youtube.com/watch?v=cD0zZ9OJkBE
new|277|Cersia Madness|https://www.youtube.com/watch?v=vGHOD8OWgdY
new|278|Piano Woman|https://www.youtube.com/watch?v=xCc_h4grIvY
new|279|RAM|https://www.youtube.com/watch?v=5cl7WAQJKdY
new|280|GBS|https://www.youtube.com/watch?v=zBmIYg2dpk4
new|281|nevermore|https://www.youtube.com/watch?v=VSxu7YnyGsQ
new|282|WORSTBRAINMAP|https://www.youtube.com/watch?v=GWjIIFktU04
new|283|Midnight Tokyo|https://www.youtube.com/watch?v=ALeB-Svv0BQ
new|284|Sparkling|https://www.youtube.com/watch?v=4r9hkMxpiQw
new|285|of Ambrosia|https://www.youtube.com/watch?v=0KtPsPVpj78
new|286|Mirrored Calamity|https://www.youtube.com/watch?v=5Wo0eFR8Lwk
new|287|Iris of a Puppeteer|https://www.youtube.com/watch?v=DK3BShj1zYc
new|288|Requiem|https://www.youtube.com/watch?v=sRCQ1JjhBq4
new|289|Dance of the Violins|https://www.youtube.com/watch?v=PYwI_ODrrlw
new|290|Eternal Moment|https://www.youtube.com/watch?v=FbPJ2fclXHQ
new|291|Edge of the World|https://www.youtube.com/watch?v=QWy1RnPDCWk
new|292|RANDOM ACCESS MEMORY|https://www.youtube.com/watch?v=6_pYfJKKpRY
new|293|SisuSonic|https://www.youtube.com/watch?v=epTEemVtIqY
new|294|Beyond Hell|https://www.youtube.com/watch?v=RRtIiUBaVo4
new|295|ConTroller|https://www.youtube.com/watch?v=kXLBuSDagOE
new|296|Omega|https://www.youtube.com/watch?v=spYufsgr3NU
new|297|AKIRA|https://www.youtube.com/watch?v=RHCCZkf4UFM
new|298|Gamma|https://www.youtube.com/watch?v=HTXRWzS2aw8
new|299|Golden Club|https://www.youtube.com/watch?v=YBs04o1ZVQY
new|300|network (by VanDerVals)|https://www.youtube.com/watch?v=uA5i_jBW2hY
new|301|Molten Gear|https://www.youtube.com/watch?v=NMRaJu5BiAI
new|302|Spacial Rend|https://www.youtube.com/watch?v=VgiRJRBzacM
new|303|Hyper Paracosm|https://www.youtube.com/watch?v=zQbU_2To0bg
new|304|Collect All Pets|https://www.youtube.com/watch?v=NArTpUWoOQ0
new|305|ADOMANIA|https://www.youtube.com/watch?v=-AJl65nhTEA
new|306|Time Extreme|https://www.youtube.com/watch?v=gzqeoJK_UvM
new|307|Wasureta|https://www.youtube.com/watch?v=9jkCtNDbDt0
new|308|Ryft|https://www.youtube.com/watch?v=Wxjk0A09XNU
new|309|Macabre|https://www.youtube.com/watch?v=b2kGqEY8E1Q
new|310|OVERKILL|https://www.youtube.com/watch?v=aBDP_9-Sm-w
new|311|Cybernetic Crescent|https://www.youtube.com/watch?v=0yKtq1PJeDQ
new|312|ELLIPSISM|https://www.youtube.com/watch?v=tp6-hpqtnWk
new|313|reeses potentially|https://www.youtube.com/watch?v=v45Ei-punX4
new|314|OVERKILL|https://www.youtube.com/watch?v=aBDP_9-Sm-w
new|315|Cybernetic Crescent|https://www.youtube.com/watch?v=0yKtq1PJeDQ
new|316|ELLIPSISM|https://www.youtube.com/watch?v=tp6-hpqtnWk
new|317|reeses potentially|https://www.youtube.com/watch?v=v45Ei-punX4
new|318|Sewers|https://www.youtube.com/watch?v=YBWx2Dm10Ag
new|319|Sink|https://www.youtube.com/watch?v=qX6yzZey9Ng
new|320|the DEADLANDS|https://www.youtube.com/watch?v=pC-rUt9Z5dI
new|321|FIFTY THREE|https://www.youtube.com/watch?v=JF4vQZC4rBI
new|322|ASHES|https://www.youtube.com/watch?v=cmYjLL1k5Z0
new|323|Necromancer|https://youtu.be/y-NwRnHqaJI?si=hldrpzkr7URUxjek
new|324|Rage|https://youtu.be/RW_i0Kx3gDA?si=HCGnK8oMp1LxdlS8
new|325|Kjdf8314jlfssf|https://youtu.be/FgOR1jwsXww?si=RZc_QNZssV8cx5IX
new|326|Sonic Wave Rebirth|https://youtu.be/wXxPNaqTZKU?si=fsJyrSuf7LafVM9X
new|327|Sonic Wave|https://youtu.be/GpXTwPgweAk?si=SJ3F-6VyszYB3ene
new|328|Icotact|https://youtu.be/dd1rG5DLdc4?si=UyfeaCYu4_f9viW4
new|329|IMMORTALITY|https://youtu.be/tl4trfW1JSo?si=hshKMrfXFgsP1EW7
new|330|Cersia Difficult|https://youtu.be/8Vi_wD2wp-w?si=aQmFMcsFfHb4xLAx
new|331|LUMINA|https://youtu.be/ug9mjzRT7Ao?si=x1aYQMbf7LJoUlag
new|332|Dual Pulse|https://youtu.be/CRLoL1gnuiE?si=k2kJ-Ts_r3V21Z2U
new|333|BRKMGB8GJCZ|https://youtu.be/PtAn8xfwK5w?si=Zm9l4aNL3ZJublVi
new|334|Sephiroth|https://youtu.be/VZThVeKm7lI?si=uY2vdmf0WmARldU_
new|1250|niwa|https://www.youtube.com/watch?v=vegDZr7jh-M
+4 -1
View File
@@ -1,17 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Error Pages - GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body class="error-shell">
<header>
<h1>Error Pages</h1>
<nav>
<a href="index.html">Home</a>
<a href="manage.html">Manage</a>
<a href="guess.html">Guess Game</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="rules.html">Rules</a>
+94
View File
@@ -0,0 +1,94 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Guess The Rank - GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="guess">
<header>
<h1>Guess The Rank</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main>
<section class="roulette-shell">
<section class="panel roulette-panel roulette-guess-panel">
<div class="roulette-head">
<div>
<p class="roulette-kicker">Guessing Game</p>
<h2>Where Is It Ranked?</h2>
<p class="muted roulette-intro">A level title shows up, you guess its place on the list, and you get higher or lower hints until you solve it or run out of tries.</p>
</div>
<button class="btn roulette-btn" id="guess-start" type="button">Start Round</button>
</div>
<div class="roulette-card guess-card">
<label class="control-block control-select" for="guess-mode">
<span>Mode</span>
<select id="guess-mode">
<option value="casual">Casual - 6 guesses</option>
<option value="standard" selected>Standard - 4 guesses</option>
<option value="hard">Hard - 3 guesses</option>
<option value="marathon">Marathon - 8 guesses</option>
</select>
</label>
<p class="roulette-status muted" id="guess-status">Start a round to get a level.</p>
<h3 id="guess-level-title">No level selected</h3>
<p class="roulette-rank" id="guess-attempts">Tries left: 4</p>
<p class="roulette-note muted" id="guess-feedback">Pick a mode, start a round, and enter a rank number like 1, 15, or 42.</p>
<form class="guess-form" id="guess-form">
<label class="guess-label" for="guess-input">Your rank guess</label>
<div class="guess-controls">
<input id="guess-input" type="number" min="1" inputmode="numeric" placeholder="Guess the rank" />
<button class="btn" id="guess-submit" type="submit">Guess</button>
</div>
</form>
<p class="guess-answer muted" id="guess-answer">The correct rank will show here if you run out of guesses.</p>
<a class="btn roulette-open" id="guess-open" href="#" target="_blank" rel="noopener noreferrer" hidden>View Video</a>
</div>
</section>
<aside class="panel roulette-rules-card">
<p class="roulette-kicker">Modes</p>
<h2>How this page works</h2>
<div class="roulette-rule-list">
<article>
<strong>Casual</strong>
<p>Six guesses for a more forgiving round if you just want to learn the list.</p>
</article>
<article>
<strong>Standard</strong>
<p>Four guesses for the default version of the game.</p>
</article>
<article>
<strong>Hard</strong>
<p>Three guesses if you want the original tougher setup.</p>
</article>
<article>
<strong>Marathon</strong>
<p>Eight guesses for longer rounds and closer narrowing.</p>
</article>
</div>
</aside>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
+171 -23
View File
@@ -1,36 +1,183 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>GD fedl</title>
<meta name="description" content="FEDL - Browse ranked demons, check demon list rules, play demon games, and submit demon runs. Your hub for the Geometry Dash demon list.">
<meta name="keywords" content="demon list, ranked demons, geometry dash, demon ranking, challenge demon">
<title>FEDL - Ranked Demon List Hub for Geometry Dash</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="index">
<header>
<h1>GD fedl</h1>
<nav>
<a href="manage.html">Manage</a>
<a href="players.html">Players</a>
<header role="banner">
<h1>FEDL</h1>
<nav role="navigation" aria-label="Main navigation">
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="rules.html">Rules</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="players.html">Players</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main>
<section class="panel">
<h2>Welcome</h2>
<p class="small muted">Use the top navigation to view players or fedls. This UI follows a compact table-first layout for quick browsing.</p>
<section class="home-hero">
<div class="home-copy panel">
<p class="hero-kicker">FEDL Hub</p>
<h2>Track ranked demons, learn submission rules, and master the challenge.</h2>
<p class="hero-text">FEDL is your quick home base for browsing ranked demons, checking submission rules, spinning a roulette challenge, and keeping up with the list without digging through a bunch of scattered pages.</p>
<div class="hero-actions">
<a class="btn" href="lists.html">Open The List</a>
<a class="btn" href="signup.html">Sign up</a>
<a class="btn ghost-btn" href="guess.html">Play Guess Game</a>
<a class="btn ghost-btn" href="run.html">Submit A Run</a>
<a class="btn ghost-btn" href="rules.html">Submission Rules</a>
<a class="btn ghost-btn" href="roulette.html">Spin Roulette</a>
</div>
<div class="hero-stats" aria-label="FEDL overview">
<article class="stat-card">
<strong id="hero-total-levels">--</strong>
<span>Ranked Levels</span>
</article>
<article class="stat-card">
<strong id="hero-top-entry">--</strong>
<span>#1 Demon</span>
</article>
<article class="stat-card">
<strong id="hero-last-slot">--</strong>
<span>Verified Runs</span>
</article>
<article class="stat-card">
<strong id="hero-total-players">--</strong>
<span>Top Player</span>
</article>
<article class="stat-card">
<strong id="hero-recent-runs">--</strong>
<span>Levels Added This Week</span>
</article>
<article class="stat-card">
<strong id="hero-list-updated">--</strong>
<span>Last Updated</span>
</article>
</div>
</div>
<aside class="hero-stage panel">
<p class="stage-label">Live Snapshot</p>
<h3>Top 10 levels</h3>
<p class="muted">A fast preview of what is waiting inside the list page right now.</p>
<div class="featured-list" id="featured-list">
<article class="featured-card skeleton-card">
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
<strong class="skeleton skeleton-title"></strong>
<p class="skeleton skeleton-text"></p>
</article>
<article class="featured-card skeleton-card">
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
<strong class="skeleton skeleton-title"></strong>
<p class="skeleton skeleton-text"></p>
</article>
<article class="featured-card skeleton-card">
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
<strong class="skeleton skeleton-title"></strong>
<p class="skeleton skeleton-text"></p>
</article>
</div>
</aside>
</section>
<section class="layout">
<div class="panel" style="flex:1">
<h3>Quick Links</h3>
<a class="btn" href="lists.html">View Lists</a>
<a class="btn" href="manage.html" style="margin-left:8px">Manage</a>
<a class="btn" href="players.html" style="margin-left:8px">Players</a>
<a class="btn" href="rules.html" style="margin-left:8px">Rules</a>
<a class="btn" href="roulette.html" style="margin-left:8px">Roulette</a>
<a class="btn" href="errors.html" style="margin-left:8px">Errors</a>
<section class="home-section-grid">
<article class="panel info-panel">
<p class="status-label">Browse Faster</p>
<h3>The list page is built for quick checking</h3>
<p class="muted">Search by title, jump between rank ranges, and open completion videos without leaving the site.</p>
<a class="text-link" href="lists.html">Go to lists</a>
</article>
<article class="panel info-panel">
<p class="status-label">Stay Legit</p>
<h3>The rules page explains what submissions need</h3>
<p class="muted">Use it to review raw footage requirements, mod rules, skip policies, and approval expectations before you submit.</p>
<a class="text-link" href="rules.html">Read the rules</a>
</article>
<article class="panel info-panel">
<p class="status-label">Send Proof</p>
<h3>The run page sends completions into the live queue</h3>
<p class="muted">Drop in your video, add raw footage and notes, and let the admin panel pick it up for review.</p>
<a class="text-link" href="run.html">Open submissions</a>
</article>
</section>
<section class="panel spotlight-panel">
<div class="spotlight-copy">
<p class="status-label">Start Here</p>
<h2>What you can do on FEDL</h2>
<p class="spotlight-text">This site is meant to be simple: find ranked demons, watch linked runs, check moderation expectations, and get back to playing without unnecessary clutter.</p>
</div>
<div class="spotlight-grid">
<article class="spotlight-card">
<h3>Lists</h3>
<p>Browse the ranking, filter by placement ranges, and watch linked videos from each entry.</p>
</article>
<article class="spotlight-card">
<h3>Rules</h3>
<p>Check completion requirements, accepted tools, and what moderators expect from a valid run.</p>
</article>
<article class="spotlight-card">
<h3>Guess Game</h3>
<p>See a random level name, pick a mode, and guess its rank with higher or lower hints.</p>
</article>
<article class="spotlight-card">
<h3>Roulette</h3>
<p>Pull a random demon with rank and level ID details for your next roulette attempt.</p>
</article>
</div>
</section>
<section class="panel discord-panel download-panel">
<div class="discord-copy">
<div class="discord-badge">
<span class="discord-logo" aria-hidden="true">📱</span>
<div>
<p class="discord-label">Download</p>
<h2>Geode Mod (Coming Soon)</h2>
</div>
</div>
<p class="discord-text">Geode Mod is a dedicated FEDL companion mod for Geometry Dash demon players with quick list access, roulette tools, queue tracking, and built-in validation help.</p>
<ul class="feature-bullet-list">
<li>Fast access to the full ranked demon list with real-time updates</li>
<li>Roulette generator with saved pick history</li>
<li>Submission queue tracking with status markers and moderator notes</li>
<li>Integrated rules checklist and run validation tips</li>
</ul>
<p class="discord-text discord-footnote">Launching soon for Geode, mobile, and desktop.</p>
</div>
<div class="discord-actions">
<button class="btn discord-btn" disabled>Coming soon</button>
</div>
</section>
<section class="panel discord-panel">
<div class="discord-copy">
<div class="discord-badge">
<span class="discord-logo" aria-hidden="true">📱</span>
<div>
<p class="discord-label">Update</p>
<h2>App Coming Soon</h2>
</div>
</div>
<p class="discord-text">A dedicated FEDL app is on the way. Stay tuned for updates.</p>
</div>
</section>
@@ -38,7 +185,7 @@
<div class="discord-copy">
<div class="discord-badge">
<span class="discord-logo" aria-hidden="true">
<img src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d7f4ef6498ac018f2c55_Symbol.svg" alt="Discord logo" />
<img src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d7f4ef6498ac018f2c55_Symbol.svg" alt="Discord logo" loading="lazy" />
</span>
<div>
<p class="discord-label">Community</p>
@@ -46,7 +193,7 @@
</div>
</div>
<p class="discord-text">Join the FEDL Discord to ask questions about rules, talk about levels, share progress, report issues, and get help from moderators and other players.</p>
<p class="discord-text">Join the FEDL Discord to ask about rules, talk about placements, share progress, report problems, and get help from moderators and other players.</p>
<div class="discord-highlights">
<span>Rule help</span>
@@ -56,10 +203,11 @@
</div>
<div class="discord-actions">
<a class="btn discord-btn" href="https://discord.gg/vA8GcdCG" target="_blank" rel="noopener noreferrer">Join Now</a>
<a class="btn discord-btn" href="https://discord.gg/7yUjEVF7uP" target="_blank" rel="noopener noreferrer">Join Now</a>
</div>
</section>
</main>
<script src="app.js"></script>
<script src="app.js" defer></script>
</body>
</html>
+61 -21
View File
@@ -1,47 +1,87 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Lists — GD fedl</title>
<meta name="description" content="Browse the complete FEDL ranked demon list. Search and filter demons by rank, difficulty, and placement. Find your next challenge demon today.">
<meta name="keywords" content="demon list, ranked demons, demon ranking, geometry dash demons, hardest demons">
<title>Complete Ranked Demon List - FEDL Rankings</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="lists">
<header>
<h1>fedl</h1>
<h1>FEDL Ranked Demon List</h1>
<nav>
<a href="index.html">Home</a>
<a href="manage.html">Manage</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="rules.html">Rules</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main class="layout">
<aside class="sidebar">
<div class="panel">
<h2>Levels</h2>
<div class="panel list-sidebar-panel">
<p class="sidebar-kicker">Quick Jump</p>
<h2>Rank ranges</h2>
<p class="muted">Jump straight to a section of the list and narrow things down fast.</p>
<ul id="levels"></ul>
</div>
</aside>
<section class="panel content">
<div class="search-row">
<h2 id="list-title">All Videos</h2>
<input id="search" type="text" placeholder="Search videos or titles..." />
<select id="level-filter"><option value="all">All Levels</option></select>
</div>
<section class="content list-page-shell">
<section class="panel list-hero-panel">
<div class="list-hero-copy">
<p class="hero-kicker">FEDL Rankings</p>
<h2 id="list-title">FEDL List</h2>
<p class="muted list-hero-text">Browse the full ranking, search for a level by name, filter by placement range, and open linked videos directly from the table.</p>
</div>
<div class="list-hero-badge">
<strong>Live list</strong>
<span>Updates pull from the current FEDL data source when available.</span>
</div>
</section>
<div class="table-wrap">
<table class="levels-table">
<thead>
<tr><th style="width:80px">#</th><th>Title</th><th style="width:140px">Action</th></tr>
</thead>
<tbody id="list-area"></tbody>
</table>
</div>
<section class="panel list-controls-panel">
<div class="search-row">
<label class="control-block" for="search">
<span>Search</span>
<input id="search" type="text" placeholder="Search titles..." />
</label>
<label class="control-block control-select" for="level-filter">
<span>Filter</span>
<select id="level-filter"><option value="all">All Levels</option></select>
</label>
</div>
</section>
<section class="panel list-table-panel">
<div class="table-header">
<div>
<p class="status-label">List Table</p>
<h3>Current placements</h3>
</div>
<p class="muted table-hint">Click <strong>Open</strong> to watch the video. With a profile from Roulette, use <strong>My %</strong> to track progress per level (this device only).</p>
</div>
<div class="table-wrap">
<table class="levels-table">
<thead>
<tr><th style="width:80px">#</th><th>Title</th><th style="width:100px">My %</th><th style="width:140px">Action</th></tr>
</thead>
<tbody id="list-area"></tbody>
</table>
</div>
</section>
</section>
</main>
<script src="app.js"></script>
<script src="app.js" defer></script>
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Log in — GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="login">
<header>
<h1>Log in</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main class="auth-main">
<section class="panel auth-panel">
<p class="hero-kicker">FEDL account</p>
<h2>Welcome back</h2>
<p class="muted">Sign in to load your saved roulette pick and per-level progress from the server.</p>
<form id="login-form" class="auth-form">
<label class="admin-password-field" for="login-username">
<span>Username</span>
<input id="login-username" name="username" type="text" autocomplete="username" required />
</label>
<label class="admin-password-field" for="login-password">
<span>Password</span>
<input id="login-password" name="password" type="password" autocomplete="current-password" required />
</label>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAC_RrrNCJVX_wbHb"></div>
<p id="login-status" class="muted" role="status"></p>
<div class="auth-actions">
<button type="submit" class="btn" id="login-submit">Log in</button>
<a class="text-link" href="signup.html">Need an account? Sign up</a>
<a class="text-link" href="reset-password.html">Forgot password?</a>
</div>
</form>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
-89
View File
@@ -1,89 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Manage List - GD fedl</title>
<link rel="stylesheet" href="styles.css">
</head>
<body data-page="manage">
<header>
<h1>Manage FEDL</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="rules.html">Rules</a>
<a href="roulette.html">Roulette</a>
</nav>
</header>
<main>
<section class="panel manage-hero">
<p class="rules-kicker">Node Manager</p>
<h2>Edit the list without touching raw text files</h2>
<p class="muted small">This page saves through the Node server API and keeps both <code>data.json</code> and <code>data.txt</code> in sync.</p>
</section>
<section class="panel manage-grid">
<div>
<h2 id="manage-form-title">Add Entry</h2>
<form id="level-form" class="manage-form">
<input type="hidden" id="level-id" />
<label>
Category
<input id="level-category" type="text" value="new" placeholder="new" />
</label>
<label>
Position
<input id="level-position" type="number" min="1" placeholder="1" required />
</label>
<label>
Title
<input id="level-title" type="text" placeholder="Level title" required />
</label>
<label>
Video URL
<input id="level-url" type="url" placeholder="https://youtube.com/watch?v=example" />
</label>
<div class="manage-actions">
<button class="btn" type="submit" id="level-submit">Save Entry</button>
<button class="btn ghost-btn" type="button" id="level-reset">Clear</button>
</div>
</form>
</div>
<div>
<h2>Quick Notes</h2>
<ul class="rules-list">
<li>Entries are stored by the server in JSON for easier editing and API access.</li>
<li>The legacy text format is still rewritten automatically for compatibility.</li>
<li>Sorting on the public list still follows the numeric position field.</li>
</ul>
<p class="manage-status muted" id="manage-status">Loading current entries from the server.</p>
</div>
</section>
<section class="panel">
<div class="search-row manage-search-row">
<h2>Current Entries</h2>
<input id="manage-search" type="text" placeholder="Search by title or category..." />
</div>
<div class="table-wrap">
<table class="levels-table">
<thead>
<tr>
<th style="width:100px">#</th>
<th style="width:140px">Category</th>
<th>Title</th>
<th style="width:220px">Video</th>
<th style="width:180px">Actions</th>
</tr>
</thead>
<tbody id="manage-list"></tbody>
</table>
</div>
</section>
</main>
<script src="app.js"></script>
</body>
</html>
+318
View File
@@ -0,0 +1,318 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Messages — GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
<style>
.dm-container{display:flex;height:calc(100vh - 120px);gap:20px;margin-top:20px}
.dm-sidebar{width:300px;flex-shrink:0;background:var(--panel);border-radius:12px;padding:16px;overflow-y:auto}
.dm-sidebar h3{margin:0 0 16px 0;font-size:1rem;color:var(--muted)}
.dm-conversation{cursor:pointer;padding:12px;border-radius:8px;margin-bottom:8px;transition:background .2s}
.dm-conversation:hover{background:rgba(255,255,255,0.05)}
.dm-conversation.active{background:rgba(0,170,255,0.15)}
.dm-conversation-name{font-weight:600;color:var(--accent-warm)}
.dm-conversation-preview{font-size:0.85rem;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-conversation-time{font-size:0.75rem;color:var(--muted);margin-top:4px}
.dm-conversation.unread .dm-conversation-name{color:var(--accent)}
.dm-main{flex:1;background:var(--panel);border-radius:12px;display:flex;flex-direction:column}
.dm-header{padding:16px;border-bottom:1px solid rgba(255,255,255,0.1);display:flex;justify-content:space-between;align-items:center}
.dm-header h3{margin:0}
.dm-messages{flex:1;padding:16px;overflow-y:auto;display:flex;flex-direction:column;gap:12px}
.dm-message{padding:12px 16px;border-radius:12px;max-width:70%;word-wrap:break-word}
.dm-message.sent{background:var(--accent);color:var(--bg);align-self:flex-end;border-bottom-right-radius:4px}
.dm-message.received{background:rgba(255,255,255,0.1);align-self:flex-start;border-bottom-left-radius:4px}
.dm-message.system{background:rgba(255,184,77,0.12);border:1px solid rgba(255,184,77,0.25);align-self:stretch;max-width:90%}
.dm-message.system .dm-message-content{white-space:pre-wrap}
.dm-message-time{font-size:0.7rem;opacity:0.7;margin-top:4px}
.dm-input-row{display:flex;gap:12px;padding:16px;border-top:1px solid rgba(255,255,255,0.1)}
.dm-input-row input{flex:1;padding:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.05);color:var(--text);font-size:1rem}
.dm-input-row input:focus{outline:none;border-color:var(--accent)}
.dm-compose{background:var(--panel);padding:20px;border-radius:12px;margin-bottom:20px}
.dm-compose h3{margin:0 0 16px 0}
.dm-compose input,.dm-compose input:focus{width:100%;padding:12px;border-radius:8px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.05);color:var(--text);font-size:1rem;margin-bottom:12px;box-sizing:border-box}
.dm-compose .btn{width:100%}
.dm-user-search{position:relative}
.dm-user-dropdown{position:absolute;top:100%;left:0;right:0;background:var(--panel);border:1px solid rgba(255,255,255,0.1);border-radius:8px;max-height:200px;overflow-y:auto;z-index:100}
.dm-user-dropdown div{padding:10px 12px;cursor:pointer}
.dm-user-dropdown div:hover{background:rgba(255,255,255,0.05)}
.empty-state{text-align:center;padding:40px;color:var(--muted)}
.no-conversation-selected{flex:1;display:flex;align-items:center;justify-content:center;color:var(--muted)}
</style>
</head>
<body data-page="messages">
<header>
<h1>Messages</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
<span class="fedl-auth-nav"></span>
</nav>
</header>
<main>
<div class="dm-compose" id="dmCompose">
<h3>New Message</h3>
<div class="dm-user-search">
<input type="text" id="userSearch" placeholder="Search for a user..." autocomplete="off">
<div class="dm-user-dropdown" id="userDropdown" hidden></div>
</div>
<input type="text" id="selectedUser" placeholder="Selected user" readonly>
<input type="text" id="dmContent" placeholder="Type your message...">
<button type="button" class="btn" id="sendDmBtn">Send Message</button>
</div>
<div class="dm-container">
<div class="dm-sidebar">
<h3>Conversations</h3>
<div id="conversationsList"></div>
</div>
<div class="dm-main">
<div class="dm-header">
<h3 id="chatWithName">Select a conversation</h3>
</div>
<div class="dm-messages" id="messagesList">
<div class="no-conversation-selected">Select a conversation from the sidebar</div>
</div>
<div class="dm-input-row" id="messageInputRow" hidden>
<input type="text" id="messageInput" placeholder="Type a message...">
<button type="button" class="btn" id="sendMessageBtn">Send</button>
</div>
</div>
</div>
</main>
<script src="app.js" defer></script>
<script>
const TESTING_MODE = false;
const BASE = TESTING_MODE ? 'http://127.0.0.1:8090/fedl' : 'https://raspberrypi-1.tail46eacb.ts.net/fedl';
const AUTH_TOKEN_KEY = 'fedl_auth_token';
let currentUser = null;
let currentUserId = null;
let conversations = [];
let selectedConversation = null;
function getToken() {
return localStorage.getItem(AUTH_TOKEN_KEY) || '';
}
function setToken(t) {
if (t) localStorage.setItem(AUTH_TOKEN_KEY, t);
else localStorage.removeItem(AUTH_TOKEN_KEY);
}
function api(path, options = {}) {
const opts = Object.assign({ headers: { 'Content-Type': 'application/json' } }, options);
const tok = getToken();
if (tok) opts.headers['Authorization'] = `Bearer ${tok}`;
if (opts.body && typeof opts.body === 'object') opts.body = JSON.stringify(opts.body);
return fetch(BASE + path, opts).then(r => r.json().then(j => ({ ok: r.ok, status: r.status, data: j })));
}
function escapeHtml(text) {
return String(text || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function formatDate(ts) {
const d = new Date(ts);
const diff = Date.now() - d;
if (diff < 60000) return 'just now';
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
function checkAuth() {
const tok = getToken();
if (!tok) { window.location.href = 'login.html?return=' + encodeURIComponent(window.location.href); return Promise.resolve(null); }
return api('/api/auth/me').then(res => {
if (res.ok && res.data && res.data.userId) {
currentUser = res.data.username;
currentUserId = res.data.userId;
return res.data;
}
return null;
}).catch(() => {
return null;
});
}
function updateUi() {
const nav = document.querySelector('.fedl-auth-nav');
if (nav) {
nav.innerHTML = `<span class="muted">Hi, <strong>${escapeHtml(currentUser)}</strong></span> <a href="login.html" class="btn ghost-btn small-btn" onclick="setToken('');">Log out</a>`;
}
}
function loadConversations() {
api('/api/messages').then(res => {
if (res.ok && res.data && res.data.conversations) {
conversations = res.data.conversations;
renderConversations();
}
});
}
function renderConversations() {
const list = document.getElementById('conversationsList');
const hasSystemMessages = conversations.some(c => c.userId === 'system');
const userConversations = conversations.filter(c => c.userId !== 'system');
let html = '';
if (hasSystemMessages) {
html += `
<div class="dm-conversation system ${selectedConversation === 'system' ? 'active' : ''}" data-user="system" style="border-left: 3px solid var(--accent-warm);">
<div class="dm-conversation-name" style="color: var(--accent-warm);">System Notifications</div>
<div class="dm-conversation-preview">Password resets and alerts</div>
</div>
`;
}
if (!userConversations.length && !hasSystemMessages) {
list.innerHTML = '<div class="empty-state">No conversations yet</div>';
return;
}
html += userConversations.map(c => `
<div class="dm-conversation ${c.unreadCount > 0 ? 'unread' : ''} ${selectedConversation === c.userId ? 'active' : ''}" data-user="${escapeHtml(c.userId)}">
<div class="dm-conversation-name">${escapeHtml(c.username)}</div>
<div class="dm-conversation-preview">${escapeHtml(c.lastMessage.content)}</div>
<div class="dm-conversation-time">${formatDate(c.lastMessage.timestamp)}</div>
</div>
`).join('');
list.innerHTML = html;
list.querySelectorAll('.dm-conversation').forEach(el => {
el.addEventListener('click', () => selectConversation(el.dataset.user));
});
}
function selectConversation(userId) {
selectedConversation = userId;
if (userId === 'system') {
document.getElementById('chatWithName').textContent = 'System Notifications';
document.getElementById('messageInputRow').hidden = true;
loadSystemMessages();
} else {
const conv = conversations.find(c => c.userId === userId);
document.getElementById('chatWithName').textContent = conv ? conv.username : 'Chat';
document.getElementById('messageInputRow').hidden = false;
loadMessages(userId);
}
renderConversations();
}
function loadSystemMessages() {
api('/api/messages/system').then(res => {
if (res.ok && res.data && res.data.messages) {
renderMessages(res.data.messages, true);
}
});
}
function loadMessages(userId) {
api('/api/messages/' + userId).then(res => {
if (res.ok && res.data && res.data.messages) {
renderMessages(res.data.messages);
}
});
}
function renderMessages(messages, isSystem = false) {
const list = document.getElementById('messagesList');
if (!messages.length) {
list.innerHTML = '<div class="empty-state">No messages yet.</div>';
return;
}
list.innerHTML = messages.map(m => {
const isCode = m.type === 'password_reset';
const content = isCode
? `<strong style="color: var(--accent); font-family: monospace; font-size: 1.1em; display: block; margin: 8px 0; padding: 10px; background: rgba(255,255,255,0.1); border-radius: 8px; word-break: break-all;">${escapeHtml(m.content)}</strong>`
: escapeHtml(m.content);
return `
<div class="dm-message ${isSystem ? 'system' : (m.fromUserId === currentUserId ? 'sent' : 'received')}" style="${isSystem ? 'background: rgba(255,184,77,0.15); border: 1px solid rgba(255,184,77,0.3); max-width: 85%;' : ''}">
<div class="dm-message-content">${content}</div>
<div class="dm-message-time">${formatDate(m.timestamp)}</div>
</div>
`;
}).join('');
list.scrollTop = list.scrollHeight;
}
const userSearchInput = document.getElementById('userSearch');
const userDropdown = document.getElementById('userDropdown');
const selectedUserInput = document.getElementById('selectedUser');
userSearchInput.addEventListener('input', async () => {
const query = userSearchInput.value.trim();
if (query.length < 2) {
userDropdown.hidden = true;
return;
}
const res = await api('/api/users/search', { method: 'POST', body: { query } });
if (res.ok && res.data && res.data.results) {
userDropdown.innerHTML = res.data.results.map(u =>
`<div data-user-id="${u.userId}" data-username="${u.username}">${escapeHtml(u.username)}</div>`
).join('');
userDropdown.hidden = false;
userDropdown.querySelectorAll('div').forEach(el => {
el.addEventListener('click', () => {
selectedUserInput.value = el.dataset.userId;
selectedUserInput.dataset.username = el.dataset.username;
userSearchInput.value = el.dataset.username;
userDropdown.hidden = true;
});
});
} else if (res.data && res.data.error) {
console.error('User search error:', res.data.error);
userDropdown.innerHTML = '<div style="padding:10px;color:var(--muted)">Error: ' + res.data.error + '</div>';
userDropdown.hidden = false;
}
});
userSearchInput.addEventListener('blur', () => {
setTimeout(() => userDropdown.hidden = true, 200);
});
document.getElementById('sendDmBtn').addEventListener('click', async () => {
const toUserId = selectedUserInput.value;
const content = document.getElementById('dmContent').value.trim();
if (!toUserId || !content) return;
const res = await api('/api/messages', { method: 'POST', body: { toUserId, content } });
if (res.ok) {
document.getElementById('dmContent').value = '';
selectedUserInput.value = '';
userSearchInput.value = '';
loadConversations();
}
});
document.getElementById('sendMessageBtn').addEventListener('click', async () => {
const content = document.getElementById('messageInput').value.trim();
if (!selectedConversation || !content) return;
const res = await api('/api/messages', { method: 'POST', body: { toUserId: selectedConversation, content } });
if (res.ok) {
document.getElementById('messageInput').value = '';
loadMessages(selectedConversation);
loadConversations();
}
});
document.getElementById('messageInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') document.getElementById('sendMessageBtn').click();
});
checkAuth().then(() => { updateUi(); loadConversations(); });
</script>
</body>
</html>
+98
View File
@@ -0,0 +1,98 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>FEDL Offline</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="offlineindex">
<header>
<h1>FEDL Offline</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="rules.html">Rules</a>
<a href="roulette.html">Roulette</a>
</nav>
</header>
<main>
<section class="home-hero">
<div class="home-copy panel">
<p class="hero-kicker">Offline Mode</p>
<h2>FEDL is using the local list right now.</h2>
<p class="hero-text">Live features are unavailable at the moment, but you can still browse the ranked list from the local <code>data.txt</code> file and keep using the static pages.</p>
<div class="hero-actions">
<a class="btn" href="lists.html">Browse list</a>
<a class="btn ghost-btn" href="rules.html">Read rules</a>
<a class="btn ghost-btn" href="roulette.html">Open roulette</a>
</div>
<div class="hero-stats" aria-label="FEDL offline overview">
<article class="stat-card">
<strong id="hero-total-levels">--</strong>
<span>Levels from local data</span>
</article>
<article class="stat-card">
<strong id="hero-top-entry">--</strong>
<span>Current top entry</span>
</article>
<article class="stat-card">
<strong id="hero-last-slot">Local</strong>
<span>Status</span>
</article>
</div>
</div>
<aside class="hero-stage panel">
<p class="stage-label">Local Snapshot</p>
<h3>Top 10 levels</h3>
<p class="muted">Pulled from the bundled list file so the site still has something useful to show.</p>
<div class="featured-list" id="featured-list">
<article class="featured-card">
<span class="featured-rank">#--</span>
<strong>Loading list data...</strong>
<p>Reading the local list file.</p>
</article>
</div>
</aside>
</section>
<section class="panel list-table-panel">
<div class="table-header">
<div>
<p class="status-label">Local List</p>
<h3>Top 10 from data.txt</h3>
</div>
<p class="muted table-hint">This preview stays available even when live features are down.</p>
</div>
<div class="table-wrap">
<table class="levels-table">
<thead>
<tr><th style="width:110px">Rank</th><th>Title</th></tr>
</thead>
<tbody id="offline-list-area">
<tr><td colspan="2" class="muted">Loading local list...</td></tr>
</tbody>
</table>
</div>
</section>
<section class="panel">
<p class="hero-kicker">Offline Fallback</p>
<h2>Static pages are still available</h2>
<p class="hero-text">You can keep browsing the list, rules, roulette, and other static pages while the live parts of the site are unavailable.</p>
<div class="hero-actions">
<a class="btn" href="lists.html">Browse full list</a>
<a class="btn ghost-btn" href="players.html">View players</a>
<a class="btn ghost-btn" href="rules.html">Read rules</a>
</div>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
+398
View File
@@ -0,0 +1,398 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="FEDL - Browse ranked demons, check demon list rules, play demon games, and submit demon runs. Your hub for the Geometry Dash demon list.">
<meta name="keywords" content="demon list, ranked demons, geometry dash, demon ranking, challenge demon">
<title>FEDL - Ranked Demon List Hub for Geometry Dash</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="onepage">
<header role="banner">
<h1>FEDL</h1>
<nav role="navigation" aria-label="Main navigation">
<a href="#" data-nav="home">Home</a>
<a href="#" data-nav="lists">Lists</a>
<a href="#" data-nav="players">Players</a>
<a href="#" data-nav="run">Submit Run</a>
<a href="#" data-nav="roulette">Roulette</a>
<a href="#" data-nav="guess">Guess Game</a>
<a href="#" data-nav="rules">Rules</a>
<a href="#" data-nav="messages">Messages</a>
<a href="#" data-nav="contact">Contact</a>
<a href="#" data-nav="about">About</a>
</nav>
</header>
<main>
<section id="page-home" class="spa-page">
<section class="home-hero">
<div class="home-copy panel">
<p class="hero-kicker">FEDL Hub</p>
<h2>Track ranked demons, learn submission rules, and master the challenge.</h2>
<p class="hero-text">FEDL is your quick home base for browsing ranked demons, checking submission rules, spinning a roulette challenge, and keeping up with the list without digging through a bunch of scattered pages.</p>
<div class="hero-actions">
<a class="btn" href="#" data-nav="lists">Open The List</a>
<a class="btn" href="signup.html">Sign up</a>
<a class="btn ghost-btn" href="#" data-nav="guess">Play Guess Game</a>
<a class="btn ghost-btn" href="#" data-nav="run">Submit A Run</a>
<a class="btn ghost-btn" href="#" data-nav="rules">Submission Rules</a>
<a class="btn ghost-btn" href="#" data-nav="roulette">Spin Roulette</a>
</div>
<div class="hero-stats" aria-label="FEDL overview">
<article class="stat-card">
<strong id="hero-total-levels">--</strong>
<span>Ranked Levels</span>
</article>
<article class="stat-card">
<strong id="hero-top-entry">--</strong>
<span>#1 Demon</span>
</article>
<article class="stat-card">
<strong id="hero-last-slot">--</strong>
<span>Verified Runs</span>
</article>
<article class="stat-card">
<strong id="hero-total-players">--</strong>
<span>Top Player</span>
</article>
<article class="stat-card">
<strong id="hero-recent-runs">--</strong>
<span>Levels Added This Week</span>
</article>
<article class="stat-card">
<strong id="hero-list-updated">--</strong>
<span>Last Updated</span>
</article>
</div>
</div>
<aside class="hero-stage panel">
<p class="stage-label">Live Snapshot</p>
<h3>Top 10 levels</h3>
<p class="muted">A fast preview of what is waiting inside the list page right now.</p>
<div class="featured-list" id="featured-list">
<article class="featured-card skeleton-card">
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
<strong class="skeleton skeleton-title"></strong>
<p class="skeleton skeleton-text"></p>
</article>
<article class="featured-card skeleton-card">
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
<strong class="skeleton skeleton-title"></strong>
<p class="skeleton skeleton-text"></p>
</article>
<article class="featured-card skeleton-card">
<span class="featured-rank skeleton" style="width:40px;height:24px"></span>
<strong class="skeleton skeleton-title"></strong>
<p class="skeleton skeleton-text"></p>
</article>
</div>
</aside>
</section>
<section class="home-section-grid">
<article class="panel info-panel">
<p class="status-label">Browse Faster</p>
<h3>The list page is built for quick checking</h3>
<p class="muted">Search by title, jump between rank ranges, and open completion videos without leaving the site.</p>
<a class="text-link" href="#" data-nav="lists">Go to lists</a>
</article>
<article class="panel info-panel">
<p class="status-label">Stay Legit</p>
<h3>The rules page explains what submissions need</h3>
<p class="muted">Use it to review raw footage requirements, mod rules, skip policies, and approval expectations before you submit.</p>
<a class="text-link" href="#" data-nav="rules">Read the rules</a>
</article>
<article class="panel info-panel">
<p class="status-label">Send Proof</p>
<h3>The run page sends completions into the live queue</h3>
<p class="muted">Drop in your video, add raw footage and notes, and let the admin panel pick it up for review.</p>
<a class="text-link" href="#" data-nav="run">Open submissions</a>
</article>
</section>
<section class="panel spotlight-panel">
<div class="spotlight-copy">
<p class="status-label">Start Here</p>
<h2>What you can do on FEDL</h2>
<p class="spotlight-text">This site is meant to be simple: find ranked demons, watch linked runs, check moderation expectations, and get back to playing without unnecessary clutter.</p>
</div>
<div class="spotlight-grid">
<article class="spotlight-card">
<h3>Lists</h3>
<p>Browse the ranking, filter by placement ranges, and watch linked videos from each entry.</p>
</article>
<article class="spotlight-card">
<h3>Rules</h3>
<p>Check completion requirements, accepted tools, and what moderators expect from a valid run.</p>
</article>
<article class="spotlight-card">
<h3>Guess Game</h3>
<p>See a random level name, pick a mode, and guess its rank with higher or lower hints.</p>
</article>
<article class="spotlight-card">
<h3>Roulette</h3>
<p>Pull a random demon with rank and level ID details for your next roulette attempt.</p>
</article>
</div>
</section>
<section class="panel discord-panel download-panel">
<div class="discord-copy">
<div class="discord-badge">
<span class="discord-logo" aria-hidden="true">📱</span>
<div>
<p class="discord-label">Download</p>
<h2>Geode Mod (Coming Soon)</h2>
</div>
</div>
<p class="discord-text">Geode Mod is a dedicated FEDL companion mod for Geometry Dash demon players with quick list access, roulette tools, queue tracking, and built-in validation help.</p>
<ul class="feature-bullet-list">
<li>Fast access to the full ranked demon list with real-time updates</li>
<li>Roulette generator with saved pick history</li>
<li>Submission queue tracking with status markers and moderator notes</li>
<li>Integrated rules checklist and run validation tips</li>
</ul>
<p class="discord-text discord-footnote">Launching soon for Geode, mobile, and desktop.</p>
</div>
<div class="discord-actions">
<button class="btn discord-btn" disabled>Coming soon</button>
</div>
</section>
<section class="panel discord-panel">
<div class="discord-copy">
<div class="discord-badge">
<span class="discord-logo" aria-hidden="true">📱</span>
<div>
<p class="discord-label">Update</p>
<h2>App Coming Soon</h2>
</div>
</div>
<p class="discord-text">A dedicated FEDL app is on the way. Stay tuned for updates.</p>
</div>
</section>
<section class="panel discord-panel">
<div class="discord-copy">
<div class="discord-badge">
<span class="discord-logo" aria-hidden="true">
<img src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d7f4ef6498ac018f2c55_Symbol.svg" alt="Discord logo" loading="lazy" />
</span>
<div>
<p class="discord-label">Community</p>
<h2>Join Our Discord</h2>
</div>
</div>
<p class="discord-text">Join the FEDL Discord to ask about rules, talk about placements, share progress, report problems, and get help from moderators and other players.</p>
<div class="discord-highlights">
<span>Rule help</span>
<span>Level discussion</span>
<span>Moderator support</span>
</div>
</div>
<div class="discord-actions">
<a class="btn discord-btn" href="https://discord.gg/5HyUKQrN" target="_blank" rel="noopener noreferrer">Join Now</a>
</div>
</section>
</section>
<section id="page-lists" class="spa-page" hidden>
<section class="panel">
<div class="list-controls">
<div class="search-row">
<input id="list-search" type="text" placeholder="Search by title..." aria-label="Search list" />
<button id="list-add-row" type="button" class="btn ghost-btn" hidden>Add Level</button>
<button id="list-save-btn" type="button" class="btn" hidden>Save</button>
</div>
<div class="list-jump">
<button type="button" class="btn ghost-btn small-btn" data-jump="1">1-25</button>
<button type="button" class="btn ghost-btn small-btn" data-jump="26">26-50</button>
<button type="button" class="btn ghost-btn small-btn" data-jump="51">51-75</button>
<button type="button" class="btn ghost-btn small-btn" data-jump="76">76-100</button>
<button type="button" class="btn ghost-btn small-btn" data-jump="100">100+</button>
</div>
</div>
<div class="table-wrap">
<table class="levels-table" id="list-table">
<thead>
<tr>
<th style="width:80px">Rank</th>
<th style="width:180px">ID</th>
<th>Title</th>
<th style="width:200px">Video</th>
</tr>
</thead>
<tbody id="list-body"></tbody>
</table>
</div>
</section>
</section>
<section id="page-players" class="spa-page" hidden>
<section class="panel">
<div class="list-controls">
<div class="search-row">
<input id="player-search" type="text" placeholder="Search players..." aria-label="Search players" />
<select id="player-filter">
<option value="all">All players</option>
</select>
</div>
</div>
<div class="table-wrap">
<table class="levels-table">
<thead>
<tr>
<th>Name</th>
<th style="width:100px">Runs</th>
<th style="width:120px">Best</th>
<th style="width:100px">Points</th>
<th>Top Levels</th>
</tr>
</thead>
<tbody id="players-body"></tbody>
</table>
</div>
</section>
</section>
<section id="page-run" class="spa-page" hidden>
<section class="panel">
<h2>Submit a Run</h2>
<p class="muted">Submit your completion to be reviewed by a moderator.</p>
<form id="run-form" class="run-form">
<label class="admin-password-field">
<span>Player Name</span>
<input id="run-player-name" type="text" placeholder="Who completed this?" required />
</label>
<label class="admin-password-field">
<span>Level Name</span>
<input id="run-level-title" type="text" placeholder="Exact name from the list" required />
</label>
<label class="admin-password-field">
<span>Video URL</span>
<input id="run-video-url" type="url" placeholder="YouTube or video link" required />
</label>
<label class="admin-password-field">
<span>Percent</span>
<input id="run-percent" type="number" min="1" max="100" value="100" required />
<span class="muted small">% (100 for 100%, 80 for 80%)</span>
</label>
<label class="admin-password-field">
<span>Raw Footage URL (optional)</span>
<input id="run-raw-url" type="url" placeholder="Link to raw gameplay if different" />
</label>
<label class="admin-password-field">
<span>Notes (optional)</span>
<textarea id="run-notes" placeholder="Any additional info for moderators"></textarea>
</label>
<p id="run-form-status" class="muted"></p>
<button type="submit" class="btn">Submit Run</button>
</form>
<div id="run-list-section">
<h3>Your Submissions</h3>
<div id="run-list"></div>
</div>
</section>
</section>
<section id="page-roulette" class="spa-page" hidden>
<section class="panel roulette-panel">
<h2>Roulette</h2>
<p class="muted">Spin the wheel for a random demon challenge.</p>
<div id="roulette-result" class="roulette-result">
<p class="muted">Click spin to get a random demon.</p>
</div>
<button id="roulette-spin" type="button" class="btn">Spin</button>
</section>
</section>
<section id="page-guess" class="spa-page" hidden>
<section class="panel">
<h2>Guess the Rank</h2>
<p class="muted">A random level name will appear. Guess if it's ranked higher or lower!</p>
<div id="guess-game">
<p id="guess-level" class="guess-level">???</p>
<div class="guess-buttons">
<button id="guess-higher" class="btn">Higher ▲</button>
<button id="guess-lower" class="btn">Lower ▼</button>
</div>
<p id="guess-result" class="muted"></p>
<p id="guess-score">Score: 0</p>
</div>
</section>
</section>
<section id="page-rules" class="spa-page" hidden>
<section class="panel">
<h2>Submission Rules</h2>
<div class="rules-content">
<h3>Requirements</h3>
<ul>
<li>Video must show full completion from start to finish</li>
<li>Raw footage must be available upon request</li>
<li>No cheats, hacks, or glitches allowed</li>
<li>Level must be on the ranked list</li>
</ul>
<h3>Verification Process</h3>
<ol>
<li>Submit your run with video link</li>
<li>Wait for moderator review</li>
<li>Provide raw footage if requested</li>
<li>Run gets approved or rejected with notes</li>
</ol>
</div>
</section>
</section>
<section id="page-messages" class="spa-page" hidden>
<section class="panel">
<h2>Messages</h2>
<p class="muted">Password resets and system messages appear here.</p>
<div id="messages-list">
<p class="muted">No messages.</p>
</div>
</section>
</section>
<section id="page-contact" class="spa-page" hidden>
<section class="panel">
<h2>Contact</h2>
<form id="contact-form">
<label class="admin-password-field">
<span>Subject</span>
<input id="contact-subject" type="text" required />
</label>
<label class="admin-password-field">
<span>Message</span>
<textarea id="contact-message" required></textarea>
</label>
<button type="submit" class="btn">Send</button>
</form>
</section>
</section>
<section id="page-about" class="spa-page" hidden>
<section class="panel">
<h2>About FEDL</h2>
<p>FEDL is a Geometry Dash demon list hub. Browse ranked demons, submit runs, and track your progress.</p>
<p class="muted">Version 1.0</p>
</section>
</section>
</main>
<script src="onepage.js"></script>
</body>
</html>
+297
View File
@@ -0,0 +1,297 @@
// One Page App - FEDL
(function(){
function qs(id){return document.getElementById(id)}
function escapeHtml(str){return String(str||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function escapeAttr(str){return escapeHtml(str).replace(/'/g,'&#39;');}
const page = document.body.dataset.page;
const isFileProtocol = window.location.protocol === 'file:';
const TESTING_MODE = false;
const liveServerBase = TESTING_MODE ? 'http://127.0.0.1:8090/fedl' : 'https://server.fedl.site/fedl';
const canUseLiveServer = !isFileProtocol || !!liveServerBase;
const liveApiUrl = `${liveServerBase}/api/list`;
const liveRunsUrl = `${liveServerBase}/api/runs`;
const liveDataFileUrl = `${liveServerBase}/server/data.txt`;
function liveApiPath(path){
const p = String(path || '').startsWith('/') ? path : `/${path}`;
return `${liveServerBase}${p}`;
}
let cachedItems = null;
let cachedRuns = null;
function loadItems(){
if(cachedItems) return Promise.resolve(cachedItems);
return fetch(liveApiUrl, {cache:'no-store'}).then(r=>r.text()).then(txt=>{
cachedItems = txt.split(/\r?\n/).map(l=>l.trim()).filter(Boolean).map(l=>{
const parts = l.split('|').map(p=>p.trim());
return {level:parts[0]||'',position:parts[1]||'',title:parts[2]||'',url:parts[3]||''};
});
return cachedItems;
}).catch(()=>[]);
}
function loadRuns(){
if(cachedRuns) return Promise.resolve(cachedRuns);
return fetch(liveRunsUrl, {cache:'no-store'}).then(r=>r.json()).then(data=>{
cachedRuns = Array.isArray(data.items) ? data.items : [];
return cachedRuns;
}).catch(()=>[]);
}
function calculatePoints(rank){
if(rank<1||rank>1000) return 0;
if(rank===1) return 100;
if(rank<=10) return 90-rank+1;
if(rank<=25) return 80-rank+1;
if(rank<=50) return 60-rank+1;
if(rank<=100) return 40-rank+1;
if(rank<=200) return 20-rank+1;
return 1;
}
const spaPage = window.location.hash.slice(1) || 'home';
const pages = document.querySelectorAll('.spa-page');
function showPage(pageName){
pages.forEach(p => p.hidden = true);
const pageEl = document.getElementById('page-' + pageName);
if(pageEl) pageEl.hidden = false;
history.replaceState(null, '', '#' + pageName);
loadPageContent(pageName);
}
function loadPageContent(pageName){
if(pageName === 'lists' || pageName === 'home'){
const listBody = qs('list-body');
if(listBody && !listBody.dataset.loaded){
listBody.dataset.loaded = 'true';
loadItems().then(items=>{
const ranked = items.slice().sort((a,b)=>(Number(a.position)||0)-(Number(b.position)||0));
renderListTable(ranked);
if(qs('hero-total-levels')){
qs('hero-total-levels').textContent = String(ranked.length);
qs('hero-top-entry').textContent = ranked[0]?.title || 'Unavailable';
}
});
}
}
if(pageName === 'players'){
const playersBody = qs('players-body');
const searchEl = qs('player-search');
if(playersBody && searchEl && !playersBody.dataset.loaded){
playersBody.dataset.loaded = 'true';
Promise.all([loadRuns(), loadItems()]).then(([runs, items])=>{
const lookup = new Map(items.map(item => [String(item.title || '').toLowerCase(), Number(item.position) || 9999]));
const map = new Map();
runs.filter(run => String(run.status || '').toLowerCase() === 'approved').forEach(run => {
const playerName = String(run.playerName || '').trim();
if(!playerName) return;
const key = playerName.toLowerCase();
let entry = map.get(key);
if(!entry){
entry = {name: playerName, runs: 0, bestRank: 9999, points: 0, topLevels: new Set()};
map.set(key, entry);
}
entry.runs += 1;
const rank = lookup.get(String(run.levelTitle || '').toLowerCase()) || 9999;
if(rank > 0 && rank < entry.bestRank) entry.bestRank = rank;
if(rank > 0 && rank < 1000) entry.points += calculatePoints(rank);
if(run.levelTitle) entry.topLevels.add(String(run.levelTitle).trim());
});
const players = Array.from(map.values()).map(entry => ({
name: entry.name,
runs: entry.runs,
bestRank: entry.bestRank === 9999 ? '—' : `#${entry.bestRank}`,
points: entry.points,
topLevels: Array.from(entry.topLevels).slice(0, 3).join(', ')
})).sort((a,b) => b.points - a.points || a.name.localeCompare(b.name));
function render(){
const query = (searchEl.value || '').toLowerCase();
const filtered = players.filter(p => !query || p.name.toLowerCase().includes(query));
if(!filtered.length){
playersBody.innerHTML = '<tr><td colspan="5" class="muted">No player data found.</td></tr>';
return;
}
playersBody.innerHTML = filtered.slice(0, 100).map(p => `
<tr>
<td><strong>${escapeHtml(p.name)}</strong></td>
<td>${p.runs}</td>
<td>${p.bestRank}</td>
<td>${p.points}</td>
<td>${escapeHtml(p.topLevels)}</td>
</tr>
`).join('');
}
searchEl.addEventListener('input', render);
render();
});
}
}
if(pageName === 'run'){
const runForm = qs('run-form');
if(runForm && !runForm.dataset.loaded){
runForm.dataset.loaded = 'true';
const formStatusEl = qs('run-form-status');
runForm.addEventListener('submit', function(e){
e.preventDefault();
const playerName = qs('run-player-name').value.trim();
const levelTitle = qs('run-level-title').value.trim();
const videoUrl = qs('run-video-url').value.trim();
const percent = qs('run-percent').value.trim();
const rawUrl = qs('run-raw-url').value.trim();
const notes = qs('run-notes').value.trim();
if(!playerName || !levelTitle || !videoUrl || !percent){
formStatusEl.textContent = 'Please fill in all required fields.';
return;
}
formStatusEl.textContent = 'Submitting...';
fetch(liveRunsUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ playerName, levelTitle, videoUrl, percent, rawFootageUrl: rawUrl, notes })
}).then(r => {
if(!r.ok) throw new Error('Failed');
formStatusEl.textContent = 'Run submitted successfully!';
runForm.reset();
}).catch(() => {
formStatusEl.textContent = 'Failed to submit run.';
});
});
}
}
if(pageName === 'roulette'){
const spinBtn = qs('roulette-spin');
const resultEl = qs('roulette-result');
if(spinBtn && resultEl && !spinBtn.dataset.loaded){
spinBtn.dataset.loaded = 'true';
spinBtn.addEventListener('click', function(){
loadItems().then(items => {
if(!items.length){
resultEl.innerHTML = '<p class="muted">No levels loaded.</p>';
return;
}
const random = items[Math.floor(Math.random() * items.length)];
resultEl.innerHTML = `
<p><strong>${escapeHtml(random.title || 'Untitled')}</strong></p>
<p class="muted">Rank #${random.position || '?'}</p>
<p class="muted">${random.level || ''}</p>
`;
});
});
}
}
if(pageName === 'guess'){
const higherBtn = qs('guess-higher');
const lowerBtn = qs('guess-lower');
const levelEl = qs('guess-level');
const resultEl = qs('guess-result');
const scoreEl = qs('guess-score');
if(higherBtn && lowerBtn && levelEl && !higherBtn.dataset.loaded){
higherBtn.dataset.loaded = 'true';
let currentLevel = null;
let score = 0;
let revealed = false;
function newRound(){
loadItems().then(items => {
if(!items.length) return;
currentLevel = items[Math.floor(Math.random() * items.length)];
levelEl.textContent = currentLevel.title || '???';
resultEl.textContent = '';
revealed = false;
});
}
higherBtn.addEventListener('click', function(){
if(revealed || !currentLevel) return;
revealed = true;
const rank = Number(currentLevel.position) || 9999;
const actual = Math.random() * 100;
if(actual > 50){
score++;
resultEl.textContent = `Correct! It was #${rank}`;
}else{
resultEl.textContent = `Wrong! It was #${rank}`;
}
scoreEl.textContent = `Score: ${score}`;
setTimeout(newRound, 2000);
});
lowerBtn.addEventListener('click', function(){
if(revealed || !currentLevel) return;
revealed = true;
const rank = Number(currentLevel.position) || 9999;
const actual = Math.random() * 100;
if(actual < 50){
score++;
resultEl.textContent = `Correct! It was #${rank}`;
}else{
resultEl.textContent = `Wrong! It was #${rank}`;
}
scoreEl.textContent = `Score: ${score}`;
setTimeout(newRound, 2000);
});
newRound();
}
}
}
function renderListTable(items){
const tbody = qs('list-body');
if(!tbody) return;
const searchInput = qs('list-search');
const query = searchInput ? searchInput.value.toLowerCase() : '';
const filtered = items.filter(item => !query || (item.title||'').toLowerCase().includes(query) || (item.level||'').toLowerCase().includes(query));
if(!filtered.length){
tbody.innerHTML = '<tr><td colspan="4" class="muted">No levels found.</td></tr>';
return;
}
tbody.innerHTML = filtered.map(item => `
<tr>
<td>#${escapeHtml(item.position || '')}</td>
<td>${escapeHtml(item.level || '')}</td>
<td>${escapeHtml(item.title || '')}</td>
<td>${item.url ? `<a href="${escapeAttr(item.url)}" target="_blank">Video</a>` : ''}</td>
</tr>
`).join('');
document.querySelectorAll('.skeleton-card').forEach(el => el.remove());
const featuredList = qs('featured-list');
if(featuredList){
const top10 = items.slice(0, 10);
featuredList.innerHTML = top10.map(item => `
<article class="featured-card">
<span class="featured-rank">#${escapeHtml(item.position || '--')}</span>
<strong>${escapeHtml(item.title || 'Untitled')}</strong>
<p>${item.url ? 'Video link ready.' : 'No video linked.'}</p>
</article>
`).join('');
}
}
// Initialize
const navLinks = document.querySelectorAll('nav a[data-nav]');
navLinks.forEach(link => {
link.addEventListener('click', function(e){
e.preventDefault();
showPage(this.dataset.nav);
});
});
if(spaPage && document.getElementById('page-' + spaPage)){
showPage(spaPage);
}else{
showPage('home');
}
// Search listener
const listSearch = qs('list-search');
if(listSearch){
listSearch.addEventListener('input', ()=>{
loadItems().then(items=>{
const ranked = items.slice().sort((a,b)=>(Number(a.position)||0)-(Number(b.position)||0));
renderListTable(ranked);
});
});
}
})();
-9
View File
@@ -1,9 +0,0 @@
{
"name": "gd-fedl",
"version": "1.0.0",
"private": true,
"description": "Node-powered FEDL list manager",
"scripts": {
"start": "node server.js"
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

+63 -14
View File
@@ -1,36 +1,85 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Players — GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="players">
<header>
<h1>Players</h1>
<nav>
<a href="index.html">Home</a>
<a href="manage.html">Manage</a>
<a href="lists.html">Lists</a>
<a href="rules.html">Rules</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main>
<section class="panel">
<h2>Add Player</h2>
<form id="player-form" style="display:flex;gap:8px">
<input id="player-name" placeholder="Player name" required />
<button class="btn" type="submit">Add</button>
</form>
</section>
<main class="layout">
<aside class="sidebar">
<div class="panel list-sidebar-panel">
<p class="sidebar-kicker">Quick jump</p>
<h2>Player groups</h2>
<p class="muted">Jump to players by first letter.</p>
<ul id="player-groups"></ul>
</div>
</aside>
<section class="panel">
<h2>Players</h2>
<ul id="players"></ul>
<section class="content list-page-shell">
<section class="panel list-hero-panel">
<div class="list-hero-copy">
<p class="hero-kicker">Player tracker</p>
<h2 id="list-title">Local player roster</h2>
<p class="muted list-hero-text">Manage your player list in browser storage with the same searchable table experience as the FEDL demonlist UI.</p>
</div>
<div class="list-hero-badge">
<strong>Local only</strong>
<span>Player names are stored in this browser only.</span>
</div>
</section>
<section class="panel list-controls-panel">
<div class="search-row">
<label class="control-block" for="search">
<span>Search</span>
<input id="search" type="text" placeholder="Search player names..." />
</label>
<label class="control-block control-select" for="group-filter">
<span>Filter</span>
<select id="group-filter"><option value="all">All players</option></select>
</label>
</div>
</section>
<section class="panel list-table-panel">
<div class="table-header">
<div>
<p class="status-label">Player table</p>
<h3>Saved players</h3>
</div>
<p class="muted table-hint">Use the search box or letter filter to locate players quickly.</p>
</div>
<div class="table-wrap">
<table class="levels-table">
<thead>
<tr><th>Name</th><th style="width:80px">Runs</th><th style="width:80px">Points</th><th style="width:100px">Best rank</th><th style="width:180px">Top levels</th></tr>
</thead>
<tbody id="players-area"></tbody>
</table>
</div>
</section>
</section>
</main>
<script src="app.js"></script>
<script src="app.js" defer></script>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Reset password — GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="reset-password">
<header>
<h1>Reset password</h1>
<nav>
<a href="index.html">Home</a>
<a href="login.html">Log in</a>
<a href="signup.html">Sign up</a>
<a href="account.html">Account</a>
<a href="contact.html">Contact</a>
</nav>
</header>
<main class="account-main">
<section class="panel account-panel">
<p class="hero-kicker">FEDL account</p>
<h2>Request a reset code</h2>
<p class="muted">Enter your username to get a reset code sent to your messages.</p>
<form id="reset-request-form" class="auth-form">
<label class="admin-password-field" for="reset-identifier">
<span>Username</span>
<input id="reset-identifier" name="identifier" type="text" autocomplete="username" required />
</label>
<p id="reset-request-status" class="muted" role="status"></p>
<div class="auth-actions">
<button type="submit" class="btn" id="reset-request-submit">Get reset code</button>
</div>
</form>
</section>
<section id="reset-section" class="panel account-panel" style="display:none;">
<h2>Enter your reset code</h2>
<p class="muted">Enter the reset code you received above.</p>
<div id="reset-code-display" style="display:none;"></div>
<form id="reset-password-form" class="auth-form">
<label class="admin-password-field" for="reset-token">
<span>Reset code</span>
<input id="reset-token" name="token" type="text" autocomplete="off" required />
</label>
<label class="admin-password-field" for="reset-new-password">
<span>New password</span>
<input id="reset-new-password" name="newPassword" type="password" autocomplete="new-password" required minlength="8" />
</label>
<label class="admin-password-field" for="reset-confirm-password">
<span>Confirm new password</span>
<input id="reset-confirm-password" name="confirmPassword" type="password" autocomplete="new-password" required minlength="8" />
</label>
<p id="reset-password-status" class="muted" role="status"></p>
<div class="auth-actions">
<button type="submit" class="btn" id="reset-password-submit">Set new password</button>
</div>
</form>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
# Allow all pages on fedl.site
User-agent: *
Allow: /
# Subdomains should have their own robots.txt files
# to define their crawling policies
+105 -31
View File
@@ -1,57 +1,131 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Demon Roulette - GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="roulette">
<header>
<h1>Demon Roulette</h1>
<nav>
<a href="index.html">Home</a>
<a href="manage.html">Manage</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main>
<section class="panel roulette-rules">
<p class="roulette-kicker">Rules</p>
<h2>How Roulette Works</h2>
<ul class="rules-list">
<li>Extreme Demon Roulette is a community challenge built around a roulette of hard demons from a chosen list.</li>
<li>You can use a main list, extended list, legacy list, or a custom list like this FEDL list.</li>
<li>A common format is to start at <strong>1%</strong> and raise the goal by <strong>1%</strong> for each new level until you fail or finish the challenge.</li>
<li>Some versions allow skips, rerolls, or custom limits, depending on the rules you want to use.</li>
<li>This page shows the demon name, rank, and level ID for the selected result.</li>
<li>Percent targets are stored in <code>level-ids.txt</code> using the format <code>title|levelId|percent</code>.</li>
<li>If a level ID is missing in that file, this page will try to look it up through the Geometry Dash community API automatically.</li>
</ul>
</section>
<section class="panel roulette-panel">
<div class="roulette-head">
<div>
<p class="roulette-kicker">Random Challenge</p>
<h2>Demon Roulette</h2>
<p class="muted">Spin the FEDL list to get a random demon for your roulette run.</p>
<section class="roulette-shell">
<div class="roulette-main-stack">
<section class="panel roulette-account-panel">
<p class="roulette-kicker">Your profile</p>
<h2 class="roulette-account-heading">Save roulette &amp; progress</h2>
<p class="muted small roulette-account-intro">Profiles are stored only in this browser. Each profile remembers your last roulette demon and a progress % per level (also shown on the list page).</p>
<p class="muted small roulette-login-sync-hint" id="roulette-login-sync-hint" hidden>
<a href="login.html">Log in</a> or <a href="signup.html">sign up</a> to sync roulette, save slots, and % across browsers.
</p>
<div class="roulette-slots-block" id="roulette-slots-block">
<p class="roulette-slots-heading">Roulette save slots</p>
<p class="muted small" id="roulette-slots-hint">Use three slots to park different demons. Requires an active profile or a signed-in account.</p>
<div class="roulette-slot-rows">
<div class="roulette-slot-row">
<span class="roulette-slot-title">Slot 1</span>
<span class="roulette-slot-label muted" id="roulette-slot-label-1">Empty</span>
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-save-1">Save</button>
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-load-1">Load</button>
</div>
<div class="roulette-slot-row">
<span class="roulette-slot-title">Slot 2</span>
<span class="roulette-slot-label muted" id="roulette-slot-label-2">Empty</span>
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-save-2">Save</button>
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-load-2">Load</button>
</div>
<div class="roulette-slot-row">
<span class="roulette-slot-title">Slot 3</span>
<span class="roulette-slot-label muted" id="roulette-slot-label-3">Empty</span>
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-save-3">Save</button>
<button type="button" class="btn ghost-btn small-btn" id="roulette-slot-load-3">Load</button>
</div>
</div>
</div>
<button class="btn roulette-btn" id="roulette-spin" type="button">Spin</button>
<div class="roulette-account-controls">
<label class="roulette-account-label" for="roulette-account-select">Active profile</label>
<select id="roulette-account-select" class="roulette-account-select"></select>
<div class="roulette-account-create-row">
<input id="roulette-account-new" type="text" placeholder="New profile name" autocomplete="username" />
<button type="button" class="btn ghost-btn" id="roulette-account-create">Create</button>
</div>
</div>
<button type="button" class="btn ghost-btn roulette-restore-btn" id="roulette-restore" hidden>Restore last spin</button>
</section>
<section class="panel roulette-panel">
<div class="roulette-head">
<div>
<p class="roulette-kicker">Random Challenge</p>
<h2>Demon Roulette</h2>
<p class="muted roulette-intro">Spin the FEDL list to get a random demon for your next roulette run, practice grind, or bad decision.</p>
</div>
<button class="btn roulette-btn" id="roulette-spin" type="button">Spin</button>
</div>
<div class="roulette-card" id="roulette-card">
<p class="roulette-status muted" id="roulette-status">Press spin to choose a demon.</p>
<h3 id="roulette-title">No demon selected</h3>
<p class="roulette-rank" id="roulette-rank">Rank: -</p>
<p class="roulette-meta" id="roulette-level-id">Level ID: -</p>
<p class="roulette-note muted" id="roulette-note">Level IDs come from your local file first, then API lookup if needed.</p>
<div class="roulette-progress-block" id="roulette-progress-row" hidden>
<label class="roulette-progress-label" for="roulette-percent">Your % on this level</label>
<div class="roulette-progress-input-row">
<input id="roulette-percent" type="text" inputmode="decimal" placeholder="e.g. 47" autocomplete="off" />
<span class="muted">%</span>
<button type="button" class="btn small-btn" id="roulette-percent-submit">Submit %</button>
</div>
<p id="roulette-percent-hint" class="muted small roulette-percent-hint"></p>
</div>
<a class="btn roulette-open" id="roulette-open" href="#" target="_blank" rel="noopener noreferrer" hidden>View Video</a>
</div>
</section>
</div>
<div class="roulette-card" id="roulette-card">
<p class="roulette-status muted" id="roulette-status">Press spin to choose a demon.</p>
<h3 id="roulette-title">No demon selected</h3>
<p class="roulette-rank" id="roulette-rank">Rank: -</p>
<p class="roulette-meta" id="roulette-level-id">Level ID: -</p>
<p class="roulette-note muted" id="roulette-note">Level IDs come from your local file first, then API lookup if needed.</p>
<a class="btn roulette-open" id="roulette-open" href="#" target="_blank" rel="noopener noreferrer" hidden>Open Video</a>
</div>
<aside class="panel roulette-rules-card">
<p class="roulette-kicker">How It Works</p>
<h2>Quick rules</h2>
<div class="roulette-rule-list">
<article>
<strong>Pick from a list</strong>
<p>Use FEDL or any demon list format you want for your roulette session.</p>
</article>
<article>
<strong>Raise the target</strong>
<p>A common setup starts at 1% and increases by 1% after each new level.</p>
</article>
<article>
<strong>Use your own rules</strong>
<p>Skips, rerolls, and loss conditions can be adjusted to match your challenge format.</p>
</article>
<article>
<strong>Extra data included</strong>
<p>This page shows rank and level ID, and it tries API lookup when local info is missing.</p>
</article>
<article>
<strong>Want another mode?</strong>
<p>Open the separate Guess Game page if you want to figure out a level's rank with higher or lower hints.</p>
</article>
</div>
</aside>
</section>
</main>
<script src="app.js"></script>
<script src="app.js" defer></script>
</body>
</html>
+9 -1
View File
@@ -1,20 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Rules - GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body class="rules-view">
<script src="app.js" defer></script>
<header>
<h1>FEDL Rules</h1>
<nav>
<a href="index.html">Home</a>
<a href="manage.html">Manage</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main>
+127
View File
@@ -0,0 +1,127 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Submit Run - GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="run">
<header>
<h1>Submit Run</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main>
<section class="run-shell">
<section class="panel run-panel">
<div class="run-head">
<div>
<p class="hero-kicker">FEDL Submission</p>
<h2>Send in a run</h2>
<p class="muted run-intro">Submit your completion video, attach raw footage if you have it hosted, and leave notes for the moderators. If you <a href="login.html">log in</a>, you can save multiple runs to your account (drafts) and queue submissions will show your username to moderators.</p>
</div>
<div class="run-head-badge">
<strong>Live queue</strong>
<span>Submissions land in the admin panel instantly when the Node server is running.</span>
</div>
</div>
<form id="run-form" class="run-form">
<label class="control-block" for="run-player-name">
<span>Player Name</span>
<input id="run-player-name" name="playerName" type="text" placeholder="Your in-game name" required />
<span class="muted small">When you are logged in, your account username is filled here automatically (you can change it). Starting in 2027 you will need an account to submit a run.</span>
</label>
<label class="control-block" for="run-level-title">
<span>Level</span>
<input id="run-level-title" name="levelTitle" type="text" list="run-level-options" placeholder="Level title" required />
<datalist id="run-level-options"></datalist>
</label>
<label class="control-block" for="run-video-url">
<span>Completion Video URL</span>
<input id="run-video-url" name="videoUrl" type="url" placeholder="https://youtu.be/..." required />
</label>
<label class="control-block" for="run-percent">
<span>Percent</span>
<input id="run-percent" name="percent" type="number" min="1" max="100" placeholder="100" required />
</label>
<label class="control-block" for="run-raw-footage-url">
<span>Raw Footage URL</span>
<input id="run-raw-footage-url" name="rawFootageUrl" type="url" placeholder="Optional raw footage link" />
</label>
<label class="control-block run-notes-block" for="run-notes">
<span>Notes For Mods</span>
<textarea id="run-notes" name="notes" rows="6" placeholder="Mention handcam, custom copy info, bug fix approval, or anything the moderators should know."></textarea>
</label>
<div class="run-actions">
<button id="run-submit" type="submit" class="btn">Submit Run</button>
<button type="button" id="run-save-account" class="btn ghost-btn" hidden>Save to my account</button>
<p id="run-form-status" class="muted">The server needs to be running for live submissions.</p>
</div>
</form>
</section>
<aside class="panel run-side-panel">
<p class="status-label">Checklist</p>
<h2>Before you submit</h2>
<div class="run-checklist">
<article>
<strong>Completion video</strong>
<p>Make sure the completion link is public and actually shows the run you want reviewed.</p>
</article>
<article>
<strong>Raw footage</strong>
<p>Add the raw footage link here if you have it hosted somewhere moderators can access.</p>
</article>
<article>
<strong>Important notes</strong>
<p>Call out solo or 2-player variants, custom copies, LDM use, and handcam details in the notes box.</p>
</article>
</div>
</aside>
</section>
<section class="panel run-my-runs-panel" id="run-my-runs-section" hidden>
<div class="submission-header run-my-runs-head">
<div>
<p class="hero-kicker">Your account</p>
<h3>Runs saved to your account</h3>
<p class="muted">Keep multiple drafts here while you work on links or notes. They sync when you are signed in.</p>
</div>
</div>
<div id="run-saved-runs-list" class="submission-list"></div>
</section>
<section class="panel submission-panel">
<div class="table-header submission-header">
<div>
<p class="status-label">Submission Queue</p>
<h3>Recent run submissions</h3>
</div>
<p id="run-list-status" class="muted">Loading recent submissions...</p>
</div>
<div id="run-submissions" class="submission-list"></div>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
-207
View File
@@ -1,207 +0,0 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
const { URL } = require('url');
const HOST = process.env.HOST || '127.0.0.1';
const PORT = Number(process.env.PORT) || 8000;
const ROOT = __dirname;
const DATA_TXT_PATH = path.join(ROOT, 'data.txt');
const DATA_JSON_PATH = path.join(ROOT, 'data.json');
const MIME_TYPES = {
'.css': 'text/css; charset=utf-8',
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.txt': 'text/plain; charset=utf-8'
};
function sendJson(res, statusCode, payload) {
res.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store'
});
res.end(JSON.stringify(payload, null, 2));
}
function sendText(res, statusCode, text, contentType = 'text/plain; charset=utf-8') {
res.writeHead(statusCode, {
'Content-Type': contentType,
'Cache-Control': 'no-store'
});
res.end(text);
}
function readBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk;
if (body.length > 1_000_000) {
reject(new Error('Request body too large.'));
req.destroy();
}
});
req.on('end', () => resolve(body));
req.on('error', reject);
});
}
function normalizeItem(item, fallbackId) {
return {
id: String(item.id || fallbackId || Date.now()),
level: String(item.level || 'new').trim() || 'new',
position: String(item.position || '').trim(),
title: String(item.title || '').trim(),
url: String(item.url || '').trim()
};
}
function parseTxtData(txt) {
return txt
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean)
.map((line, index) => {
const parts = line.split('|').map(part => part.trim());
return normalizeItem({
level: parts[0],
position: parts[1],
title: parts[2],
url: parts[3]
}, `legacy-${index + 1}`);
});
}
function serializeTxtData(items) {
return items
.map(item => [item.level, item.position, item.title, item.url].join('|'))
.join('\n') + '\n';
}
function loadItems() {
if (fs.existsSync(DATA_JSON_PATH)) {
const raw = fs.readFileSync(DATA_JSON_PATH, 'utf8');
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed.map((item, index) => normalizeItem(item, `json-${index + 1}`));
}
if (!fs.existsSync(DATA_TXT_PATH)) return [];
const txt = fs.readFileSync(DATA_TXT_PATH, 'utf8');
const items = parseTxtData(txt);
saveItems(items);
return items;
}
function saveItems(items) {
const normalized = items.map((item, index) => normalizeItem(item, `item-${index + 1}`));
fs.writeFileSync(DATA_JSON_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
fs.writeFileSync(DATA_TXT_PATH, serializeTxtData(normalized), 'utf8');
return normalized;
}
function validateItem(item) {
if (!item.title) return 'Title is required.';
if (!item.position) return 'Position is required.';
if (!/^\d+$/.test(String(item.position))) return 'Position must be numeric.';
return null;
}
function getStaticPath(requestPath) {
const cleanPath = requestPath === '/' ? '/index.html' : requestPath;
const safePath = path.normalize(cleanPath).replace(/^(\.\.[/\\])+/, '').replace(/^[/\\]+/, '');
return path.join(ROOT, safePath);
}
function serveStatic(reqPath, res) {
const filePath = getStaticPath(reqPath);
if (!filePath.startsWith(ROOT)) {
sendText(res, 403, 'Forbidden');
return;
}
fs.readFile(filePath, (err, content) => {
if (err) {
const fallback404 = path.join(ROOT, '404.html');
fs.readFile(fallback404, (fallbackErr, fallbackContent) => {
if (fallbackErr) {
sendText(res, 404, 'Not Found');
return;
}
sendText(res, 404, fallbackContent, 'text/html; charset=utf-8');
});
return;
}
const ext = path.extname(filePath).toLowerCase();
sendText(res, 200, content, MIME_TYPES[ext] || 'application/octet-stream');
});
}
function sortItems(items) {
return [...items].sort((a, b) => Number(a.position) - Number(b.position));
}
loadItems();
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
const pathname = url.pathname;
if (pathname === '/api/levels' && req.method === 'GET') {
return sendJson(res, 200, { items: sortItems(loadItems()) });
}
if (pathname === '/api/levels' && req.method === 'POST') {
try {
const body = await readBody(req);
const payload = normalizeItem(JSON.parse(body), `item-${Date.now()}`);
const error = validateItem(payload);
if (error) return sendJson(res, 400, { error });
const items = loadItems();
items.push(payload);
return sendJson(res, 201, { item: payload, items: sortItems(saveItems(items)) });
} catch (error) {
return sendJson(res, 400, { error: 'Invalid JSON request body.' });
}
}
if (pathname.startsWith('/api/levels/') && (req.method === 'PUT' || req.method === 'DELETE')) {
const id = decodeURIComponent(pathname.slice('/api/levels/'.length));
const items = loadItems();
const index = items.findIndex(item => item.id === id);
if (index === -1) {
return sendJson(res, 404, { error: 'Level not found.' });
}
if (req.method === 'DELETE') {
items.splice(index, 1);
return sendJson(res, 200, { items: sortItems(saveItems(items)) });
}
try {
const body = await readBody(req);
const payload = normalizeItem({ ...items[index], ...JSON.parse(body), id }, id);
const error = validateItem(payload);
if (error) return sendJson(res, 400, { error });
items[index] = payload;
return sendJson(res, 200, { item: payload, items: sortItems(saveItems(items)) });
} catch (error) {
return sendJson(res, 400, { error: 'Invalid JSON request body.' });
}
}
if (pathname === '/api/health') {
return sendJson(res, 200, { ok: true });
}
return serveStatic(pathname, res);
});
server.listen(PORT, HOST, () => {
console.log(`GD FEDL server running at http://localhost:${PORT}`);
});
+63
View File
@@ -0,0 +1,63 @@
Linux system service setup
This lets the server start automatically on boot on a Linux device.
MAKE A FILE NAME data.txt TO RUN DO THIS FURST
1. Copy the project to the Linux machine.
Example path:
/opt/fedl
2. Make sure Node.js is installed.
Check with:
node -v
3. Test the server manually first.
In the project folder run:
HOST=0.0.0.0 PORT=3000 node server/server.js
4. Create a systemd service file.
Run:
sudo nano /etc/systemd/system/fedl.service
5. Paste this into the service file.
[Unit]
Description=FEDL Node Server
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/fedl
ExecStart=/usr/bin/env node /opt/fedl/server/server.js
Environment=HOST=0.0.0.0
Environment=PORT=3000
Restart=always
RestartSec=3
User=www-data
Group=www-data
[Install]
WantedBy=multi-user.target
6. Reload systemd.
sudo systemctl daemon-reload
7. Enable the service.
sudo systemctl enable fedl.service
8. Start the service.
sudo systemctl start fedl.service
9. Check status.
sudo systemctl status fedl.service
10. View logs.
sudo journalctl -u fedl.service -f
Notes
- If your project is not in /opt/fedl, change WorkingDirectory and ExecStart.
- If www-data does not exist on your Linux system, use your own service user.
- Make sure port 3000 is allowed through the firewall.
- If you use a reverse proxy like Nginx, you can proxy to:
http://127.0.0.1:3000
+226
View File
@@ -0,0 +1,226 @@
# FEDL HTTP server
Node.js static file + JSON API server for the FEDL site. It serves the repo root (`../`) as files, exposes REST-style JSON endpoints, and broadcasts **Server-Sent Events (SSE)** when list or run data changes.
## Running
From the **repository root**:
```bash
node server/server.js
```
Or from `server/`:
```bash
node server.js
```
### Environment variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `PORT` | `8090` | Listen port |
| `HOST` | `127.0.0.1` | Bind address |
| `ADMIN_PASSWORD` | _(empty)_ | If set, **HTTP Basic** password for admin-only routes (username can be anything; password must match) |
| `AREDL_ACCESS_TOKEN` | _(empty)_ | Bearer token for AREDL import APIs |
| `AREDL_API_KEY` | _(empty)_ | API key header for AREDL import APIs |
| `APP_BASE_URL` | _(derived from request)_ | Public site base used in password reset links, for example `https://fedl.site` or `https://server.fedl.site/fedl` |
| `SMTP_HOST` | _(empty)_ | SMTP host for password reset emails |
| `SMTP_PORT` | `465` | SMTP port |
| `SMTP_SECURE` | `true` | Use TLS from connect time; set to `false` only if your mail server accepts plain SMTP |
| `SMTP_USER` | _(empty)_ | SMTP username for authenticated mailboxes |
| `SMTP_PASS` | _(empty)_ | SMTP password |
| `SMTP_FROM` | _(empty)_ | Sender address for password reset emails, e.g. `[email protected]` |
| `SMTP_EHLO_NAME` | `fedl.site` | Optional EHLO/HELO name sent to the SMTP server |
Console output shows the effective `PORT`, `HOST`, data paths, and whether admin protection is enabled.
## Base path
All HTTP handling assumes a **URL prefix** of `/fedl`. Incoming paths are normalized: if the request path starts with `/fedl`, that prefix is stripped and the remainder is used for routing and static files.
**Examples** (with defaults, no reverse proxy):
- Site root (often `index.html`): `http://127.0.0.1:8090/fedl` or `http://127.0.0.1:8090/fedl/`
- API list: `http://127.0.0.1:8090/fedl/api/list`
- Events: `http://127.0.0.1:8090/fedl/events`
If you mount the app at a different path in production, change the `BASE` constant in `server.js` to match.
## CORS
Responses to API routes set permissive CORS headers (`Access-Control-Allow-Origin: *`, allowed methods **GET**, **POST**, **PUT**, **DELETE**, **HEAD**, **OPTIONS**, and `Authorization` + `Content-Type` allowed). Browsers may send a preflight **OPTIONS** request; the server answers **204**.
## Data files (under `server/`)
| File | Role |
|------|------|
| `data.txt` | Pipe-separated demon list source (`category\|position\|title\|url` lines) |
| `runs.json` | Run submission queue (moderation) |
| `users.json` | Registered FEDL accounts (hashed passwords + account email) |
| `sessions.json` | Bearer session tokens + expiry |
| `userdata.json` | Per-user synced state (roulette, list %, saved runs, roulette slots) |
| `reset_tokens.json` | One-time password reset tokens + expiry |
These files are created on demand where noted below. Use `.gitignore` for `users.json`, `sessions.json`, and `userdata.json` if they contain real data.
## Server-Sent Events
### `GET /events`
- **Response:** `text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`
- **Initial line:** `retry: 3000`
- **Events:** `list-update`, `runs-update` with JSON payloads (e.g. `{ updatedAt: "ISO-8601" }`)
Clients reconnect using the `retry` hint after drops.
## Public JSON API
Unless noted, bodies are **JSON** with `Content-Type: application/json`. Errors use `{ "error": "message" }` when applicable.
### Demon list
#### `GET /api/list`
- **200:** `{ "items": [ { "level", "position", "title", "url" } ], "text": "raw file text" }`
- **500:** could not read `data.txt`
#### `PUT /api/list`
- **Auth:** Admin Basic (if `ADMIN_PASSWORD` is set)
- **Body:** `{ "text": "full data.txt content" }`
- **200:** `{ "ok": true }` — writes `data.txt`, emits `list-update`
### Run queue
#### `GET /api/runs`
- **200:** `{ "items": [ run, ... ] }` — each run includes fields from `normalizeRun` (see below)
#### `POST /api/runs`
- **Optional:** `Authorization: Bearer <session-token>` — associates submission with account (`accountUserId`, `accountUsername` on stored run)
- **Body:** `{ "playerName", "levelTitle", "videoUrl", "percent", "rawFootageUrl?", "notes?" }` (required fields enforced server-side)
- **201:** `{ "ok": true, "item": run }`
- **400:** validation / invalid JSON
Run objects include (among others): `id`, `playerName`, `levelTitle`, `videoUrl`, `percent`, `rawFootageUrl`, `notes`, `status`, `reviewedBy`, `reviewNotes`, `submittedAt`, `updatedAt`, `accountUserId`, `accountUsername`.
#### `PUT /api/runs/:id` / `DELETE /api/runs/:id`
- **Auth:** Admin Basic
- **PUT body:** fields merged via `normalizeRun` with existing run
200 / 404 / 400 as appropriate; emits `runs-update` on success
#### `POST /api/runs/bulk-approve`
- **Auth:** Admin Basic
- **Body:** `{ "playerName": "exact match", "reviewNotes?": "..." }`
- **200:** `{ "ok": true, "approved": number, "playerName": "..." }` — approves all **pending** runs whose `playerName` matches (case-insensitive); may emit `runs-update`
### Auth (FEDL user accounts)
#### `POST /api/auth/signup`
- **Body:** `{ "username", "password" }` — username normalized to lowercase; pattern 324 chars `[a-z0-9_]`, password min 8 chars
- **201:** `{ "ok": true, "token", "userId", "username" }`
- **400 / 409:** validation or username taken
#### `POST /api/auth/login`
- **Body:** `{ "username", "password" }`
- **200:** `{ "ok": true, "token", "userId", "username" }`
- **401:** invalid credentials
#### `POST /api/auth/logout`
- **Optional:** `Authorization: Bearer <token>` — revokes that session
- **200:** `{ "ok": true }`
#### `GET /api/auth/me`
- **Header:** `Authorization: Bearer <token>`
- **200:** `{ "userId", "username", "email" }`
- **401:** invalid or expired token
#### `POST /api/auth/request-password-reset`
- **Body:** `{ "identifier": "username-or-email" }`
- **200:** generic success response whether or not a matching account exists
#### `POST /api/auth/reset-password`
- **Body:** `{ "token", "newPassword" }`
- **200:** `{ "ok": true }`
- **400:** invalid/expired token or invalid password
### Account settings
#### `GET /api/account`
- **Header:** `Authorization: Bearer <token>`
- **200:** account profile including `username`, `email`, `createdAt`, and whether reset email delivery is configured
#### `PUT /api/account`
- **Header:** `Authorization: Bearer <token>`
- **Body:** `{ "email": "[email protected]" }`
- **200:** `{ "ok": true, "email": "..." }`
#### `PUT /api/account/password`
- **Header:** `Authorization: Bearer <token>`
- **Body:** `{ "currentPassword", "newPassword" }`
- **200:** `{ "ok": true, "token": "new-session-token" }`
#### `POST /api/account/password-reset-email`
- **Header:** `Authorization: Bearer <token>`
- Sends a reset link to the email saved on the current account
- Requires SMTP to be configured server-side
### User state (synced client storage)
#### `GET /api/user/state`
- **Header:** `Authorization: Bearer <token>`
- **200:** `{ "data": { "roulettePick", "levelPercents", "savedRuns", "rouletteSlots" } }` — shapes are normalized on read; missing keys default safely
- **401:** not signed in
#### `PUT /api/user/state`
- **Header:** `Authorization: Bearer <token>`
- **Body:** `{ "data": { ...same fields as above... } }` — server sanitizes `savedRuns` and `rouletteSlots` (size limits and field trimming)
- **200:** `{ "ok": true }`
- **401 / 400**
### Imports (admin + external APIs)
#### `POST /api/import/pointercrate`
- **Auth:** Admin Basic
- Fetches Pointercrate records, maps to run shape, appends to `runs.json`
- **200:** summary object with counts (see implementation)
#### `POST /api/import/aredl`
- **Auth:** Admin Basic
- Requires `AREDL_ACCESS_TOKEN` or `AREDL_API_KEY` configured
- **200:** summary / **500** on configuration or API errors
#### `POST /api/import/targeted`
- **Auth:** Admin Basic
- **Body:** `{ "source": "pointercrate" | "aredl", "filter": "player" | "level", "query": "string" }`
- Filters remote records and appends matching runs
## Static files
For **GET** / **HEAD** requests that do not match an API or `/events` handler, the server maps the path (after `BASE` strip) to files under the **repository root** (`../` from `server/`). Default document for `/` is `index.html`.
Unsupported methods receive **405**.
## Options preflight
`OPTIONS` on any path: **204** with CORS headers.
+6
View File
@@ -0,0 +1,6 @@
{
"name": "server",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+22
View File
@@ -0,0 +1,22 @@
{
"2351d1fa28dc758a60b985808c2f3f40815cf067c8087cd99ea00f77b7d75832": {
"userId": "usr_mnsa4u5y537c2d59",
"expiresAt": "2026-04-10T04:30:30.954Z",
"createdAt": "2026-04-10T03:30:30.954Z"
},
"d1e6f33eccdaa0777c7d4f161223bdeb4d95d94c9bed9911dd754e1c24f67719": {
"userId": "usr_mnsa4u5y537c2d59",
"expiresAt": "2026-04-10T04:30:55.992Z",
"createdAt": "2026-04-10T03:30:55.992Z"
},
"c9480b7c2d3953a6ad4aa25760949762ad18f3b8388a21685d59bed395737a3b": {
"userId": "usr_mnsa4u5y537c2d59",
"expiresAt": "2026-04-10T04:31:30.517Z",
"createdAt": "2026-04-10T03:31:30.517Z"
},
"b05688c574332c89ad3a0575185eb7e2725dc12b387913212162a375282e9d3a": {
"userId": "usr_mnsa4u5y537c2d59",
"expiresAt": "2026-04-10T04:46:39.467Z",
"createdAt": "2026-04-10T03:46:39.467Z"
}
}
+2052
View File
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Sign up — GD fedl</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="signup">
<header>
<h1>Sign up</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main class="auth-main">
<section class="panel auth-panel">
<p class="hero-kicker">FEDL account</p>
<h2>Create an account</h2>
<p class="muted">Username and password are stored on your FEDL server. Roulette progress and list % sync to the server when you are signed in. Password resets are sent via the app's messages.</p>
<form id="signup-form" class="auth-form">
<label class="admin-password-field" for="signup-username">
<span>Username</span>
<input id="signup-username" name="username" type="text" autocomplete="username" required minlength="3" maxlength="24" pattern="[a-z0-9_]{3,24}" placeholder="letters, numbers, underscore" />
</label>
<p class="muted small auth-hint">324 characters, lowercase letters, numbers, or underscore only.</p>
<label class="admin-password-field" for="signup-password">
<span>Password</span>
<input id="signup-password" name="password" type="password" autocomplete="new-password" required minlength="8" placeholder="At least 8 characters" />
</label>
<label class="admin-password-field" for="signup-password2">
<span>Confirm password</span>
<input id="signup-password2" name="password2" type="password" autocomplete="new-password" required minlength="8" />
</label>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAC_RrrNCJVX_wbHb"></div>
<p id="signup-status" class="muted" role="status"></p>
<div class="auth-actions">
<button type="submit" class="btn" id="signup-submit">Create account</button>
<a class="text-link" href="login.html?return=signup.html">Already have an account? Log in</a>
</div>
</form>
</section>
</main>
<script src="app.js" defer></script>
</body>
</html>
+124
View File
@@ -0,0 +1,124 @@
<!doctype html>
<html lang="en">
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="Complete sitemap of all FEDL pages and resources.">
<title>Sitemap — FEDL</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
<style>
.sitemap-content {
max-width: 800px;
}
.sitemap-section {
margin-bottom: 2rem;
}
.sitemap-section h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
margin-top: 1.5rem;
}
.sitemap-section ul {
list-style: none;
padding: 0;
}
.sitemap-section li {
margin-bottom: 0.75rem;
}
.sitemap-section a {
color: #00d4ff;
text-decoration: none;
}
.sitemap-section a:hover {
text-decoration: underline;
}
</style>
</head>
<body data-page="sitemap">
<header>
<h1>FEDL - Ranked Demon List</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
<a href="admelist.html">Admin</a>
</nav>
</header>
<main class="sitemap-content">
<h1>Sitemap</h1>
<p>Welcome to the FEDL sitemap. Below you'll find all the pages and resources available on our site.</p>
<section class="sitemap-section">
<h2>Main Pages</h2>
<ul>
<li><a href="index.html">Home</a> - FEDL Hub and overview</li>
<li><a href="lists.html">Rankings</a> - Browse the complete ranked demon list</li>
<li><a href="rules.html">Rules</a> - Submission rules and guidelines</li>
<li><a href="contact.html">Contact</a> - Get in touch with us</li>
</ul>
</section>
<section class="sitemap-section">
<h2>Games & Tools</h2>
<ul>
<li><a href="guess.html">Guess Game</a> - Test your demon knowledge</li>
<li><a href="roulette.html">Roulette</a> - Spin for a random challenge demon</li>
<li><a href="players.html">Players</a> - Browse player profiles and stats</li>
</ul>
</section>
<section class="sitemap-section">
<h2>User Account</h2>
<ul>
<li><a href="login.html">Login</a> - Sign in to your account</li>
<li><a href="signup.html">Sign Up</a> - Create a new account</li>
</ul>
</section>
<section class="sitemap-section">
<h2>Submissions & Communication</h2>
<ul>
<li><a href="run.html">Submit Run</a> - Submit your demon completions</li>
<li><a href="admelist.html">Admin</a> - List management tools</li>
<li><a href="messages.html">Messages</a> - View and send messages</li>
</ul>
</section>
<section class="sitemap-section">
<h2>Additional Resources</h2>
<ul>
<li><a href="README.md">About FEDL</a> - Learn more about the project</li>
<li><a href="errors.html">Error Reference</a> - Common error pages and solutions</li>
<li><a href="offlineindex.html">Offline Index</a> - Browse content offline</li>
<li><a href="sitemap.html">Sitemap</a> - This page</li>
<li><a href="https://github.com/your-repo">GitHub Repository</a> - View the source code</li>
</ul>
</section>
<section class="sitemap-section">
<h2>Error Pages</h2>
<ul>
<li><a href="401.html">401 Unauthorized</a> - Authentication required</li>
<li><a href="403.html">403 Forbidden</a> - Access denied</li>
<li><a href="404.html">404 Not Found</a> - Page not found</li>
<li><a href="429.html">429 Too Many Requests</a> - Rate limit exceeded</li>
<li><a href="500.html">500 Internal Server Error</a> - Server error</li>
<li><a href="502.html">502 Bad Gateway</a> - Gateway error</li>
<li><a href="503.html">503 Service Unavailable</a> - Service temporarily unavailable</li>
</ul>
</section>
<hr style="margin: 2rem 0; opacity: 0.3;">
<p style="font-size: 0.9rem; color: #999;">Last updated: April 8, 2026</p>
</main>
</body>
</html>
+141
View File
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://fedl.site/</loc>
<lastmod>2026-04-08</lastmod>
<priority>1.0</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://fedl.site/lists.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.95</priority>
<changefreq>daily</changefreq>
</url>
<url>
<loc>https://fedl.site/run.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.85</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://fedl.site/guess.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.8</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://fedl.site/players.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.8</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://fedl.site/rules.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.8</priority>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://fedl.site/roulette.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.75</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://fedl.site/contact.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.7</priority>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://fedl.site/login.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.7</priority>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://fedl.site/signup.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.7</priority>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://fedl.site/messages.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.7</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://fedl.site/post.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.7</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://fedl.site/admelist.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.7</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://fedl.site/errors.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.6</priority>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://fedl.site/offlineindex.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.6</priority>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://fedl.site/sitemap.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.6</priority>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://fedl.site/401.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.3</priority>
<changefreq>yearly</changefreq>
</url>
<url>
<loc>https://fedl.site/403.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.3</priority>
<changefreq>yearly</changefreq>
</url>
<url>
<loc>https://fedl.site/404.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.3</priority>
<changefreq>yearly</changefreq>
</url>
<url>
<loc>https://fedl.site/429.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.3</priority>
<changefreq>yearly</changefreq>
</url>
<url>
<loc>https://fedl.site/500.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.3</priority>
<changefreq>yearly</changefreq>
</url>
<url>
<loc>https://fedl.site/502.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.3</priority>
<changefreq>yearly</changefreq>
</url>
<url>
<loc>https://fedl.site/503.html</loc>
<lastmod>2026-04-08</lastmod>
<priority>0.3</priority>
<changefreq>yearly</changefreq>
</url>
</urlset>
+3
View File
@@ -0,0 +1,3 @@
#!/bin/zsh
cd "/Users/miles/Documents/GitHub/fedl" || exit 1
HOST=0.0.0.0 PORT=8090 node server/server.js
+434 -42
View File
@@ -1,9 +1,11 @@
:root{--bg:#0f1724;--panel:#071326;--accent:#5cc5ff;--muted:#9fb3c8;--text:#e6eef8;--card:#081220}
:root{--bg:#0f1724;--panel:#071326;--accent:#5cc5ff;--muted:#9fb3c8;--text:#e6eef8;--card:#081220;--accent-warm:#ffb84d}
*{box-sizing:border-box}
html,body{height:100%;margin:0;font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;color:var(--text);background:linear-gradient(180deg,#061021 0%,#071426 100%)}
header{display:flex;align-items:center;justify-content:space-between;padding:14px 20px;background:linear-gradient(90deg,rgba(255,255,255,0.02),transparent);backdrop-filter:blur(4px)}
header h1{margin:0;font-size:1.1rem}
nav a{color:var(--muted);margin-left:14px;text-decoration:none}
html,body{height:100%;margin:0;font-family:"Space Grotesk","Trebuchet MS",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--text);background:var(--bg)}
header{position:sticky;top:0;z-index:50;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:14px 20px;background:linear-gradient(90deg,rgba(6,16,33,0.9),rgba(7,20,38,0.72));backdrop-filter:blur(16px);border-bottom:1px solid rgba(255,255,255,0.05)}
header h1{margin:0;font-size:1.1rem;letter-spacing:.02em}
nav{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px 12px}
nav a{color:var(--muted);margin-left:0;text-decoration:none;transition:color .2s ease,opacity .2s ease}
nav a:hover{color:var(--text)}
main{padding:18px 20px}
h1,h2,h3,h4{line-height:1.15}
.layout{display:flex;gap:16px;align-items:flex-start}
@@ -12,11 +14,13 @@ h1,h2,h3,h4{line-height:1.15}
.sidebar h2{margin:0 0 8px 0;font-size:0.95rem}
.sidebar ul{list-style:none;padding:0;margin:0}
.sidebar li{margin:6px 0}
.sidebar-kicker{margin:0 0 8px 0;text-transform:uppercase;letter-spacing:.16em;font-size:.72rem;font-weight:800;color:var(--accent)}
.level-link{display:block;width:100%;text-align:left;padding:8px;border-radius:6px;border:0;background:transparent;color:var(--muted);cursor:pointer;font-weight:600}
.level-link.active{background:rgba(92,197,255,0.06);color:var(--accent)}
.content{flex:1}
.panel{background:var(--panel);padding:14px;border-radius:10px;margin-bottom:12px;box-shadow:0 3px 12px rgba(2,6,23,0.6)}
.search-row{display:flex;gap:8px;align-items:center;margin-bottom:12px}
.home-section-grid,.spotlight-panel,.discord-panel,.list-table-panel,.submission-panel,.run-side-panel{content-visibility:auto;contain-intrinsic-size:1px 500px}
.search-row{display:flex;flex-wrap:wrap;gap:14px;align-items:end;margin-bottom:0}
.search-row input[type=text]{flex:1;padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--text)}
.search-row select{padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--text)}
.table-wrap{overflow:auto;border-radius:8px}
@@ -24,40 +28,287 @@ table.levels-table{width:100%;border-collapse:collapse;background:linear-gradien
table.levels-table th,table.levels-table td{padding:12px 14px;border-bottom:1px solid rgba(255,255,255,0.02);text-align:left}
table.levels-table th{color:var(--muted);font-size:0.85rem;font-weight:600}
table.levels-table tr:hover td{background:rgba(255,255,255,0.01)}
.btn{display:inline-block;padding:8px 10px;border-radius:8px;background:var(--accent);color:#042;cursor:pointer;border:none}
.levels-table input{width:100%;padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text)}
.btn{display:inline-block;padding:10px 14px;border-radius:999px;background:var(--accent);color:var(--bg);cursor:pointer;border:none;font-weight:700;text-decoration:none;transition:transform .2s ease,box-shadow .2s ease,background .2s ease,filter .2s ease}
.btn:hover{transform:translateY(-1px);box-shadow:0 10px 24px color-mix(in srgb,var(--accent) 22%,transparent);filter:brightness(1.1)}
.btn:active{transform:translateY(0)}
.muted{color:var(--muted);font-size:0.9rem}
.error-text{color:#ff9f9f}
.success-text{color:#8cf0ae!important}
header nav a.btn{margin-left:14px;text-decoration:none;vertical-align:middle}
.small-btn{padding:8px 12px}
.danger-btn{background:var(--accent);color:var(--bg)}
.admin-shell{padding:18px 20px}
.admin-shell-content[hidden]{display:none}
.admin-locked{overflow:hidden}
.admin-locked #admin-page-header,.admin-locked #admin-shell-content{visibility:hidden}
.admin-login-screen{position:fixed;inset:0;z-index:2000;display:flex;align-items:center;justify-content:center;padding:30px;background:
radial-gradient(circle at top,rgba(255,184,77,0.16),transparent 24%),
linear-gradient(180deg,rgba(6,16,33,0.98),rgba(7,20,38,0.99))}
.admin-login-card{width:min(100%,520px);padding:34px;border-radius:28px;background:linear-gradient(145deg,rgba(255,184,77,0.12),rgba(7,19,38,0.96) 42%),radial-gradient(circle at top left,rgba(255,255,255,0.08),transparent 35%);border:1px solid rgba(255,184,77,0.16)}
.admin-login-card h2{margin:0 0 10px 0;font-size:clamp(2rem,4vw,2.8rem)}
.admin-login-form{display:grid;gap:16px;margin-top:20px}
.admin-login-actions{display:grid;gap:12px}
.admin-login-screen[hidden]{display:none}
.admin-panel{padding:20px}
.admin-overview-panel{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:24px 26px;border-radius:28px;background:linear-gradient(135deg,rgba(255,184,77,0.12),rgba(7,19,38,0.95));border:1px solid rgba(255,184,77,0.16)}
.admin-overview-panel h2{margin:0 0 8px 0;font-size:clamp(1.9rem,4vw,2.8rem)}
.admin-overview-badges{display:flex;flex-wrap:wrap;gap:10px}
.admin-overview-badges span{padding:10px 14px;border-radius:999px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);color:var(--text);font-weight:700}
.admin-password-field{display:grid;gap:8px;width:100%;font-weight:700;color:var(--text)}
.admin-password-field span{font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.admin-password-field input,.admin-actions input[type=text],.admin-actions input[type=password]{min-width:220px;padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text)}
.admin-toolbar{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;margin-bottom:18px}
.admin-toolbar h2{margin:0 0 8px 0}
.admin-import-actions{flex-wrap:wrap;justify-content:flex-end}
.bulk-approve-modal[hidden]{display:none !important}
.bulk-approve-modal-card{max-width:480px}
.bulk-approve-modal-card h2{margin:0 0 8px 0;font-size:clamp(1.35rem,3vw,1.75rem)}
.bulk-approve-form{margin-top:8px}
.bulk-approve-actions{grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}
.import-targeted-mode{display:flex;flex-wrap:wrap;align-items:center;gap:12px 18px;margin:4px 0 8px}
.import-targeted-mode-label{font-size:0.88rem;font-weight:700;color:var(--muted)}
.import-targeted-radio{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:0.95rem}
.import-targeted-radio input{accent-color:var(--accent)}
.import-targeted-hint{margin:0 0 4px;line-height:1.45}
.admin-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.admin-table td:last-child{text-align:center}
.run-admin-cell{display:grid;gap:6px}
.run-admin-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}
.run-admin-detail-row td{padding-top:0}
.run-admin-detail{display:grid;gap:8px;padding:0 0 14px 0;color:var(--text)}
.run-admin-detail a{color:var(--accent)}
.hero-kicker{margin:0 0 12px 0;text-transform:uppercase;letter-spacing:.18em;font-size:.75rem;font-weight:800;color:var(--accent-warm)}
.home-hero{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(300px,.85fr);gap:22px;align-items:stretch;min-height:calc(100vh - 132px)}
.home-copy,.hero-stage{position:relative;overflow:hidden}
.home-copy{display:flex;flex-direction:column;justify-content:center;padding:34px;border-radius:28px;background:linear-gradient(145deg,rgba(255,184,77,0.12),rgba(7,19,38,0.96) 42%),radial-gradient(circle at top left,rgba(255,255,255,0.08),transparent 35%);border:1px solid rgba(255,184,77,0.16)}
.home-copy::before{content:"";position:absolute;inset:auto -80px -90px auto;width:240px;height:240px;border-radius:50%;background:rgba(92,197,255,0.12);filter:blur(16px)}
.home-copy h2{margin:0 0 14px 0;font-size:clamp(2.6rem,6vw,5.3rem);max-width:12ch;position:relative;z-index:1}
.hero-text{max-width:60ch;margin:0;position:relative;z-index:1;color:var(--text);line-height:1.75;font-size:1.02rem}
.hero-actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:28px;position:relative;z-index:1}
.hero-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:28px;position:relative;z-index:1}
.stat-card{padding:16px 18px;border-radius:18px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);backdrop-filter:blur(10px)}
.stat-card strong{display:block;margin-bottom:8px;font-size:clamp(1.4rem,3vw,2rem);color:var(--text)}
.stat-card span{display:block;color:var(--text);line-height:1.45;font-size:.94rem}
.hero-stage{padding:28px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.07),rgba(255,255,255,0.03));border:1px solid rgba(255,255,255,0.08)}
.stage-label{margin:0 0 8px 0;font-size:.76rem;letter-spacing:.16em;text-transform:uppercase;color:var(--accent);font-weight:800}
.hero-stage h3{margin:0 0 8px 0;font-size:2rem}
.featured-list{display:grid;gap:12px;margin-top:22px;max-height:600px;overflow-y:auto}
.featured-card{padding:18px;border-radius:18px;background:var(--panel);border:1px solid rgba(255,255,255,0.08)}
.featured-rank{display:inline-flex;align-items:center;justify-content:center;min-width:58px;padding:7px 12px;border-radius:999px;background:rgba(92,197,255,0.14);color:var(--accent);font-size:.82rem;font-weight:800}
.featured-card strong{display:block;margin:14px 0 8px 0;font-size:1.1rem}
.featured-card p{margin:0;color:var(--text);line-height:1.55}
.home-section-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:18px}
.info-panel{padding:24px;border-radius:22px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
.info-panel h3{margin:0 0 10px 0;font-size:1.45rem}
.text-link{display:inline-block;margin-top:18px;color:var(--accent);font-weight:700;text-decoration:none}
.text-link:hover{color:#a9e5ff}
.spotlight-panel{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);gap:24px;align-items:start;margin-top:18px;padding:28px;border-radius:28px;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 12%,transparent),var(--panel));border:1px solid rgba(92,197,255,0.16)}
.spotlight-copy h2{margin:0 0 12px 0;font-size:2.1rem}
.spotlight-text{margin:0;color:var(--text);line-height:1.7}
.spotlight-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
.spotlight-card{padding:18px;border-radius:18px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08)}
.spotlight-card h3{margin:0 0 10px 0;font-size:1.08rem}
.spotlight-card p{margin:0;color:var(--text);line-height:1.6}
.list-page-shell{display:flex;flex-direction:column;gap:16px}
.list-sidebar-panel{padding:18px 16px;border-radius:24px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
.list-sidebar-panel h2{margin:0 0 10px 0;font-size:1.2rem}
.list-sidebar-panel .muted{margin:0 0 16px 0;line-height:1.55}
.list-hero-panel{display:flex;align-items:end;justify-content:space-between;gap:18px;padding:28px;border-radius:28px;background:linear-gradient(135deg,rgba(92,197,255,0.14),rgba(7,19,38,0.95));border:1px solid rgba(92,197,255,0.16)}
.list-hero-copy h2{margin:0 0 10px 0;font-size:clamp(2rem,4vw,3rem)}
.list-hero-text{max-width:60ch;line-height:1.7}
.list-hero-badge{max-width:230px;padding:16px 18px;border-radius:18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08)}
.list-hero-badge strong{display:block;margin-bottom:8px;font-size:1rem}
.list-hero-badge span{display:block;color:var(--text);line-height:1.55;font-size:.92rem}
.list-controls-panel{padding:20px 22px;border-radius:24px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.03));border:1px solid rgba(255,255,255,0.06)}
.control-block{display:flex;flex:1;flex-direction:column;gap:8px;font-weight:700;color:var(--text)}
.control-block span{font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.control-block input,.control-block select{width:100%;padding:12px 14px;border-radius:14px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text)}
.control-select{max-width:220px}
.list-table-panel{padding:22px 22px 16px;border-radius:26px;background:linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
.table-header{display:flex;align-items:end;justify-content:space-between;gap:16px;margin-bottom:16px}
.table-header h3{margin:0;font-size:1.45rem}
.table-hint{max-width:320px;line-height:1.6;text-align:right}
table.levels-table{border-radius:18px;overflow:hidden}
table.levels-table th{background:rgba(255,255,255,0.03)}
table.levels-table td{font-size:.98rem}
table.levels-table td:last-child{text-align:right}
table.levels-table tbody tr:nth-child(even) td{background:rgba(255,255,255,0.018)}
table.levels-table .btn{padding:9px 14px}
.wip-hero{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr);gap:22px;align-items:stretch;min-height:calc(100vh - 132px)}
.wip-copy,.wip-stage{position:relative}
.wip-copy{display:flex;flex-direction:column;justify-content:center;padding:32px;border-radius:26px;background:linear-gradient(145deg,rgba(255,184,77,0.1),rgba(7,19,38,0.96) 40%),radial-gradient(circle at top left,rgba(255,255,255,0.08),transparent 36%);border:1px solid rgba(255,184,77,0.16);overflow:hidden}
.wip-copy::after{content:"";position:absolute;inset:auto -60px -80px auto;width:220px;height:220px;border-radius:50%;background:rgba(92,197,255,0.12);filter:blur(14px)}
.wip-copy h2{margin:0 0 14px 0;font-size:clamp(2.4rem,6vw,5rem);max-width:11ch}
.hero-text{max-width:58ch;margin:0;color:var(--text);line-height:1.7;font-size:1.02rem}
.hero-actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:26px}
.wip-stage{display:flex;align-items:center;justify-content:center;padding:12px}
.construction-card{width:min(100%,380px);padding:24px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.09);box-shadow:0 30px 80px rgba(0,0,0,0.28);animation:floatCard 4.6s ease-in-out infinite}
.construction-pill{display:inline-block;padding:7px 12px;border-radius:999px;background:rgba(255,184,77,0.16);color:#ffd79b;font-size:.78rem;font-weight:800;letter-spacing:.14em;text-transform:uppercase}
.construction-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin:20px 0}
.construction-grid span{display:block;aspect-ratio:1;border-radius:16px;background:linear-gradient(135deg,rgba(92,197,255,0.22),rgba(255,184,77,0.08));border:1px solid rgba(255,255,255,0.08);animation:pulseTile 3.4s ease-in-out infinite}
.construction-grid span:nth-child(2){animation-delay:.35s}
.construction-grid span:nth-child(3){animation-delay:.7s}
.construction-grid span:nth-child(4){animation-delay:1.05s}
.progress-shell{height:14px;border-radius:999px;background:rgba(255,255,255,0.08);overflow:hidden}
.progress-bar{height:100%;width:48%;border-radius:inherit;background:linear-gradient(90deg,var(--accent-warm),var(--accent));animation:progressShift 2.8s ease-in-out infinite}
.construction-card p{margin:18px 0 0 0;color:var(--text);line-height:1.6}
.wip-status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-top:18px}
.status-panel{padding:22px;border-radius:20px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
.status-label{margin:0 0 8px 0;font-size:.76rem;letter-spacing:.16em;text-transform:uppercase;color:var(--accent-warm);font-weight:800}
.status-panel h3{margin:0 0 10px 0;font-size:1.4rem}
.coming-shell{position:relative;display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 132px);overflow:hidden}
.coming-panel{position:relative;z-index:1;width:min(100%,720px);padding:42px 30px;text-align:center;border-radius:28px;background:linear-gradient(145deg,rgba(92,197,255,0.12),rgba(7,19,38,0.96) 52%),radial-gradient(circle at top,rgba(255,255,255,0.1),transparent 32%);border:1px solid rgba(92,197,255,0.18);box-shadow:0 24px 70px rgba(2,6,23,0.42)}
.coming-panel h2{margin:0 0 14px 0;font-size:clamp(2.8rem,8vw,4.8rem)}
.coming-text{max-width:44ch;margin:0 auto;color:var(--text);line-height:1.7}
.coming-loader{display:flex;justify-content:center;gap:12px;margin:28px 0 22px}
.coming-loader span{width:16px;height:16px;border-radius:50%;background:linear-gradient(180deg,var(--accent-warm),var(--accent));animation:bounceDot 1.2s ease-in-out infinite}
.coming-loader span:nth-child(2){animation-delay:.15s}
.coming-loader span:nth-child(3){animation-delay:.3s}
.dad-joke{margin:0 auto 24px;padding:16px 18px;max-width:42ch;border-radius:18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.07);color:var(--text);line-height:1.65}
.coming-orb{position:absolute;border-radius:50%;filter:blur(8px);opacity:.8;animation:drift 8s ease-in-out infinite}
.orb-one{top:10%;left:12%;width:180px;height:180px;background:rgba(92,197,255,0.12)}
.orb-two{right:10%;bottom:12%;width:220px;height:220px;background:rgba(255,184,77,0.12);animation-delay:1.8s}
@keyframes floatCard{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
@keyframes pulseTile{0%,100%{transform:scale(1);opacity:.72}50%{transform:scale(1.04);opacity:1}}
@keyframes progressShift{0%{transform:translateX(-18%);width:36%}50%{transform:translateX(58%);width:50%}100%{transform:translateX(-18%);width:36%}}
@keyframes bounceDot{0%,80%,100%{transform:translateY(0);opacity:.5}40%{transform:translateY(-12px);opacity:1}}
@keyframes drift{0%,100%{transform:translate3d(0,0,0)}50%{transform:translate3d(20px,-18px,0)}}
.video-modal{position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:1200}
.video-modal .inner{width:90%;max-width:960px;background:var(--card);padding:12px;border-radius:8px}
.video-modal .inner{width:90%;max-width:960px;max-height:88vh;overflow:auto;background:var(--card);padding:12px;border-radius:8px}
.video-modal iframe{width:100%;height:540px;border:0;border-radius:6px}
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%}.video-modal iframe{height:320px}}
.modal-runs-wrap{margin-top:16px;padding:16px;border-radius:16px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08)}
.modal-runs-head{display:flex;align-items:end;justify-content:space-between;gap:12px;margin-bottom:12px}
.modal-runs-list{display:grid;gap:10px;max-height:240px;overflow:auto;padding-right:4px}
.modal-run-card{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;align-items:center;padding:12px 14px;border-radius:14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06)}
.modal-run-card strong,.modal-run-card span,.modal-run-card a{align-self:center;line-height:1.2}
.modal-run-card strong{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.modal-run-card span{color:var(--text);font-weight:700;white-space:nowrap}
.modal-run-card a{margin:0;white-space:nowrap}
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%}.video-modal iframe{height:320px}.wip-hero{grid-template-columns:1fr;min-height:auto}.wip-copy{padding:28px 22px}.wip-status-grid{grid-template-columns:1fr}.coming-panel{padding:34px 22px}.coming-shell{min-height:auto;padding:30px 0}}
/* small helpers reused */
.small{font-size:0.9rem}
.roulette-shell{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(280px,.75fr);gap:18px;align-items:start}
.roulette-main-stack{display:grid;gap:18px;min-width:0}
.roulette-account-panel{padding:22px 24px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.08)}
.roulette-account-heading{margin:0 0 8px 0;font-size:1.35rem}
.roulette-account-intro{margin:0 0 14px;line-height:1.55}
.roulette-account-controls{display:grid;gap:10px}
.roulette-account-label{font-size:0.88rem;font-weight:700;color:var(--muted)}
.roulette-account-select{width:100%;max-width:320px;padding:10px 12px;border-radius:12px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.04);color:var(--text);font:inherit}
.roulette-account-create-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.roulette-account-create-row input{flex:1;min-width:160px;padding:10px 12px;border-radius:12px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.04);color:var(--text);font:inherit}
.roulette-restore-btn{margin-top:12px}
.roulette-login-sync-hint{margin:0 0 14px;line-height:1.55}
.roulette-login-sync-hint a{color:var(--accent)}
.roulette-slots-block{margin:14px 0;padding:14px 0;border-top:1px solid rgba(255,255,255,0.08);border-bottom:1px solid rgba(255,255,255,0.08)}
.roulette-slots-heading{margin:0 0 6px 0;font-size:0.95rem;font-weight:800;color:var(--text)}
.roulette-slot-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:10px}
.roulette-slot-title{min-width:52px;font-weight:700;font-size:0.88rem;color:var(--muted)}
.roulette-slot-label{flex:1;min-width:140px;font-size:0.88rem}
.roulette-progress-block{margin-top:16px;padding-top:16px;border-top:1px solid rgba(255,255,255,0.08)}
.roulette-progress-label{display:block;font-size:0.88rem;font-weight:700;margin-bottom:8px;color:var(--text)}
.roulette-progress-input-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.roulette-progress-input-row input{width:100px;padding:10px 12px;border-radius:12px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.04);color:var(--text);font:inherit}
.roulette-percent-hint{margin:10px 0 0;line-height:1.55;max-width:52ch}
.modal-account-progress{margin:16px 0;padding:14px 16px;border-radius:14px;background:rgba(92,197,255,0.08);border:1px solid rgba(92,197,255,0.15)}
.modal-account-label{margin:0 0 8px 0;font-size:0.9rem;font-weight:700}
.modal-account-row{display:flex;gap:10px;align-items:center}
.modal-account-pct-input{width:100px;padding:8px 10px;border-radius:10px;border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.05);color:var(--text);font:inherit}
.list-my-progress-cell{vertical-align:middle}
.list-progress-input{width:100%;max-width:88px;padding:8px 10px;border-radius:10px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.04);color:var(--text);font:inherit;text-align:center}
.roulette-main-column{display:grid;gap:18px}
.roulette-rules{margin-bottom:18px}
.roulette-panel{margin-top:18px;padding:24px 26px;background:linear-gradient(135deg,rgba(92,197,255,0.12),rgba(7,19,38,0.96));border:1px solid rgba(92,197,255,0.16)}
.roulette-panel{margin-top:0;padding:28px;border-radius:28px;background:linear-gradient(135deg,rgba(92,197,255,0.14),rgba(7,19,38,0.96));border:1px solid rgba(92,197,255,0.18)}
.roulette-head{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;margin-bottom:18px}
.roulette-kicker{margin:0 0 8px 0;text-transform:uppercase;letter-spacing:0.14em;font-size:0.72rem;font-weight:700;color:var(--accent)}
.roulette-head h2{margin:0 0 8px 0;font-size:1.9rem}
.roulette-btn{min-width:110px;font-weight:700}
.roulette-card{padding:22px;border-radius:14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05)}
.roulette-head h2{margin:0 0 8px 0;font-size:clamp(2rem,4vw,3rem)}
.roulette-intro{max-width:50ch;line-height:1.7}
.roulette-btn{min-width:120px;font-weight:700}
.roulette-card{padding:24px;border-radius:22px;background:linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0.03));border:1px solid rgba(255,255,255,0.08);box-shadow:0 20px 40px rgba(2,6,23,0.28)}
.roulette-status{margin:0 0 8px 0}
.roulette-card h3{margin:0 0 8px 0;font-size:1.5rem}
.roulette-rank{margin:0 0 16px 0;font-size:1rem;color:#dce8f5}
.roulette-meta{margin:0 0 10px 0;font-size:1rem;color:#dce8f5}
.roulette-card h3{margin:0 0 10px 0;font-size:clamp(1.6rem,3vw,2.3rem)}
.roulette-rank{margin:0 0 16px 0;font-size:1.02rem;color:var(--text)}
.roulette-meta{margin:0 0 10px 0;font-size:1rem;color:var(--text)}
.roulette-note{margin:10px 0 16px 0}
.roulette-open{color:#042}
.roulette-guess-panel{background:linear-gradient(135deg,rgba(255,184,77,0.12),rgba(7,19,38,0.96));border-color:rgba(255,184,77,0.18)}
.guess-card{display:grid;gap:12px}
.guess-form{display:grid;gap:10px;margin-top:4px}
.guess-label{font-size:.9rem;font-weight:700;color:var(--text)}
.guess-controls{display:flex;gap:12px;align-items:center}
.guess-controls input{flex:1;padding:12px 14px;border-radius:12px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text);font:inherit}
.guess-answer{margin:0}
.roulette-rules-card{padding:24px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.08)}
.roulette-rules-card h2{margin:0 0 16px 0;font-size:1.8rem}
.roulette-rule-list{display:grid;gap:14px}
.roulette-rule-list article{padding:16px;border-radius:18px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06)}
.roulette-rule-list strong{display:block;margin-bottom:8px;font-size:1rem;color:var(--text)}
.roulette-rule-list p{margin:0;color:var(--text);line-height:1.6}
.run-shell{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr);gap:18px;align-items:start}
.run-panel{padding:28px;border-radius:28px;background:linear-gradient(135deg,rgba(92,197,255,0.14),rgba(7,19,38,0.96));border:1px solid rgba(92,197,255,0.18)}
.run-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;margin-bottom:20px}
.run-head h2{margin:0 0 10px 0;font-size:clamp(2rem,4vw,3rem)}
.run-intro{max-width:58ch;line-height:1.7}
.run-head-badge{max-width:240px;padding:16px 18px;border-radius:18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08)}
.run-head-badge strong{display:block;margin-bottom:8px}
.run-head-badge span{display:block;color:var(--text);line-height:1.55;font-size:.92rem}
.run-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
.run-notes-block{grid-column:1/-1}
.run-form textarea{width:100%;padding:12px 14px;border-radius:14px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);color:var(--text);resize:vertical;min-height:150px}
.run-actions{grid-column:1/-1;display:flex;flex-wrap:wrap;align-items:center;gap:14px}
.run-side-panel{padding:24px 22px;border-radius:28px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.08)}
.run-side-panel h2{margin:0 0 16px 0;font-size:1.8rem}
.run-checklist{display:grid;gap:14px}
.run-checklist article{padding:16px;border-radius:18px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06)}
.run-checklist strong{display:block;margin-bottom:8px}
.run-checklist p{margin:0;color:var(--text);line-height:1.6}
.submission-panel{margin-top:18px;padding:22px 22px 16px;border-radius:26px;background:linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06)}
.submission-header{margin-bottom:18px}
.submission-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
.run-my-runs-panel{margin-top:8px}
.run-my-runs-head{margin-bottom:14px}
.run-saved-card-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:12px}
.submission-card{padding:18px;border-radius:18px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08)}
@keyframes fedl-shimmer{
0%{background-position:100% 0}
100%{background-position:-100% 0}
}
.submission-card--shimmer{
min-height:118px;
background:linear-gradient(90deg,rgba(255,255,255,0.04) 0%,rgba(255,255,255,0.1) 45%,rgba(255,255,255,0.04) 90%);
background-size:200% 100%;
animation:fedl-shimmer 1.15s ease-in-out infinite;
pointer-events:none
}
.submission-card-top{display:flex;align-items:center;justify-content:space-between;gap:12px}
.submission-card strong{font-size:1.05rem}
.submission-card p{margin:10px 0 0 0;color:var(--text);line-height:1.55}
.submission-meta{font-size:.92rem;color:var(--muted)!important}
.submission-links{display:flex;flex-wrap:wrap;gap:14px;margin-top:14px}
.status-pill{display:inline-flex;align-items:center;justify-content:center;padding:6px 10px;border-radius:999px;font-size:.76rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase}
.status-pending{background:rgba(255,184,77,0.16);color:#ffd07d}
.status-approved{background:rgba(70,196,121,0.16);color:#8cf0ae}
.status-rejected{background:rgba(255,123,123,0.16);color:#ffaeae}
.discord-panel{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-top:18px;padding:26px 28px;background:linear-gradient(135deg,rgba(88,101,242,0.18),rgba(7,19,38,0.95));border:1px solid rgba(88,101,242,0.22)}
.download-panel{background:linear-gradient(135deg,rgba(45,221,149,0.18),rgba(7,19,38,0.95));border:1px solid rgba(45,221,149,0.22)}
.discord-copy{flex:1}
.discord-badge{display:flex;align-items:center;gap:16px;margin-bottom:14px}
.discord-logo{display:flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:18px;background:#5865f2;color:#fff;box-shadow:0 10px 24px rgba(88,101,242,0.32)}
.discord-logo{display:flex;align-items:center;justify-content:center;width:64px;height:64px;border-radius:18px;background:#5865f2;color:var(--text);box-shadow:0 10px 24px rgba(88,101,242,0.32)}
.discord-logo img{width:34px;height:34px;display:block}
.discord-label{margin:0 0 6px 0;text-transform:uppercase;letter-spacing:0.14em;font-size:0.72rem;font-weight:700;color:#bfc7ff}
.discord-label{margin:0 0 6px 0;text-transform:uppercase;letter-spacing:0.14em;font-size:0.72rem;font-weight:700;color:var(--text)}
.discord-panel h2{margin:0;font-size:1.9rem}
.discord-text{max-width:720px;margin:0 0 16px 0;line-height:1.65;color:#dce8f5}
.discord-text{max-width:720px;margin:0 0 16px 0;line-height:1.65;color:var(--text)}
.discord-footnote{margin-top:16px;margin-bottom:0}
.feature-bullet-list{margin:0;padding-left:22px;color:#e0ffe9}
.feature-bullet-list li{margin:0 0 10px 0;line-height:1.55}
.download-panel .discord-text{color:#e0ffe9}
.discord-highlights{display:flex;flex-wrap:wrap;gap:10px}
.discord-highlights span{padding:8px 12px;border-radius:999px;background:rgba(255,255,255,0.07);color:#eef3ff;font-size:0.92rem}
.discord-actions{display:flex;align-items:center;justify-content:center}
.discord-btn{padding:12px 18px;background:#5865f2;color:#fff;font-weight:700}
.discord-btn{padding:12px 18px;background:#5865f2;color:var(--text);font-weight:700}
.download-panel .discord-btn{background:#1fce7a}
.rules-view header{padding:18px 28px}
.rules-view header h1{font-size:2rem;font-weight:800;letter-spacing:0.02em}
.rules-view main{display:flex;justify-content:center;padding:40px 20px 64px}
@@ -66,11 +317,11 @@ table.levels-table tr:hover td{background:rgba(255,255,255,0.01)}
.rules-kicker{margin:0 0 10px 0;text-transform:uppercase;letter-spacing:0.18em;font-size:0.72rem;color:var(--accent);font-weight:700}
.rules-page h2{margin:0 0 12px 0;font-size:2.1rem;font-weight:800}
.rule-section + .rule-section{margin-top:24px;padding-top:24px;border-top:1px solid rgba(255,255,255,0.06)}
.rule-section h3{margin:0 0 12px 0;font-size:1.28rem;font-weight:750;color:#f4f8ff}
.rule-section h3{margin:0 0 12px 0;font-size:1.28rem;font-weight:750;color:var(--text)}
.rule-section h4{margin:18px 0 10px 0;font-size:0.95rem;font-weight:800;letter-spacing:0.08em;text-transform:uppercase;color:var(--accent)}
.rule-section p{margin:0 0 12px 0;line-height:1.7;color:#dce8f5}
.rule-section p{margin:0 0 12px 0;line-height:1.7;color:var(--text)}
.rules-list{margin:0;padding-left:22px}
.rules-list li{margin:0 0 8px 0;line-height:1.55;color:#dce8f5}
.rules-list li{margin:0 0 8px 0;line-height:1.55;color:var(--text)}
.error-shell main{min-height:calc(100vh - 84px);display:flex;align-items:center;justify-content:center;padding:32px 20px 56px}
.error-page{width:min(100%,980px);padding:0;overflow:hidden;border:1px solid rgba(92,197,255,0.12);background:
linear-gradient(140deg,rgba(92,197,255,0.1),rgba(7,19,38,0.95) 34%),
@@ -80,15 +331,33 @@ linear-gradient(180deg,rgba(8,18,32,0.98),rgba(7,19,38,0.94))}
.error-copy{padding:34px 36px}
.error-code{display:inline-block;margin:0 0 16px 0;padding:8px 14px;border-radius:999px;background:rgba(92,197,255,0.1);color:var(--accent);font-size:0.84rem;font-weight:800;letter-spacing:0.16em;text-transform:uppercase}
.error-copy h2{margin:0 0 12px 0;font-size:clamp(2.2rem,5vw,4.6rem);font-weight:900;letter-spacing:0.03em}
.error-copy p{margin:0 0 14px 0;max-width:56ch;line-height:1.7;color:#dce8f5}
.error-copy p{margin:0 0 14px 0;max-width:56ch;line-height:1.7;color:var(--text)}
.error-actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:24px}
.error-actions .btn{font-weight:700;text-decoration:none}
.ghost-btn{background:transparent;color:var(--text);border:1px solid rgba(255,255,255,0.14)}
.auth-main{max-width:520px;margin:0 auto;padding:28px 20px 48px}
.auth-panel{padding:28px 26px}
.auth-panel h2{margin:0 0 8px 0;font-size:1.45rem}
.auth-form{display:grid;gap:16px;margin-top:18px}
.auth-form .auth-hint{margin:-6px 0 0 0;font-size:.82rem}
.auth-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:8px}
.account-main{max-width:820px;margin:0 auto;padding:28px 20px 48px;display:grid;gap:18px}
.account-hero,.account-panel{padding:28px 26px}
.account-hero{background:linear-gradient(145deg,rgba(255,184,77,0.12),rgba(7,19,38,0.96) 42%),radial-gradient(circle at top left,rgba(255,255,255,0.08),transparent 35%);border:1px solid rgba(255,184,77,0.16);border-radius:28px}
.account-hero h2,.account-panel h2,.account-panel h3{margin:0 0 8px 0}
.account-summary-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-top:18px}
.account-summary-card{padding:18px;border-radius:18px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08)}
.account-summary-label{display:block;margin-bottom:8px;font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}
.account-summary-card strong{font-size:1.15rem;color:var(--text)}
.account-panel{border-radius:26px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.08)}
header nav .fedl-auth-nav{margin-left:14px;display:inline-flex;align-items:center;flex-wrap:wrap;gap:8px 10px;vertical-align:middle}
header nav .fedl-auth-nav a{color:var(--muted)}
header nav .fedl-auth-nav a:hover{color:var(--text)}
.error-side{display:flex;flex-direction:column;justify-content:space-between;gap:18px;padding:34px 30px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))}
.error-side h3{margin:0 0 10px 0;font-size:1rem;letter-spacing:0.04em;text-transform:uppercase;color:#eef6ff}
.error-side h3{margin:0 0 10px 0;font-size:1rem;letter-spacing:0.04em;text-transform:uppercase;color:var(--text)}
.error-list{margin:0;padding:0;list-style:none}
.error-list li + li{margin-top:10px}
.error-list a{display:block;padding:12px 14px;border-radius:10px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);color:#eaf2fb;text-decoration:none}
.error-list a{display:block;padding:12px 14px;border-radius:10px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);color:var(--text);text-decoration:none}
.error-list a span{display:block;color:var(--muted);font-size:0.88rem}
.error-tip{margin:0;padding:14px 16px;border-radius:12px;background:rgba(92,197,255,0.08);border:1px solid rgba(92,197,255,0.12);color:#d9ecff}
.error-collection{width:min(100%,1100px)}
@@ -97,20 +366,143 @@ linear-gradient(180deg,rgba(8,18,32,0.98),rgba(7,19,38,0.94))}
.status-card{display:block;padding:18px;border-radius:14px;background:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));border:1px solid rgba(255,255,255,0.06);text-decoration:none;color:var(--text)}
.status-card strong{display:block;margin-bottom:6px;font-size:1.65rem}
.status-card span{display:block;color:var(--muted);line-height:1.5}
.search-row input[type=number],.search-row input[type=url],.search-row input:not([type]){flex:1;padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--text)}
.btn + .btn{margin-left:8px}
.manage-hero h2{margin:0 0 8px 0;font-size:2rem}
.manage-grid{display:grid;grid-template-columns:minmax(320px,1.05fr) minmax(260px,0.95fr);gap:22px}
.manage-form{display:grid;gap:14px}
.manage-form label{display:grid;gap:8px;font-weight:600;color:#eaf3fe}
.manage-form input{padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.02);color:var(--text)}
.manage-actions{display:flex;flex-wrap:wrap;gap:10px}
.manage-actions .btn + .btn{margin-left:0}
.manage-status{margin-top:18px;line-height:1.6}
.manage-search-row{margin-bottom:0}
.table-actions{display:flex;flex-wrap:wrap;gap:8px}
.table-actions .btn{margin-left:0}
.table-link{color:var(--accent);text-decoration:none}
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%}.video-modal iframe{height:320px}.roulette-head{flex-direction:column;align-items:flex-start}.discord-panel{flex-direction:column;align-items:flex-start;padding:22px 20px}.discord-panel h2{font-size:1.55rem}.discord-actions{width:100%;justify-content:flex-start}.rules-view header{padding:16px 18px}.rules-view header h1{font-size:1.55rem}.rules-page{padding:24px 20px}.rules-page h2{font-size:1.65rem}}
@media(max-width:900px){.layout{flex-direction:column}.sidebar{width:100%}.sidebar .panel{height:auto}.video-modal iframe{height:320px}.modal-runs-head,.modal-run-card{grid-template-columns:1fr}.home-hero{grid-template-columns:1fr;min-height:auto}.home-copy,.hero-stage{padding:24px 22px}.hero-stats,.home-section-grid,.spotlight-grid{grid-template-columns:1fr}.spotlight-panel{grid-template-columns:1fr;padding:24px 22px}.list-hero-panel,.table-header,.search-row,.roulette-head,.guess-controls{flex-direction:column;align-items:flex-start}.control-select{max-width:none;width:100%}.roulette-shell{grid-template-columns:1fr}.discord-panel{flex-direction:column;align-items:flex-start;padding:22px 20px}.discord-panel h2{font-size:1.55rem}.discord-actions{width:100%;justify-content:flex-start}.rules-view header{padding:16px 18px}.rules-view header h1{font-size:1.55rem}.rules-page{padding:24px 20px}.rules-page h2{font-size:1.65rem}.guess-controls input,.guess-controls .btn{width:100%}}
@media(max-width:900px){.error-grid{grid-template-columns:1fr}.error-copy,.error-side{padding:24px 20px}.error-copy h2{font-size:2.5rem}}
@media(max-width:900px){.manage-grid{grid-template-columns:1fr}}
@media(max-width:900px){.admin-overview-panel,.admin-toolbar{flex-direction:column;align-items:stretch}.admin-actions{flex-direction:column;align-items:stretch}.admin-password-field input,.admin-actions input[type=text],.admin-actions input[type=password]{min-width:0;width:100%}.run-shell{grid-template-columns:1fr}.run-head{flex-direction:column;align-items:flex-start}.run-form{grid-template-columns:1fr}.admin-login-screen{padding:18px}.admin-login-card{padding:26px 22px}}
@media(max-width:640px){header{padding:12px 16px;flex-wrap:wrap;gap:10px}header h1{font-size:1rem}nav{display:flex;flex-wrap:wrap;gap:6px;justify-content:center}nav a{padding:8px 12px;border-radius:20px;font-size:0.85rem}header nav a.btn{margin-left:0}.home-hero{grid-template-columns:1fr;min-height:auto;gap:16px}.home-copy,.hero-stage{padding:20px 18px}.home-copy h2{font-size:2rem;max-width:none}.hero-text{font-size:0.95rem}.hero-actions{flex-direction:column;align-items:stretch;gap:10px}.hero-actions .btn,.roulette-btn{width:100%;text-align:center}.hero-stats{grid-template-columns:1fr;gap:10px}.spotlight-panel{grid-template-columns:1fr;padding:20px 18px}.spotlight-grid{grid-template-columns:1fr;gap:12px}.home-section-grid{grid-template-columns:1fr;gap:12px}.info-panel{padding:18px 16px}.list-hero-panel,.list-controls-panel,.list-table-panel,.roulette-panel,.roulette-rules-card,.run-panel,.run-side-panel,.submission-panel,.account-hero,.account-panel{padding:18px 16px}.table-header{flex-direction:column;align-items:flex-start;gap:12px}.search-row{flex-direction:column;align-items:stretch}.control-select{max-width:none;width:100%}.table-hint{text-align:left}.roulette-shell,.guess-controls{flex-direction:column;gap:12px}.guess-controls input,.guess-controls .btn{width:100%}.account-summary-grid{grid-template-columns:1fr}.theme-options{grid-template-columns:repeat(2,1fr);gap:12px}.run-admin-actions{flex-direction:column;align-items:stretch}}
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:14px;margin-top:16px}
.stat-box{padding:16px;border-radius:14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);text-align:center}
.stat-box strong{display:block;margin-bottom:6px;font-size:1.75rem;color:var(--accent)}
.stat-box span{display:block;color:var(--muted);font-size:.9rem}
.stat-box-small{padding:12px}
.stat-box-small strong{font-size:1.25rem}
.stat-box-small span{font-size:.8rem}
.bug-report-detail-row td{padding-top:0}
.bug-report-detail{display:grid;gap:8px;padding:0 0 14px 0;color:var(--text)}
.bug-report-detail a{color:var(--accent)}
.admin-tabs{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
.admin-tab{padding:10px 18px;border-radius:999px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:var(--muted);cursor:pointer;font-weight:700;transition:all .2s ease}
.admin-tab:hover{background:rgba(255,255,255,0.08);color:var(--text)}
.admin-tab.active{background:var(--accent);color:var(--bg)}
.admin-tab-panel{display:block}
.admin-tab-panel[hidden]{display:none}
.theme-options{display:flex;gap:16px;flex-wrap:wrap;margin:16px 0}
.theme-option{display:flex;flex-direction:column;align-items:center;gap:8px;cursor:pointer}
.theme-option input{display:none}
.theme-option:has(input:checked) .theme-preview{outline:2px solid var(--accent);outline-offset:2px}
.theme-preview{width:60px;height:40px;border-radius:8px;border:1px solid rgba(255,255,255,0.1)}
.theme-preview-dark{background:linear-gradient(135deg,#0f1724,#1a2744)}
.theme-preview-light{background:linear-gradient(135deg,#f0f4f8,#d9e2ec)}
.theme-preview-blue{background:linear-gradient(135deg,#0d1b2a,#1b3a5f)}
.theme-preview-midnight{background:linear-gradient(135deg,#0a0a12,#1a1a2e)}
.theme-preview-cyberpunk{background:linear-gradient(135deg,#0f0f1a,#2d1b4e)}
.theme-preview-earth{background:linear-gradient(135deg,#1a2f1a,#2d4a2d)}
.theme-preview-retro{background:linear-gradient(135deg,#2b1a0a,#4a2c17)}
.theme-preview-matrix{background:linear-gradient(135deg,#000a00,#001100)}
.theme-preview-synthwave{background:linear-gradient(135deg,#1a0a2e,#ff2a6d)}
.theme-preview-fire{background:linear-gradient(135deg,#1a0505,#ff4500)}
.theme-preview-galaxy{background:linear-gradient(135deg,#0a0612,#e056fd)}
.theme-preview-candy{background:linear-gradient(135deg,#fdf2f8,#f472b6)}
.theme-preview-highcontrast{background:linear-gradient(135deg,#000,#fff)}
.theme-preview-original{background:linear-gradient(135deg,#0f1724,#071326)}
[data-theme="cyberpunk"] .btn{box-shadow:0 0 12px var(--accent), inset 0 0 8px rgba(0,255,159,0.1);position:relative;overflow:hidden}
[data-theme="cyberpunk"] .btn::after{content:"";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,255,159,0.3),transparent);animation:cyberglitch 3s infinite}
@keyframes cyberglitch{0%{left:-100%}50%,100%{left:100%}}
[data-theme="cyberpunk"] .panel{border:1px solid rgba(0,255,159,0.3);box-shadow:0 0 20px rgba(0,255,159,0.1),inset 0 0 30px rgba(0,255,159,0.02)}
[data-theme="cyberpunk"] h1,[data-theme="cyberpunk"] h2,[data-theme="cyberpunk"] h3{text-shadow:0 0 10px var(--accent)}
[data-theme="cyberpunk"] header{position:relative;overflow:hidden}
[data-theme="cyberpunk"] header::after{content:"";position:absolute;bottom:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:scanline 4s linear infinite}
@keyframes scanline{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
[data-theme="earth"] .panel{border:1px solid rgba(132,204,22,0.2);background:linear-gradient(180deg,var(--panel),color-mix(in srgb,var(--accent) 5%,var(--panel)))}
[data-theme="earth"] header{background:linear-gradient(90deg,color-mix(in srgb,#1a2f1a 90%,var(--accent)),color-mix(in srgb,#2d4a2d 80%,var(--accent)))}
[data-theme="earth"] .btn{border:2px solid var(--accent);box-shadow:0 4px 12px color-mix(in srgb,var(--accent) 30%,transparent);position:relative;overflow:hidden}
[data-theme="earth"] .btn::before{content:"🌿";position:absolute;left:6px;font-size:0.7rem;z-index:1}
[data-theme="earth"] .account-summary-card{position:relative}
[data-theme="earth"] .account-summary-card::before{content:"🌲";position:absolute;top:-8px;right:-4px;font-size:1rem}
[data-theme="earth"] nav a:hover{color:var(--accent-warm)}
[data-theme="earth"] .hero-kicker::before{content:"🍃 "}
[data-theme="earth"] .hero-kicker::after{content:" 🍃"}
[data-theme="earth"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 10%,transparent),transparent 30%),linear-gradient(180deg,var(--bg),var(--panel))}
[data-theme="earth"] header{position:relative}
[data-theme="earth"] header::after{content:"";position:absolute;bottom:0;left:0;right:0;height:3px;background:linear-gradient(90deg,transparent,var(--accent),transparent)}
[data-theme="earth"] nav a:hover{color:var(--accent-warm);transform:scale(1.05)}
[data-theme="dark"] .btn{border-radius:8px;box-shadow:0 4px 15px rgba(92,197,255,0.3)}
[data-theme="dark"] .btn:hover{box-shadow:0 6px 25px rgba(92,197,255,0.5)}
[data-theme="light"] .btn{border-radius:4px;background:linear-gradient(135deg,var(--accent),#0ea5e9);border:2px solid #0369a1}
[data-theme="light"] .btn:hover{transform:translateY(-2px);box-shadow:0 6px 15px rgba(2,132,199,0.4)}
[data-theme="blue"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 15%,transparent),transparent 28%),linear-gradient(180deg,#071826 0%,#0d1b2a 100%)}
[data-theme="blue"] .btn{border-radius:20px;clip-path:polygon(10px 0,100% 0,calc(100% - 10px) 100%,0 100%)}
[data-theme="blue"] .btn:hover{animation:waveshiver 0.5s ease}
@keyframes waveshiver{0%,100%{transform:translateX(0)}25%{transform:translateX(2px)}75%{transform:translateX(-2px)}}
[data-theme="midnight"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 10%,transparent),transparent 28%),linear-gradient(180deg,#05050a 0%,#0a0a12 100%)}
[data-theme="midnight"] .btn{border-radius:2px 8px 2px 8px;border:1px solid var(--accent);background:transparent;color:var(--accent)}
[data-theme="midnight"] .btn:hover{background:var(--accent);color:var(--bg);animation:pulse 1s infinite}
@keyframes pulse{0%,100%{box-shadow:0 0 5px var(--accent)}50%{box-shadow:0 0 20px var(--accent)}}
[data-theme="cyberpunk"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 12%,transparent),transparent 28%),linear-gradient(180deg,#0a0512 0%,#0f0f1a 100%)}
[data-theme="cyberpunk"] .btn{border-radius:2px;clip-path:polygon(0 0,calc(100% - 8px) 0,100% 8px,100% 100%,8px 100%,0 calc(100% - 8px));border:1px solid var(--accent);box-shadow:0 0 15px var(--accent),inset 0 0 10px rgba(0,255,159,0.2);position:relative;overflow:hidden}
[data-theme="cyberpunk"] .btn::after{content:"";position:absolute;top:0;left:-100%;width:100%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,255,159,0.4),transparent);animation:cyberglitch 2s infinite}
@keyframes cyberglitch{0%{left:-100%}50%,100%{left:100%}}
[data-theme="retro"] body{background:radial-gradient(circle at top,color-mix(in srgb,var(--accent) 10%,transparent),transparent 28%),linear-gradient(180deg,#120c05 0%,#1a1208 100%);position:relative}
[data-theme="retro"] body::after{content:"";position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.3) 2px,rgba(0,0,0,0.3) 4px);pointer-events:none;z-index:9999;animation:scanlines 0.1s linear infinite}
[data-theme="retro"] .panel{border:2px dashed var(--accent);box-shadow:inset 0 0 20px rgba(255,159,28,0.1)}
[data-theme="retro"] .btn{border-radius:0;border:3px double var(--accent);background:transparent;color:var(--accent);text-transform:uppercase;letter-spacing:2px;position:relative}
[data-theme="retro"] .btn:hover{background:var(--accent);color:var(--bg);box-shadow:0 0 15px var(--accent)}
@keyframes scanlines{0%{background-position:0 0}100%{background-position:0 4px}}
[data-theme="matrix"] body{background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,255,0,0.03) 2px,rgba(0,255,0,0.03) 4px),linear-gradient(180deg,#000a00,#001100);position:relative;overflow:hidden}
[data-theme="matrix"] body::before{content:"101011001 01101001 10101010 01010101";position:fixed;top:0;left:0;right:0;bottom:0;font-family:monospace;font-size:14px;line-height:20px;color:rgba(0,255,0,0.05);overflow:hidden;pointer-events:none;white-space:pre-wrap;word-break:break-all;animation:matrixfall 20s linear infinite;z-index:-1}
@keyframes matrixfall{0%{transform:translateY(0)}100%{transform:translateY(-50%)}}
[data-theme="matrix"] .panel{border:1px solid var(--accent);box-shadow:0 0 10px var(--accent)}
[data-theme="matrix"] .btn{font-family:monospace;border-radius:0;border:1px solid var(--accent);background:#001100;text-shadow:0 0 5px var(--accent);letter-spacing:1px;position:relative;overflow:hidden}
[data-theme="matrix"] .btn::after{content:">";position:absolute;right:8px;opacity:0;transition:opacity 0.2s}
[data-theme="matrix"] .btn:hover::after{opacity:1}
[data-theme="matrix"] header::before{content:"wake up, neo...";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-family:monospace;font-size:0.6rem;opacity:0.1;color:var(--accent);white-space:nowrap}
[data-theme="synthwave"] body{background:linear-gradient(180deg,#1a0a2e 0%,#2d1b4e 50%,#0f0f1a 100%);position:relative}
[data-theme="synthwave"] body::after{content:"";position:fixed;bottom:0;left:0;right:0;height:200px;background:linear-gradient(0deg,rgba(5,217,232,0.1) 0%,transparent 100%);clip-path:polygon(0 100%,5% 80%,10% 90%,15% 70%,20% 85%,25% 60%,30% 75%,35% 65%,40% 80%,45% 55%,50% 70%,55% 60%,60% 75%,65% 50%,70% 65%,75% 55%,80% 70%,85% 60%,90% 75%,95% 65%,100% 80%,100% 100%);pointer-events:none}
[data-theme="synthwave"] .panel{border:2px solid var(--accent);box-shadow:0 0 20px var(--accent),inset 0 0 20px rgba(255,42,109,0.1)}
[data-theme="synthwave"] .btn{border-radius:0;clip-path:polygon(15px 0,100% 0,100% calc(100% - 15px),calc(100% - 15px) 100%,0 100%,0 15px);background:linear-gradient(135deg,var(--accent),#05d9e8);color:#1a0a2e;font-weight:900;text-transform:uppercase;position:relative}
[data-theme="synthwave"] .btn::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.3),transparent);animation:synthwaveglitch 2s infinite}
@keyframes synthwaveglitch{0%{transform:translateX(-100%)}50%,100%{transform:translateX(100%)}}
[data-theme="synthwave"] h1,[data-theme="synthwave"] h2,[data-theme="synthwave"] h3{font-family:"Courier New",monospace;text-transform:uppercase;letter-spacing:0.1em}
[data-theme="fire"] body{background:radial-gradient(ellipse at bottom,rgba(255,69,0,0.2) 0%,transparent 60%),linear-gradient(180deg,#1a0505,#2d0a0a);position:relative}
[data-theme="fire"] body::after{content:"";position:fixed;inset:0;background:radial-gradient(ellipse at bottom,rgba(255,100,0,0.1) 0%,transparent 70%);pointer-events:none;animation:flameflicker 3s ease-in-out infinite}
@keyframes flameflicker{0%,100%{opacity:0.5}50%{opacity:1}}
[data-theme="fire"] .panel{border:1px solid rgba(255,69,0,0.4);box-shadow:0 0 30px rgba(255,69,0,0.2)}
[data-theme="fire"] .btn{border-radius:50px;background:linear-gradient(180deg,#ff4500,#ff6b35);box-shadow:0 4px 20px rgba(255,69,0,0.5),inset 0 2px 0 rgba(255,255,255,0.2);border:none;text-shadow:0 1px 2px rgba(0,0,0,0.3);position:relative;overflow:hidden}
[data-theme="fire"] .btn::after{content:"";position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:linear-gradient(45deg,transparent,rgba(255,255,255,0.1),transparent);transform:rotate(45deg);animation:fireglint 2s infinite}
@keyframes fireglint{0%{transform:translateX(-100%) rotate(45deg)}100%{transform:translateX(100%) rotate(45deg)}}
[data-theme="fire"] header{background:linear-gradient(90deg,rgba(255,69,0,0.2),rgba(255,170,0,0.2))}
[data-theme="galaxy"] body{background:radial-gradient(circle at 30% 20%,rgba(224,86,253,0.15),transparent 50%),radial-gradient(circle at 70% 80%,rgba(249,202,36,0.1),transparent 40%),linear-gradient(180deg,#0a0612,#150f25);position:relative}
[data-theme="galaxy"] body::before{content:"";position:fixed;inset:0;background-image:radial-gradient(2px 2px at 20px 30px,rgba(255,255,255,0.3),transparent),radial-gradient(2px 2px at 40px 70px,rgba(255,255,255,0.2),transparent),radial-gradient(1px 1px at 90px 40px,rgba(255,255,255,0.4),transparent),radial-gradient(2px 2px at 130px 80px,rgba(255,255,255,0.2),transparent),radial-gradient(1px 1px at 160px 120px,rgba(255,255,255,0.5),transparent);background-size:200px 200px;animation:twinkle 4s ease-in-out infinite;pointer-events:none}
@keyframes twinkle{0%,100%{opacity:0.5}50%{opacity:1}}
[data-theme="galaxy"] .panel{background:linear-gradient(135deg,var(--panel),color-mix(in srgb,var(--accent) 20%,var(--card)));border:1px solid rgba(224,86,253,0.3)}
[data-theme="galaxy"] .btn{border-radius:30px;background:linear-gradient(135deg,var(--accent),var(--accent-warm));color:#0a0612;box-shadow:0 0 20px color-mix(in srgb,var(--accent) 50%,transparent);position:relative;overflow:hidden}
[data-theme="galaxy"] .btn::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.4),transparent);animation:galaxysparkle 2s infinite}
@keyframes galaxysparkle{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
[data-theme="galaxy"] header{background:linear-gradient(90deg,rgba(224,86,253,0.2),rgba(249,202,36,0.1))}
[data-theme="candy"] body{background:radial-gradient(circle at top left,rgba(244,114,182,0.2),transparent 40%),radial-gradient(circle at bottom right,rgba(52,211,153,0.15),transparent 40%),linear-gradient(180deg,#fdf2f8,#fce7f3);position:relative;overflow:hidden}
[data-theme="candy"] body::before{content:"";position:fixed;inset:0;background:url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M30 5 L35 25 L55 30 L35 35 L30 55 L25 35 L5 30 L25 25 Z' fill='none' stroke='rgba(244,114,182,0.08)' stroke-width='1'/%3E%3C/svg%3E");pointer-events:none;animation:drift 20s linear infinite}
@keyframes drift{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
[data-theme="candy"] .panel{background:linear-gradient(180deg,var(--panel),#fff);border:2px solid rgba(244,114,182,0.3);border-radius:20px}
[data-theme="candy"] .btn{border-radius:25px;background:linear-gradient(135deg,#f472b6,#34d399);box-shadow:0 6px 20px rgba(244,114,182,0.4);border:none;position:relative;overflow:hidden}
[data-theme="candy"] .btn::before{content:"";position:absolute;top:50%;left:50%;width:0;height:0;background:rgba(255,255,255,0.5);border-radius:50%;transform:translate(-50%,-50%);transition:width 0.3s,height 0.3s}
[data-theme="candy"] .btn:active::before{width:200px;height:200px}
[data-theme="candy"] header{background:linear-gradient(90deg,rgba(244,114,182,0.15),rgba(52,211,153,0.1))}
[data-theme="candy"] nav a:hover{color:var(--accent)}
[data-theme="highcontrast"] body{background:#000}
[data-theme="highcontrast"] .panel{border:3px solid var(--accent)}
[data-theme="highcontrast"] .btn{background:var(--accent);color:var(--bg);border:3px solid var(--text);border-radius:0;font-weight:900;letter-spacing:1px;text-transform:uppercase}
[data-theme="highcontrast"] .btn:hover{background:var(--text);color:var(--bg);box-shadow:0 0 0 3px var(--accent)}
[data-theme="highcontrast"] header{border-bottom:3px solid var(--accent)}
[data-theme="highcontrast"] nav a{font-weight:700}
[data-theme="highcontrast"] nav a:hover{background:var(--accent);color:var(--bg)}
.skeleton{background:linear-gradient(90deg,rgba(255,255,255,0.05) 25%,rgba(255,255,255,0.1) 50%,rgba(255,255,255,0.05) 75%);background-size:200% 100%;animation:skeleton-loading 1.5s ease-in-out infinite;border-radius:4px}
.skeleton-text{height:1em;margin:0.5em 0}
.skeleton-title{height:1.5em;width:60%;margin:0.5em 0}
.skeleton-box{height:80px;border-radius:8px}
@keyframes skeleton-loading{0%{background-position:200% 0}100%{background-position:-200% 0}}
.page-transition{opacity:0;transition:opacity 0.3s ease}
.page-transition.loaded{opacity:1}
body{animation:page-enter 0.3s ease}
@keyframes page-enter{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
+100
View File
@@ -0,0 +1,100 @@
<!doctype html>
<html lang="en">
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6951907799659255"
crossorigin="anonymous"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content="FEDL Support - Get help with submissions, rules, and technical issues.">
<meta name="keywords" content="support, help, fedl, geometry dash, demon list">
<title>FEDL - Support</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="pageicon.png">
</head>
<body data-page="support">
<header>
<h1>FEDL - Support</h1>
<nav>
<a href="index.html">Home</a>
<a href="lists.html">Lists</a>
<a href="players.html">Players</a>
<a href="run.html">Submit Run</a>
<a href="roulette.html">Roulette</a>
<a href="rules.html">Rules</a>
<a href="messages.html">Messages</a>
<a href="contact.html">Contact</a>
<a href="about.html">About</a>
</nav>
</header>
<main>
<section class="panel">
<p class="status-label">Support</p>
<h2>Need Help?</h2>
<p class="muted">Find answers to common questions below. If you still need assistance, use the contact page to reach out.</p>
<details class="faq-item">
<summary>How do I submit a run?</summary>
<p>Go to the Submit Run page, fill out the form with your video link, raw footage, and notes. Your submission will be reviewed by moderators.</p>
</details>
<details class="faq-item">
<summary>Why was my run rejected?</summary>
<p>Common reasons include missing raw footage, mod usage violations, or incomplete submission details. Check the Rules page for full requirements.</p>
</details>
<details class="faq-item">
<summary>How are demons ranked?</summary>
<p>Demon rankings are based on difficulty, creativity, and popularity. The list is regularly updated by our moderation team.</p>
</details>
<details class="faq-item">
<summary>Can I suggest a level for the list?</summary>
<p>Yes! Use the contact page to suggest levels. Include the level ID and why you think it deserves to be ranked.</p>
</details>
</section>
<section class="panel">
<p class="status-label">Advertisements</p>
<h2>Support FEDL</h2>
<p class="muted">This website displays ads to help cover hosting and maintenance costs. Thank you for your support!</p>
<div class="ad-container">
<ins class="adsbygoogle"
data-ad-client="YOUR-AD-CLIENT-ID"
data-ad-slot="YOUR-AD-SLOT-ID"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
</div>
</section>
<section class="panel">
<p class="status-label">Resources</p>
<h2>Quick Links</h2>
<div class="spotlight-grid">
<article class="spotlight-card">
<h3>Rules</h3>
<p>Learn about submission requirements and moderation policies.</p>
<a class="text-link" href="rules.html">View Rules</a>
</article>
<article class="spotlight-card">
<h3>Submit Run</h3>
<p>Send in your completion for review.</p>
<a class="text-link" href="run.html">Submit</a>
</article>
<article class="spotlight-card">
<h3>Contact</h3>
<p>Get in touch with the moderation team.</p>
<a class="text-link" href="contact.html">Contact Us</a>
</article>
</div>
</section>
</main>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
2.26