mlv2boii 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html CHANGED
@@ -1,39 +1,47 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
6
  <title>mlv2</title>
7
- <meta name="description" content="mlv2 — a small, fast library of games." />
7
+ <meta name="description" content="mlv2: a small, fast library of games." />
8
8
  <meta name="color-scheme" content="dark" />
9
- <meta name="theme-color" content="#09090b" />
10
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%2309090b'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%23e8142f'/%3E%3C/svg%3E" />
9
+ <meta name="theme-color" content="#000000" />
10
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23000'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%23e8142f'/%3E%3C/svg%3E" />
11
11
 
12
- <!-- Fonts -->
12
+ <!-- Fonts: SF Pro on Apple devices (system stack), Inter everywhere else -->
13
13
  <link rel="preconnect" href="https://fonts.googleapis.com" />
14
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
15
- <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
15
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
16
16
 
17
17
  <!-- Tailwind CSS v4 (Play CDN, mirrored on jsDelivr) -->
18
18
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
19
19
  <style type="text/tailwindcss">
20
+ /*
21
+ Design language
22
+ Mood: quiet, precise, confident. Airy. Ancestor: Vercel/Resend black, Apple HIG habits.
23
+ Color: void black canvas, Apple grays for text, ONE red for tint (selection, primary action, focus).
24
+ Type: one family. 17/19/21 for reading, 13 for labels, 44-72 for the title.
25
+ Shape: 10px controls, 12px groups, 28px sheets (concentric: 12 + 16 padding), pills for chips,
26
+ 22.37% on game icons (the iOS app icon ratio).
27
+ Motion: 150-300ms ease-out on interaction, one entrance, all of it off under reduced-motion.
28
+ */
20
29
  @theme {
21
- --font-display: "Sora", ui-sans-serif, system-ui, sans-serif;
22
- --font-sans: "Manrope", ui-sans-serif, system-ui, sans-serif;
23
- --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace;
24
-
25
- --color-ink-950: #09090b;
26
- --color-ink-900: #0e0e11;
27
- --color-ink-850: #131316;
28
- --color-ink-800: #18181c;
29
- --color-ink-700: #202024;
30
- --color-ink-600: #2a2a30;
31
- --color-ink-500: #3f3f47;
32
- --color-ink-400: #6e6e78;
33
- --color-ink-300: #98979f;
34
- --color-ink-200: #c2c1c7;
35
- --color-ink-100: #e2e1e6;
36
- --color-ink-50: #f6f5f7;
30
+ --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", ui-sans-serif, system-ui, sans-serif;
31
+ --font-mono: ui-monospace, "SF Mono", Menlo, monospace;
32
+
33
+ --color-ink-950: #000000;
34
+ --color-ink-900: #0a0a0b;
35
+ --color-ink-850: #101012;
36
+ --color-ink-800: #161618;
37
+ --color-ink-700: #232326;
38
+ --color-ink-600: #2c2c30;
39
+ --color-ink-500: #48484c;
40
+ --color-ink-400: #76767b;
41
+ --color-ink-300: #a1a1a6;
42
+ --color-ink-200: #c7c7cc;
43
+ --color-ink-100: #e5e5ea;
44
+ --color-ink-50: #f5f5f7;
37
45
 
38
46
  --color-red-700: #8f0e20;
39
47
  --color-red-600: #c11228;
@@ -41,147 +49,143 @@
41
49
  --color-red-400: #ff4d61;
42
50
  --color-red-300: #ff8b96;
43
51
  }
52
+
53
+ @layer components {
54
+ .btn { @apply inline-flex h-11 items-center justify-center rounded-[10px] px-4 text-[15px] font-medium transition-[background-color,filter,transform] duration-200 active:scale-[0.98] disabled:opacity-50 sm:h-9 sm:text-[14px]; }
55
+ .btn-primary { @apply bg-red-500 text-white hover:brightness-110; }
56
+ .btn-ghost { @apply bg-white/10 text-ink-50 hover:bg-white/15; }
57
+ .icon-btn { @apply grid h-11 w-11 shrink-0 place-items-center rounded-full bg-white/10 text-ink-200 transition-colors duration-200 hover:bg-white/15 hover:text-white sm:h-9 sm:w-9; }
58
+ .field { @apply h-11 w-full rounded-[10px] bg-white/[0.07] px-3 text-[16px] text-ink-50 ring-1 ring-inset ring-white/10 transition-shadow duration-200 placeholder:text-ink-400 focus:ring-2 focus:ring-red-500 sm:h-9 sm:text-[14px]; }
59
+ .chip { @apply h-10 shrink-0 whitespace-nowrap rounded-full px-4 text-[14px] font-medium transition-colors duration-200 sm:h-9; }
60
+ }
44
61
  </style>
45
62
 
46
63
  <style>
47
- /* ---------------------------------------------------------------
48
- Base layer — kept outside the Tailwind @theme block so it works
49
- regardless of Tailwind's preflight timing.
50
- ----------------------------------------------------------------*/
51
- html, body { background: var(--color-ink-950); }
64
+ html, body { background: #000; }
52
65
  body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
53
- ::selection { background: rgba(232,20,47,0.35); color: #fff; }
66
+ ::selection { background: rgba(232,20,47,0.4); color: #fff; }
67
+ input { caret-color: var(--color-red-400); }
68
+
69
+ /* keyboard focus: one red ring everywhere */
70
+ :focus-visible { outline: 2px solid var(--color-red-400); outline-offset: 2px; }
71
+ input:focus-visible { outline: none; }
72
+ .tile:focus-visible { outline: none; }
73
+ .tile:focus-visible .tile-icon { outline: 2px solid var(--color-red-400); outline-offset: 3px; }
74
+
75
+ /* safe areas (viewport-fit=cover) */
76
+ .page-x { padding-left: max(1.25rem, env(safe-area-inset-left)); padding-right: max(1.25rem, env(safe-area-inset-right)); }
54
77
 
55
- /* thin, quiet scrollbars */
56
- * { scrollbar-width: thin; scrollbar-color: #2a2a30 transparent; }
78
+ /* game icons: 10% edge so dark art never melts into the canvas */
79
+ .tile-img { outline: 1px solid rgba(255,255,255,0.1); outline-offset: -1px; }
80
+
81
+ .no-scrollbar { scrollbar-width: none; }
82
+ .no-scrollbar::-webkit-scrollbar { display: none; }
83
+ * { scrollbar-width: thin; scrollbar-color: #2c2c30 transparent; }
57
84
  *::-webkit-scrollbar { width: 8px; height: 8px; }
58
- *::-webkit-scrollbar-track { background: transparent; }
59
- *::-webkit-scrollbar-thumb { background: #26262b; border-radius: 999px; }
60
- *::-webkit-scrollbar-thumb:hover { background: #3a3a42; }
61
-
62
- /* film-grain texture, very subtle, for depth without gradients */
63
- .grain { position: fixed; inset: 0; pointer-events: none; z-index: 0; opacity: 0.035; mix-blend-mode: overlay;
64
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
65
-
66
- .spotlight { position: fixed; top: -20%; left: 50%; width: 1100px; height: 600px; transform: translateX(-50%);
67
- background: radial-gradient(closest-side, rgba(232,20,47,0.16), transparent 70%); pointer-events: none; z-index: 0; }
68
-
69
- /* skeleton shimmer */
70
- @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
71
- .skel { background: linear-gradient(100deg, #18181c 30%, #202024 45%, #18181c 60%); background-size: 200% 100%; animation: shimmer 1.6s ease-in-out infinite; }
72
-
73
- [data-fx] { will-change: transform, opacity; }
74
-
75
- .hero-orb { animation: heroFloat 7s ease-in-out infinite; }
76
- .hero-line { animation: heroLine 2.8s ease-in-out infinite; }
77
- @keyframes heroFloat {
78
- 0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
79
- 50% { transform: translate3d(0, 12px, 0) scale(1.04); }
80
- }
81
- @keyframes heroLine {
82
- 0%, 100% { opacity: .35; transform: scaleX(.7); }
83
- 50% { opacity: 1; transform: scaleX(1); }
84
- }
85
- </style>
85
+ *::-webkit-scrollbar-thumb { background: #2c2c30; border-radius: 999px; }
86
+
87
+ /* the single page-load moment: title and subline rise in */
88
+ @keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
89
+ .rise { animation: rise 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation-delay: var(--d, 0s); }
90
+ @media (prefers-reduced-motion: reduce) { .rise { animation: none; } }
91
+
92
+ /* Blank Overlay Lock Screen */
93
+ #blank-overlay {
94
+ position: fixed;
95
+ inset: 0;
96
+ background-color: #000000;
97
+ z-index: 99999;
98
+ display: flex;
99
+ transition: opacity 0.4s ease, visibility 0.4s ease;
100
+ }
101
+ #blank-overlay.unlocked {
102
+ opacity: 0;
103
+ visibility: hidden;
104
+ pointer-events: none;
105
+ }
106
+ </style>
86
107
  </head>
87
108
 
88
- <body class="bg-ink-950 text-ink-100 font-sans antialiased min-h-screen">
89
- <div class="grain"></div>
90
- <div class="spotlight"></div>
109
+ <body class="min-h-screen bg-ink-950 font-sans text-ink-50 antialiased">
110
+
111
+ <!-- Blank Overlay Screen -->
112
+ <div id="blank-overlay"></div>
91
113
 
92
114
  <!-- ================= HEADER ================= -->
93
- <header class="sticky top-0 z-40 border-b border-ink-800/80 bg-ink-950/75 backdrop-blur-md">
94
- <div class="mx-auto max-w-6xl px-4 sm:px-6 h-14 flex items-center gap-3">
95
- <a href="#" class="flex items-center gap-2 shrink-0 group">
96
- <span class="relative h-2 w-2">
97
- <span class="absolute inset-0 rounded-full bg-red-500"></span>
98
- <span class="absolute inset-0 animate-ping rounded-full bg-red-500 opacity-60"></span>
99
- </span>
100
- <span class="font-display font-semibold tracking-tight text-[15px] leading-none">mlv2</span>
115
+ <header class="sticky top-0 z-40 border-b border-white/[0.08] bg-black/70 pt-[env(safe-area-inset-top)] backdrop-blur-xl backdrop-saturate-150">
116
+ <div class="page-x mx-auto flex h-14 max-w-5xl items-center gap-4">
117
+ <a href="#" class="flex shrink-0 items-center gap-2" aria-label="mlv2 home">
118
+ <span class="h-2.5 w-2.5 rounded-full bg-red-500"></span>
119
+ <span class="text-[17px] font-semibold tracking-[-0.02em]">mlv2</span>
101
120
  </a>
102
121
 
103
- <div class="flex-1 max-w-xs sm:max-w-sm relative">
104
- <svg class="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-ink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
105
- <input id="search" type="text" placeholder="Search games…" autocomplete="off"
106
- class="w-full rounded-lg border border-ink-700 bg-ink-900/70 py-1.5 pl-8 pr-3 text-[13px] placeholder:text-ink-400 outline-none transition-colors focus:border-red-500/60 focus:bg-ink-900" />
122
+ <div class="ml-auto flex min-w-0 flex-1 items-center justify-end gap-4">
123
+ <span id="count-badge" class="hidden shrink-0 text-[13px] tabular-nums text-ink-400 sm:block"><span id="count-num">0</span>&nbsp;games</span>
124
+ <div class="relative w-full max-w-[16rem]">
125
+ <svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-ink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
126
+ <input id="search" type="text" placeholder="Search games" aria-label="Search games" autocomplete="off" enterkeyhint="search" class="field pl-9" />
127
+ </div>
107
128
  </div>
108
-
109
- <span id="count-badge" class="hidden sm:flex items-center gap-1.5 shrink-0 text-[11px] font-mono uppercase tracking-wider text-ink-400">
110
- <span id="count-num">0</span>&nbsp;games
111
- </span>
112
- </div>
113
-
114
- <div class="border-t border-ink-800/60">
115
- <div id="category-bar" class="mx-auto flex max-w-5xl items-center justify-center gap-1.5 overflow-x-auto px-4 py-2.5 sm:px-6"></div>
116
129
  </div>
117
130
  </header>
118
131
 
119
132
  <!-- ================= MAIN ================= -->
120
- <main class="relative z-10 mx-auto max-w-5xl px-4 sm:px-6 py-8 sm:py-12">
121
-
122
- <!-- ================= HERO ================= -->
123
- <section class="relative mx-auto mb-10 max-w-2xl overflow-hidden rounded-[2rem] border border-ink-700/70 bg-ink-900/65 px-5 py-10 text-center shadow-[0_24px_80px_-40px_rgba(232,20,47,0.5)] sm:px-10 sm:py-14">
124
- <div class="hero-orb pointer-events-none absolute -right-20 -top-24 h-64 w-64 rounded-full bg-red-500/10 blur-3xl"></div>
125
- <div class="hero-orb pointer-events-none absolute -bottom-28 -left-20 h-56 w-56 rounded-full bg-red-700/10 blur-3xl" style="animation-delay: -3.5s"></div>
126
- <div class="relative">
127
- <div class="mx-auto mb-5 flex w-fit items-center gap-2 rounded-full border border-red-500/25 bg-red-500/[0.07] px-3 py-1 text-[10px] font-mono uppercase tracking-[0.22em] text-red-300">
128
- <span class="h-1.5 w-1.5 rounded-full bg-red-400 shadow-[0_0_12px_rgba(255,77,97,0.9)]"></span>
129
- Play something
130
- </div>
131
- <h1 class="font-display text-4xl font-semibold tracking-[-0.05em] text-ink-50 sm:text-6xl">Your next <span class="text-red-400">favorite</span> game.</h1>
132
- <p class="mx-auto mt-4 max-w-md text-sm leading-6 text-ink-300 sm:text-[15px]">A curated shelf of quick escapes, hidden gems, and games worth one more round.</p>
133
- <div class="hero-line mx-auto mt-7 h-px w-24 bg-gradient-to-r from-transparent via-red-400 to-transparent"></div>
134
- </div>
135
- </section>
133
+ <main class="page-x mx-auto max-w-5xl pb-24">
134
+
135
+ <!-- ================= TITLE ================= -->
136
+ <section class="pb-10 pt-14 sm:pb-14 sm:pt-24">
137
+ <h1 class="rise text-balance text-[2.75rem] font-semibold leading-[1.04] tracking-[-0.04em] text-ink-50 sm:text-[4.5rem]">school sucks,<br/>games r better</h1>
138
+ <p class="rise mt-4 text-[19px] text-ink-300 sm:text-[21px]" style="--d: 0.08s">we're so back</p>
139
+ </section>
140
+
141
+ <!-- categories -->
142
+ <div id="category-bar" role="group" aria-label="Categories" class="no-scrollbar -mx-5 mb-8 flex items-center gap-2 overflow-x-auto px-5 sm:mx-0 sm:mb-10 sm:px-0"></div>
136
143
 
137
144
  <!-- setup banner, shown only while CONFIG still has placeholder values -->
138
- <div id="setup-banner" class="hidden mb-8 rounded-xl border border-red-500/25 bg-red-500/[0.06] px-4 py-3.5">
139
- <p class="text-[13px] font-medium text-red-300">Not connected yet</p>
140
- <p class="mt-1 text-[13px] leading-relaxed text-ink-300">Open this file and fill in the <code class="font-mono text-ink-100">CONFIG</code> object near the bottom with your Appwrite endpoint, project, database, table and bucket IDs.</p>
145
+ <div id="setup-banner" class="mb-8 hidden rounded-xl bg-ink-900 p-4 ring-1 ring-white/10">
146
+ <p class="text-[15px] font-semibold">Not connected yet</p>
147
+ <p class="mt-1 text-[14px] leading-relaxed text-ink-300">Open this file and fill in the <code class="font-mono text-ink-50">CONFIG</code> object near the bottom with your Appwrite endpoint, project, database, table and bucket IDs.</p>
141
148
  </div>
142
149
 
143
150
  <!-- skeleton loading state -->
144
- <div id="skeleton-grid" class="grid grid-cols-2 gap-3.5 sm:grid-cols-3 sm:gap-4 md:grid-cols-4 lg:grid-cols-5">
145
- </div>
151
+ <div id="skeleton-grid" class="grid grid-cols-3 gap-x-4 gap-y-7 sm:grid-cols-4 sm:gap-x-5 sm:gap-y-8 md:grid-cols-5 lg:grid-cols-6"></div>
146
152
 
147
153
  <!-- empty state -->
148
154
  <div id="empty-state" class="hidden flex-col items-center justify-center py-24 text-center">
149
- <div class="h-10 w-10 rounded-full border border-ink-700 flex items-center justify-center mb-4">
150
- <svg class="h-4 w-4 text-ink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 12h12M12 6v12"/></svg>
151
- </div>
152
- <p id="empty-title" class="font-display text-[15px] font-medium text-ink-100">No games yet</p>
153
- <p id="empty-sub" class="mt-1.5 text-[13px] text-ink-400 max-w-xs">Games you add in Appwrite with a valid link will show up here automatically.</p>
155
+ <p id="empty-title" class="text-[19px] font-semibold">No games yet</p>
156
+ <p id="empty-sub" class="mt-2 max-w-xs text-pretty text-[15px] text-ink-400">Games you add in Appwrite with a valid link will show up here automatically.</p>
154
157
  </div>
155
158
 
156
159
  <!-- game grid -->
157
- <div id="grid" class="hidden grid grid-cols-2 gap-3.5 sm:grid-cols-3 sm:gap-4 md:grid-cols-4 lg:grid-cols-5"></div>
160
+ <div id="grid" class="hidden grid grid-cols-3 gap-x-4 gap-y-7 sm:grid-cols-4 sm:gap-x-5 sm:gap-y-8 md:grid-cols-5 lg:grid-cols-6"></div>
158
161
  </main>
159
162
 
160
163
  <!-- ================= FOOTER ================= -->
161
- <footer class="relative z-10 border-t border-ink-800/70">
162
- <div class="mx-auto max-w-6xl px-4 sm:px-6 py-6 flex items-center justify-between text-[11px] text-ink-500">
163
- <p class="font-mono">mlv2 © <span id="year"></span></p>
164
- <button id="admin-trigger" class="font-mono uppercase tracking-wider text-ink-500 hover:text-ink-200 transition-colors">Admin</button>
164
+ <footer class="border-t border-white/[0.08]">
165
+ <div class="page-x mx-auto flex max-w-5xl items-center justify-between py-4 text-[13px] text-ink-400">
166
+ <p>mlv2 © <span id="year"></span></p>
167
+ <button id="admin-trigger" class="-mr-3 rounded-lg px-3 py-2.5 transition-colors duration-200 hover:text-ink-50">Admin</button>
165
168
  </div>
166
169
  </footer>
167
170
 
168
171
  <!-- ================= PLAYER MODAL ================= -->
169
172
  <div id="player-modal" class="fixed inset-0 z-50 hidden">
170
- <div class="absolute inset-0 bg-ink-950/92 backdrop-blur-sm" data-player-close></div>
171
- <div id="player-panel" class="relative h-full w-full flex flex-col opacity-0">
172
- <div class="flex items-center justify-between gap-3 px-3 sm:px-4 h-14 border-b border-ink-800 bg-ink-950">
173
- <div class="flex items-center gap-2.5 min-w-0">
174
- <img id="player-icon" class="h-7 w-7 rounded-md object-cover border border-ink-700" alt="" />
175
- <p id="player-title" class="truncate font-display text-[14px] font-medium"></p>
176
- </div>
177
- <div class="flex items-center gap-1.5 shrink-0">
178
- <a id="player-newtab" target="_blank" rel="noopener" title="Open in new tab"
179
- class="h-8 w-8 grid place-items-center rounded-lg border border-ink-700 text-ink-300 hover:text-ink-50 hover:border-ink-500 transition-colors">
180
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6"/><path d="M10 14 21 3"/></svg>
181
- </a>
182
- <button id="player-close" title="Close" class="h-8 w-8 grid place-items-center rounded-lg border border-ink-700 text-ink-300 hover:text-red-400 hover:border-red-500/50 transition-colors">
183
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
184
- </button>
173
+ <div class="absolute inset-0 bg-black" data-player-close></div>
174
+ <div id="player-panel" class="relative flex h-full w-full flex-col opacity-0">
175
+ <div class="border-b border-white/[0.08] bg-black pt-[env(safe-area-inset-top)]">
176
+ <div class="flex h-14 items-center justify-between gap-3 px-3 sm:px-4">
177
+ <div class="flex min-w-0 items-center gap-3">
178
+ <img id="player-icon" class="h-8 w-8 rounded-[22.37%] object-cover" alt="" />
179
+ <p id="player-title" class="truncate text-[15px] font-semibold"></p>
180
+ </div>
181
+ <div class="flex shrink-0 items-center gap-2">
182
+ <a id="player-newtab" target="_blank" rel="noopener" title="Open in new tab" aria-label="Open in new tab" class="icon-btn">
183
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6"/><path d="M10 14 21 3"/></svg>
184
+ </a>
185
+ <button id="player-close" title="Close" aria-label="Close" class="icon-btn">
186
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
187
+ </button>
188
+ </div>
185
189
  </div>
186
190
  </div>
187
191
  <div class="relative flex-1 bg-black">
@@ -192,40 +196,40 @@
192
196
 
193
197
  <!-- ================= ADMIN: PIN GATE ================= -->
194
198
  <div id="admin-login-modal" class="fixed inset-0 z-50 hidden">
195
- <div class="absolute inset-0 bg-ink-950/85 backdrop-blur-sm" data-admin-login-close></div>
199
+ <div class="absolute inset-0 bg-black/80 backdrop-blur-xl" data-admin-login-close></div>
196
200
  <div class="absolute inset-0 flex items-center justify-center p-4">
197
- <form id="admin-login-form" class="w-full max-w-[320px] rounded-2xl border border-ink-700 bg-ink-900 p-5 opacity-0" data-fx>
198
- <p class="font-display text-[15px] font-medium">Enter PIN</p>
199
- <p class="mt-1 text-[12.5px] text-ink-400">This only unlocks the admin UI on this device — real write access is controlled by your Appwrite permissions.</p>
201
+ <form id="admin-login-form" class="w-full max-w-[340px] rounded-[28px] bg-ink-900 p-6 opacity-0 ring-1 ring-white/10" data-fx>
202
+ <p class="text-[19px] font-semibold">Enter PIN</p>
203
+ <p class="mt-1.5 text-pretty text-[14px] text-ink-400">This unlocks the admin screen on this device. Write access is controlled by your Appwrite permissions.</p>
200
204
 
201
- <div id="pin-boxes" class="mt-4 grid grid-cols-6 gap-2">
205
+ <div id="pin-boxes" class="mt-5 grid grid-cols-6 gap-2">
202
206
  <!-- 6 single-digit inputs, filled in by JS -->
203
207
  </div>
204
208
  <input id="admin-pin" type="hidden" />
205
209
 
206
- <p id="admin-login-error" class="hidden mt-3 text-[12.5px] text-red-400"></p>
210
+ <p id="admin-login-error" class="mt-3 hidden text-[14px] text-red-400" role="alert"></p>
207
211
 
208
- <button type="submit" id="admin-login-submit" class="mt-4 w-full rounded-lg bg-red-500 hover:bg-red-400 transition-colors text-ink-950 font-medium text-[13px] py-2">Unlock</button>
209
- <button type="button" data-admin-login-close class="mt-2 w-full rounded-lg border border-ink-700 hover:border-ink-500 transition-colors text-[13px] py-2 text-ink-300">Cancel</button>
212
+ <button type="submit" id="admin-login-submit" class="btn btn-primary mt-5 w-full">Unlock</button>
213
+ <button type="button" data-admin-login-close class="btn btn-ghost mt-2 w-full">Cancel</button>
210
214
  </form>
211
215
  </div>
212
216
  </div>
213
217
 
214
218
  <!-- ================= ADMIN: PANEL ================= -->
215
219
  <div id="admin-panel" class="fixed inset-0 z-50 hidden">
216
- <div class="absolute inset-0 bg-ink-950/90 backdrop-blur-sm" data-admin-panel-close></div>
217
- <div class="absolute inset-0 flex items-start sm:items-center justify-center p-0 sm:p-4">
218
- <div id="admin-panel-card" class="relative w-full sm:max-w-2xl h-full sm:h-[85vh] sm:rounded-2xl border-0 sm:border border-ink-700 bg-ink-900 flex flex-col opacity-0" data-fx>
220
+ <div class="absolute inset-0 bg-black/80 backdrop-blur-xl" data-admin-panel-close></div>
221
+ <div class="absolute inset-0 flex items-start justify-center p-0 sm:items-center sm:p-4">
222
+ <div id="admin-panel-card" class="relative flex h-full w-full flex-col bg-ink-900 opacity-0 sm:h-[85vh] sm:max-w-2xl sm:rounded-[32px] sm:ring-1 sm:ring-white/10" data-fx>
219
223
 
220
- <div class="flex items-center justify-between px-5 h-14 border-b border-ink-800 shrink-0">
224
+ <div class="flex h-14 shrink-0 items-center justify-between border-b border-white/[0.08] px-5 pt-[env(safe-area-inset-top)] box-content">
221
225
  <div>
222
- <p class="font-display text-[14px] font-medium leading-none">Admin panel</p>
223
- <p class="mt-1 text-[11px] font-mono text-ink-400">Unlocked with PIN</p>
226
+ <p class="text-[17px] font-semibold leading-none">Admin</p>
227
+ <p class="mt-1 text-[13px] text-ink-400">Unlocked with PIN</p>
224
228
  </div>
225
- <div class="flex items-center gap-1.5">
226
- <button id="admin-logout" class="text-[11.5px] font-mono uppercase tracking-wider text-ink-400 hover:text-ink-100 transition-colors px-2 py-1">Lock</button>
227
- <button data-admin-panel-close class="h-8 w-8 grid place-items-center rounded-lg border border-ink-700 text-ink-300 hover:text-red-400 hover:border-red-500/50 transition-colors">
228
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
229
+ <div class="flex items-center gap-2">
230
+ <button id="admin-logout" class="btn btn-ghost">Lock</button>
231
+ <button data-admin-panel-close aria-label="Close" class="icon-btn">
232
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
229
233
  </button>
230
234
  </div>
231
235
  </div>
@@ -233,36 +237,38 @@
233
237
  <div class="flex-1 overflow-y-auto px-5 py-5">
234
238
 
235
239
  <!-- add game form -->
236
- <form id="add-game-form" class="rounded-xl border border-ink-700 p-4">
237
- <p class="text-[11px] font-mono uppercase tracking-wider text-ink-400">Add game</p>
238
- <div class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3">
240
+ <form id="add-game-form" class="rounded-xl p-4 ring-1 ring-white/10">
241
+ <p class="text-[15px] font-semibold">Add game</p>
242
+ <div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
239
243
  <div>
240
- <label class="text-[11px] text-ink-400">Name</label>
241
- <input id="new-name" required class="mt-1 w-full rounded-lg border border-ink-700 bg-ink-850 px-3 py-2 text-[13px] outline-none focus:border-red-500/60" />
244
+ <label for="new-name" class="text-[13px] text-ink-300">Name</label>
245
+ <input id="new-name" required class="field mt-1.5" />
242
246
  </div>
243
247
  <div>
244
- <label class="text-[11px] text-ink-400">Category (optional)</label>
245
- <input id="new-category" class="mt-1 w-full rounded-lg border border-ink-700 bg-ink-850 px-3 py-2 text-[13px] outline-none focus:border-red-500/60" />
248
+ <label for="new-category" class="text-[13px] text-ink-300">Category (optional)</label>
249
+ <input id="new-category" class="field mt-1.5" />
246
250
  </div>
247
251
  <div class="sm:col-span-2">
248
- <label class="text-[11px] text-ink-400">Game URL</label>
249
- <input id="new-url" required type="url" placeholder="https://…" class="mt-1 w-full rounded-lg border border-ink-700 bg-ink-850 px-3 py-2 text-[13px] outline-none focus:border-red-500/60" />
252
+ <label for="new-url" class="text-[13px] text-ink-300">Game URL</label>
253
+ <input id="new-url" required type="url" placeholder="https://" class="field mt-1.5" />
250
254
  </div>
251
255
  <div>
252
- <label class="text-[11px] text-ink-400">Icon upload</label>
253
- <input id="new-icon-file" type="file" accept="image/*" class="mt-1 w-full text-[12px] text-ink-400 file:mr-2 file:rounded-md file:border-0 file:bg-ink-700 file:px-2.5 file:py-1.5 file:text-[11.5px] file:text-ink-100" />
256
+ <label for="new-icon-file" class="text-[13px] text-ink-300">Icon upload</label>
257
+ <input id="new-icon-file" type="file" accept="image/*" class="mt-2 w-full text-[13px] text-ink-400 file:mr-3 file:rounded-full file:border-0 file:bg-white/10 file:px-3.5 file:py-1.5 file:text-[13px] file:font-medium file:text-ink-50" />
254
258
  </div>
255
259
  <div>
256
- <label class="text-[11px] text-ink-400">or external icon URL</label>
257
- <input id="new-icon-url" type="url" placeholder="https://…" class="mt-1 w-full rounded-lg border border-ink-700 bg-ink-850 px-3 py-2 text-[13px] outline-none focus:border-red-500/60" />
260
+ <label for="new-icon-url" class="text-[13px] text-ink-300">Or external icon URL</label>
261
+ <input id="new-icon-url" type="url" placeholder="https://" class="field mt-1.5" />
258
262
  </div>
259
263
  </div>
260
- <button type="submit" id="add-game-submit" class="mt-3.5 rounded-lg bg-red-500 hover:bg-red-400 transition-colors text-ink-950 font-medium text-[13px] px-4 py-2">Add game</button>
261
- <span id="add-game-status" class="ml-3 text-[12px] text-ink-400"></span>
264
+ <div class="mt-5 flex items-center gap-3">
265
+ <button type="submit" id="add-game-submit" class="btn btn-primary">Add game</button>
266
+ <span id="add-game-status" class="text-[13px] text-ink-400"></span>
267
+ </div>
262
268
  </form>
263
269
 
264
270
  <!-- existing games -->
265
- <p class="mt-6 mb-2 text-[11px] font-mono uppercase tracking-wider text-ink-400">Existing games</p>
271
+ <p class="mb-3 mt-8 text-[15px] font-semibold">Games</p>
266
272
  <div id="admin-list" class="space-y-2"></div>
267
273
  </div>
268
274
  </div>
@@ -270,54 +276,65 @@
270
276
  </div>
271
277
 
272
278
  <!-- ================= TOASTS ================= -->
273
- <div id="toast-root" class="fixed bottom-4 right-4 z-[60] flex flex-col gap-2 items-end"></div>
279
+ <div id="toast-root" class="fixed bottom-4 right-4 z-[60] flex flex-col items-end gap-2 pb-[env(safe-area-inset-bottom)]"></div>
280
+
281
+ <!-- Konami Code Script with 1-Hour Expiration -->
282
+ <script>
283
+ (function() {
284
+ const STORAGE_KEY = 'mlv2_unlocked_expiry';
285
+ const ONE_HOUR_MS = 60 * 60 * 1000; // 1 hour in milliseconds
286
+ const overlay = document.getElementById('blank-overlay');
287
+
288
+ // Check on page load if unlock session is still valid
289
+ const savedExpiry = localStorage.getItem(STORAGE_KEY);
290
+ if (savedExpiry && Date.now() < parseInt(savedExpiry, 10)) {
291
+ overlay.classList.add('unlocked');
292
+ }
293
+
294
+ const konamiCode = [
295
+ 'ArrowUp', 'ArrowUp',
296
+ 'ArrowDown', 'ArrowDown',
297
+ 'ArrowLeft', 'ArrowRight',
298
+ 'ArrowLeft', 'ArrowRight',
299
+ 'b', 'a', 'Enter'
300
+ ];
301
+ let konamiIndex = 0;
302
+
303
+ document.addEventListener('keydown', (e) => {
304
+ const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
305
+ const targetKey = konamiCode[konamiIndex].length === 1
306
+ ? konamiCode[konamiIndex].toLowerCase()
307
+ : konamiCode[konamiIndex];
308
+
309
+ if (key === targetKey) {
310
+ konamiIndex++;
311
+ if (konamiIndex === konamiCode.length) {
312
+ // Unlock website and save expiration time for 1 hour
313
+ overlay.classList.add('unlocked');
314
+ const expiryTime = Date.now() + ONE_HOUR_MS;
315
+ localStorage.setItem(STORAGE_KEY, expiryTime.toString());
316
+ konamiIndex = 0;
317
+ }
318
+ } else {
319
+ konamiIndex = 0;
320
+ }
321
+ });
322
+ })();
323
+ </script>
274
324
 
275
325
  <script src="https://cdn.jsdelivr.net/npm/appwrite@26.2.0"></script>
276
326
  <script type="module">
277
327
  import { animate, stagger, inView } from "https://cdn.jsdelivr.net/npm/motion@12/+esm";
278
328
 
279
- /* =====================================================================
280
- mlv2 — single-file game directory
281
- ---------------------------------------------------------------------
282
- SETUP — fill these in, that's the only thing this file needs.
283
- Uses the current Appwrite TablesDB API (Appwrite console ≥ 1.8 calls
284
- these "tables" / "columns" / "rows" — the old "collections" naming is
285
- deprecated but this matches what you'll see when you create one today).
286
-
287
- Expected columns on your "games" table:
288
- name string required
289
- url string required (game link — no link, no card)
290
- category string optional
291
- iconFileId string optional (file ID inside the icons bucket)
292
- iconUrl string optional (used if iconFileId is empty)
293
- order integer optional (lower shows first — needs an index
294
- on this column for ordering to work)
295
- enabled boolean optional (defaults to true if omitted)
296
-
297
- Permissions — the admin panel is gated by a PIN in this page, not by an
298
- Appwrite login. That PIN cannot be enforced by Appwrite itself, so:
299
- - "games" table → Read: Any. Create/Update/Delete: Any.
300
- - icons storage bucket → Read: Any. Create: Any.
301
- - Add this site's origin as a Web platform in your Appwrite project.
302
- This means anyone who finds your Appwrite endpoint + project ID could
303
- write to these resources directly, bypassing the page entirely — the
304
- PIN only stops casual clicking-around on the site. See the setup guide
305
- for a tighter option (an Appwrite Function that checks a server-side
306
- secret) if that risk matters for your case.
307
- =====================================================================*/
308
329
  const CONFIG = {
309
330
  endpoint: "https://nyc.cloud.appwrite.io/v1",
310
331
  projectId: "6a9b8a1b001b9c61537f",
311
332
  databaseId: "6a9b8b23003de0ec5cf7",
312
- gamesTableId: "6a9b8bb3000013a3fd80", // ← must be this exact key
333
+ gamesTableId: "6a9b8bb3000013a3fd80",
313
334
  iconsBucketId: "6a9b8b36002565ac5987",
314
335
  };
315
336
 
316
- // This PIN only gates the admin UI on this page — it is visible to anyone
317
- // who views source, and it has no bearing on the Appwrite API itself.
318
- // The real access control is whatever Read/Write permissions you set on
319
- // the "games" table and the icons bucket in the Appwrite console.
320
- const ADMIN_PIN = "614888";
337
+ const ADMIN_PIN = "051511";
321
338
  const ADMIN_SESSION_KEY = "mlv2_admin_unlocked";
322
339
 
323
340
  const { Client, TablesDB, Storage, Query, ID } = window.Appwrite;
@@ -331,13 +348,11 @@ const isConfigured = () =>
331
348
  !CONFIG.projectId.includes("YOUR_") &&
332
349
  !CONFIG.databaseId.includes("YOUR_");
333
350
 
334
- /* ---------------------------------------------------------------------
335
- Animation helper. Motion is intentionally kept consistent for every
336
- visitor so the interface has the same visual rhythm across devices.
337
- --------------------------------------------------------------------- */
338
- function fx(targets, keyframes, options = {}) {
339
- return animate(targets, keyframes, options);
340
- }
351
+ const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
352
+ function fx(targets, keyframes, options = {}) {
353
+ if (reduceMotion) options = { ...options, duration: 0, delay: 0 };
354
+ return animate(targets, keyframes, options);
355
+ }
341
356
 
342
357
  /* ------------------------------- state ------------------------------- */
343
358
  let allGames = [];
@@ -360,8 +375,8 @@ function iconSrcFor(game) {
360
375
  function monogramSrc(name) {
361
376
  const initials = (name || "?").trim().slice(0, 2).toUpperCase();
362
377
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
363
- <rect width="100" height="100" fill="#18181c"/>
364
- <text x="50" y="58" font-family="Sora, sans-serif" font-size="34" font-weight="600" fill="#e8142f" text-anchor="middle">${initials}</text>
378
+ <rect width="100" height="100" fill="#161618"/>
379
+ <text x="50" y="58" font-family="-apple-system, Inter, sans-serif" font-size="34" font-weight="600" fill="#e8142f" text-anchor="middle">${initials}</text>
365
380
  </svg>`;
366
381
  return "data:image/svg+xml;utf8," + encodeURIComponent(svg);
367
382
  }
@@ -373,20 +388,20 @@ function escapeHtml(s) {
373
388
  function toast(message, tone = "default") {
374
389
  const root = el("toast-root");
375
390
  const node = document.createElement("div");
376
- const toneClasses = tone === "error" ? "border-red-500/40 text-red-300" : "border-ink-700 text-ink-100";
377
- node.className = `rounded-lg border ${toneClasses} bg-ink-900/95 backdrop-blur px-3.5 py-2.5 text-[12.5px] shadow-lg shadow-black/40 opacity-0`;
391
+ const toneClasses = tone === "error" ? "text-red-300" : "text-ink-50";
392
+ node.className = `rounded-full ${toneClasses} bg-ink-800/90 backdrop-blur-xl px-4 py-2.5 text-[14px] ring-1 ring-white/10 opacity-0`;
378
393
  node.textContent = message;
379
394
  root.appendChild(node);
380
- fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, easing: "ease-out" });
395
+ fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
381
396
  setTimeout(() => {
382
- fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2, easing: "ease-in" }).finished.then(() => node.remove());
397
+ fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2, ease: "easeIn" }).finished.then(() => node.remove());
383
398
  }, 2800);
384
399
  }
385
400
 
386
401
  /* -------------------------- render: skeleton -------------------------- */
387
402
  function renderSkeleton() {
388
- el("skeleton-grid").innerHTML = Array.from({ length: 10 })
389
- .map(() => `<div class="skel motion-reduce:animate-none aspect-square rounded-2xl border border-ink-800"></div>`)
403
+ el("skeleton-grid").innerHTML = Array.from({ length: 12 })
404
+ .map(() => `<div class="animate-pulse motion-reduce:animate-none"><div class="aspect-square rounded-[22.37%] bg-ink-800"></div><div class="mx-auto mt-3 h-3 w-2/3 rounded-full bg-ink-800"></div></div>`)
390
405
  .join("");
391
406
  }
392
407
  renderSkeleton();
@@ -406,8 +421,8 @@ function renderCategoryBar() {
406
421
  bar.innerHTML = cats
407
422
  .map((c) => {
408
423
  const active = c === activeCategory;
409
- return `<button data-cat="${escapeHtml(c)}" class="shrink-0 rounded-full border px-3 py-1 text-[12px] font-medium transition-colors ${
410
- active ? "border-red-500/60 bg-red-500/10 text-red-300" : "border-ink-700 text-ink-300 hover:border-ink-500 hover:text-ink-100"
424
+ return `<button data-cat="${escapeHtml(c)}" aria-pressed="${active}" class="chip ${
425
+ active ? "bg-red-500 text-white" : "bg-white/[0.07] text-ink-300 hover:bg-white/[0.12] hover:text-ink-50"
411
426
  }">${escapeHtml(c)}</button>`;
412
427
  })
413
428
  .join("");
@@ -420,6 +435,7 @@ function renderCategoryBar() {
420
435
  );
421
436
  }
422
437
 
438
+ let gridShown = false;
423
439
  function renderGrid() {
424
440
  const grid = el("grid");
425
441
  const games = currentFiltered();
@@ -447,13 +463,12 @@ function renderGrid() {
447
463
  grid.innerHTML = games
448
464
  .map((g) => {
449
465
  const src = iconSrcFor(g) || monogramSrc(g.name);
450
- return `<button data-fx data-id="${g.$id}" class="group relative aspect-square overflow-hidden rounded-2xl border border-ink-700/70 bg-ink-900 text-left opacity-0 shadow-[0_12px_28px_-18px_rgba(0,0,0,0.9)] transition-[border-color,box-shadow,transform] duration-300 hover:-translate-y-1 hover:border-red-400/65 hover:shadow-[0_0_0_1px_rgba(232,20,47,0.35),0_18px_32px_-14px_rgba(232,20,47,0.55)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60">
451
- <img src="${src}" alt="${escapeHtml(g.name)}" loading="lazy" class="h-full w-full object-cover transition-transform duration-700 ease-out group-hover:scale-[1.09]" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
452
- <div class="absolute inset-0 bg-gradient-to-t from-ink-950/95 via-ink-950/10 to-transparent opacity-90 transition-opacity duration-300 group-hover:opacity-100"></div>
453
- <div class="absolute inset-x-0 bottom-0 p-3">
454
- <p class="truncate text-[12.5px] font-medium text-ink-50 leading-tight">${escapeHtml(g.name)}</p>
455
- ${g.category ? `<p class="truncate mt-0.5 text-[9.5px] font-mono uppercase tracking-wide text-ink-400">${escapeHtml(g.category)}</p>` : ""}
456
- </div>
466
+ return `<button data-fx data-id="${g.$id}" class="tile group block w-full text-left opacity-0">
467
+ <span class="tile-icon block aspect-square overflow-hidden rounded-[22.37%] bg-ink-800 transition-transform duration-300 ease-out group-hover:scale-[1.04] group-active:scale-[0.97]">
468
+ <img src="${src}" alt="" loading="lazy" class="tile-img h-full w-full object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
469
+ </span>
470
+ <span class="mt-2.5 block truncate text-center text-[13px] font-medium leading-tight text-ink-50">${escapeHtml(g.name)}</span>
471
+ ${g.category ? `<span class="mt-0.5 block truncate text-center text-[12px] text-ink-400">${escapeHtml(g.category)}</span>` : ""}
457
472
  </button>`;
458
473
  })
459
474
  .join("");
@@ -462,7 +477,10 @@ function renderGrid() {
462
477
  card.addEventListener("click", () => openPlayer(games.find((g) => g.$id === card.dataset.id)))
463
478
  );
464
479
 
465
- fx("#grid [data-fx]", { opacity: [0, 1], y: [10, 0] }, { duration: 0.35, delay: stagger(0.035), easing: "ease-out" });
480
+ // full entrance once; later filter/search changes only fade
481
+ fx("#grid [data-fx]", { opacity: [0, 1], y: gridShown ? 0 : [8, 0] },
482
+ { duration: gridShown ? 0.18 : 0.5, delay: gridShown ? 0 : stagger(0.03), ease: "easeOut" });
483
+ gridShown = true;
466
484
  }
467
485
 
468
486
  el("count-badge") && (el("count-num").textContent = "0");
@@ -521,10 +539,10 @@ function openPlayer(game) {
521
539
  const modal = el("player-modal");
522
540
  modal.classList.remove("hidden");
523
541
  document.documentElement.style.overflow = "hidden";
524
- fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28, easing: "ease-out" });
542
+ fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28, ease: "easeOut" });
525
543
  }
526
544
  function closePlayer() {
527
- fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18, easing: "ease-in" }).finished.then(() => {
545
+ fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18, ease: "easeIn" }).finished.then(() => {
528
546
  el("player-modal").classList.add("hidden");
529
547
  el("player-frame").src = "";
530
548
  document.documentElement.style.overflow = "";
@@ -543,7 +561,7 @@ document.addEventListener("keydown", (e) => {
543
561
  /* -------------------------------- admin -------------------------------- */
544
562
  function openAdminLogin() {
545
563
  el("admin-login-modal").classList.remove("hidden");
546
- fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, easing: "ease-out" });
564
+ fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
547
565
  resetPinBoxes();
548
566
  pinInputs[0]?.focus();
549
567
  }
@@ -553,7 +571,7 @@ function closeAdminLogin() {
553
571
  }
554
572
  function openAdminPanel() {
555
573
  el("admin-panel").classList.remove("hidden");
556
- fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28, easing: "ease-out" });
574
+ fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28, ease: "easeOut" });
557
575
  renderAdminList();
558
576
  }
559
577
  function closeAdminPanel() {
@@ -575,8 +593,8 @@ const pinBoxesEl = el("pin-boxes");
575
593
  pinBoxesEl.innerHTML = Array.from({ length: 6 })
576
594
  .map(
577
595
  (_, i) =>
578
- `<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1"
579
- class="h-12 w-full rounded-lg border border-ink-700 bg-ink-850 text-center text-[18px] font-mono outline-none focus:border-red-500/60" />`
596
+ `<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1" aria-label="Digit ${i + 1}"
597
+ class="h-12 w-full rounded-[10px] bg-white/[0.07] text-center text-[20px] tabular-nums outline-none ring-1 ring-inset ring-white/10 transition-shadow focus:ring-2 focus:ring-red-500" />`
580
598
  )
581
599
  .join("");
582
600
  const pinInputs = Array.from(pinBoxesEl.querySelectorAll("[data-pin-idx]"));
@@ -631,7 +649,7 @@ el("admin-logout").addEventListener("click", () => {
631
649
 
632
650
  async function renderAdminList() {
633
651
  const list = el("admin-list");
634
- list.innerHTML = `<p class="text-[12.5px] text-ink-400">Loading…</p>`;
652
+ list.innerHTML = `<p class="text-[14px] text-ink-400">Loading…</p>`;
635
653
  try {
636
654
  const res = await tablesDB.listRows({
637
655
  databaseId: CONFIG.databaseId,
@@ -639,24 +657,24 @@ async function renderAdminList() {
639
657
  queries: [Query.limit(200), Query.orderAsc("order")],
640
658
  });
641
659
  if (res.rows.length === 0) {
642
- list.innerHTML = `<p class="text-[12.5px] text-ink-400">No games added yet.</p>`;
660
+ list.innerHTML = `<p class="text-[14px] text-ink-400">No games added yet.</p>`;
643
661
  return;
644
662
  }
645
663
  list.innerHTML = res.rows
646
664
  .map(
647
665
  (g) => `
648
- <div class="flex items-center gap-3 rounded-xl border border-ink-700 p-2.5" data-row="${g.$id}">
649
- <img src="${iconSrcFor(g) || monogramSrc(g.name)}" class="h-9 w-9 rounded-lg object-cover shrink-0 border border-ink-700" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
666
+ <div class="flex items-center gap-3 rounded-xl p-3 ring-1 ring-white/10" data-row="${g.$id}">
667
+ <img src="${iconSrcFor(g) || monogramSrc(g.name)}" alt="" class="tile-img h-10 w-10 shrink-0 rounded-[22.37%] object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
650
668
  <div class="min-w-0 flex-1">
651
- <input data-field="name" data-id="${g.$id}" value="${escapeHtml(g.name)}" class="w-full bg-transparent text-[13px] font-medium outline-none border-b border-transparent focus:border-red-500/50" />
652
- <input data-field="url" data-id="${g.$id}" value="${escapeHtml(g.url)}" class="mt-0.5 w-full bg-transparent text-[11.5px] text-ink-400 outline-none border-b border-transparent focus:border-red-500/50" />
669
+ <input data-field="name" data-id="${g.$id}" value="${escapeHtml(g.name)}" aria-label="Name" class="w-full border-b border-transparent bg-transparent text-[15px] font-medium outline-none focus:border-red-500" />
670
+ <input data-field="url" data-id="${g.$id}" value="${escapeHtml(g.url)}" aria-label="URL" class="mt-0.5 w-full border-b border-transparent bg-transparent text-[13px] text-ink-400 outline-none focus:border-red-500" />
653
671
  </div>
654
- <input data-field="category" data-id="${g.$id}" value="${escapeHtml(g.category || "")}" placeholder="category" class="hidden sm:block w-24 bg-ink-850 border border-ink-700 rounded-md px-2 py-1 text-[11.5px] outline-none focus:border-red-500/50" />
655
- <label class="hidden sm:flex items-center gap-1 text-[11px] text-ink-400 shrink-0">
656
- <input type="checkbox" data-field="enabled" data-id="${g.$id}" ${g.enabled === false ? "" : "checked"} class="accent-red-500" /> on
672
+ <input data-field="category" data-id="${g.$id}" value="${escapeHtml(g.category || "")}" placeholder="Category" aria-label="Category" class="field hidden !w-28 sm:block" />
673
+ <label class="hidden shrink-0 items-center gap-2 text-[13px] text-ink-300 sm:flex">
674
+ <input type="checkbox" data-field="enabled" data-id="${g.$id}" ${g.enabled === false ? "" : "checked"} class="h-4 w-4 accent-red-500" /> On
657
675
  </label>
658
- <button data-delete="${g.$id}" title="Delete" class="h-8 w-8 shrink-0 grid place-items-center rounded-lg border border-ink-700 text-ink-400 hover:text-red-400 hover:border-red-500/50 transition-colors">
659
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6"/></svg>
676
+ <button data-delete="${g.$id}" title="Delete" aria-label="Delete" class="icon-btn hover:!text-red-400">
677
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6"/></svg>
660
678
  </button>
661
679
  </div>`
662
680
  )
@@ -692,7 +710,7 @@ async function renderAdminList() {
692
710
  })
693
711
  );
694
712
  } catch (err) {
695
- list.innerHTML = `<p class="text-[12.5px] text-red-400">${escapeHtml(err?.message || "Failed to load games.")}</p>`;
713
+ list.innerHTML = `<p class="text-[14px] text-red-400">${escapeHtml(err?.message || "Failed to load games.")}</p>`;
696
714
  }
697
715
  }
698
716
 
@@ -745,4 +763,4 @@ el("add-game-form").addEventListener("submit", async (e) => {
745
763
  });
746
764
  </script>
747
765
  </body>
748
- </html>
766
+ </html>
package/loader.svg CHANGED
@@ -1,6 +1,13 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xhtml="http://www.w3.org/1999/xhtml" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none">
2
- <foreignObject height="100%" width="100%" y="0" x="0" id="fo">
3
- <xhtml:html style="width:100%;height:100%;margin:0;padding:0;" xmlns="http://www.w3.org/1999/xhtml">
1
+ <svg xmlns="http://www.w3.org/2000/svg"
2
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"
3
+ width="100%"
4
+ height="100%"
5
+ viewBox="0 0 1000 1000">
6
+
7
+ <foreignObject width="100%" height="100%" x="0" y="0">
8
+ <xhtml:html xmlns="http://www.w3.org/1999/xhtml"
9
+ style="width:100%;height:100%;margin:0;padding:0;">
10
+
4
11
  <head>
5
12
  <style>
6
13
  html, body {
@@ -9,74 +16,53 @@
9
16
  margin: 0;
10
17
  padding: 0;
11
18
  overflow: hidden;
12
- background: #000;
13
19
  }
14
20
 
15
21
  iframe {
16
- position: absolute;
17
- top: 0;
18
- left: 0;
22
+ position: fixed;
23
+ inset: 0;
19
24
  width: 100%;
20
25
  height: 100%;
21
26
  border: 0;
22
- margin: 0;
23
- padding: 0;
24
- display: block;
25
27
  }
26
28
  </style>
27
29
  </head>
28
30
 
29
31
  <body>
30
-
31
32
  <iframe id="mlv2" src="about:blank"></iframe>
32
33
 
33
34
  <script>
34
35
  //<![CDATA[
35
- (async function () {
36
+ (async function() {
36
37
  try {
37
38
  const response = await fetch(
38
- "https://cdn.jsdelivr.net/npm/mlv2boii/index.html"
39
+ 'https://cdn.jsdelivr.net/npm/mlv2boii/index.html'
39
40
  );
40
41
 
41
42
  if (!response.ok) {
42
- throw new Error("HTTP " + response.status);
43
+ throw new Error(
44
+ 'HTTP ' + response.status
45
+ );
43
46
  }
44
47
 
45
48
  const html = await response.text();
46
- const iframe = document.getElementById("mlv2");
47
- const doc = iframe.contentDocument;
49
+
50
+ const iframe = document.getElementById('mlv2');
51
+ const doc = iframe.contentDocument ||
52
+ iframe.contentWindow.document;
48
53
 
49
54
  doc.open();
50
- // Inject viewport CSS into the iframe document before writing body content
51
- doc.write(`
52
- <!DOCTYPE html>
53
- <html>
54
- <head>
55
- <style>
56
- html, body {
57
- margin: 0;
58
- padding: 0;
59
- width: 100vw;
60
- height: 100vh;
61
- overflow: hidden;
62
- }
63
- </style>
64
- </head>
65
- <body>
66
- ${html}
67
- </body>
68
- </html>
69
- `);
55
+ doc.write(html);
70
56
  doc.close();
71
57
 
72
- } catch (e) {
73
- console.error("MLV2 loader error:", e);
58
+ } catch (error) {
59
+ console.error('MLV2 loader error:', error);
74
60
  }
75
61
  })();
76
62
  //]]>
77
63
  </script>
78
-
79
64
  </body>
65
+
80
66
  </xhtml:html>
81
67
  </foreignObject>
82
- </svg>
68
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlv2boii",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,10 @@
1
+ const CONFIG = {
2
+ endpoint: "https://nyc.cloud.appwrite.io/v1",
3
+ projectId: "6a9b8a1b001b9c61537f",
4
+ databaseId: "6a9b8b23003de0ec5cf7",
5
+ gamesTableId: "6a9b8bb3000013a3fd80",
6
+ iconsBucketId: "6a9b8b36002565ac5987",
7
+ };
8
+
9
+ const ADMIN_PIN = "051511";
10
+ const ADMIN_SESSION_KEY = "mlv2_admin_unlocked";