mlv2boii 1.0.6 → 1.0.8

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,166 +49,159 @@
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); }
54
68
 
55
- /* thin, quiet scrollbars */
56
- * { scrollbar-width: thin; scrollbar-color: #2a2a30 transparent; }
57
- *::-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; }
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; }
61
74
 
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"); }
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)); }
65
77
 
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; }
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; }
68
80
 
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
81
 
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); }
82
+ /* Liquid glass: translucent fill, blur + saturation, bright top edge, light rim.
83
+ Used only on floating layers (header, sheets, toasts, controls). */
84
+ .glass {
85
+ background: rgba(255,255,255,0.06);
86
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
87
+ backdrop-filter: blur(24px) saturate(180%);
88
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.22), inset 0 -1px 0 rgba(255,255,255,0.04), 0 10px 32px rgba(0,0,0,0.45);
80
89
  }
81
- @keyframes heroLine {
82
- 0%, 100% { opacity: .35; transform: scaleX(.7); }
83
- 50% { opacity: 1; transform: scaleX(1); }
90
+ .glass-sheet {
91
+ background: rgba(28,28,31,0.55);
92
+ -webkit-backdrop-filter: blur(40px) saturate(180%);
93
+ backdrop-filter: blur(40px) saturate(180%);
84
94
  }
85
-
86
- /* Blank Overlay Lock Screen */
87
- #blank-overlay {
88
- position: fixed;
89
- inset: 0;
90
- background-color: #000000;
91
- z-index: 99999;
92
- display: flex;
93
- transition: opacity 0.4s ease, visibility 0.4s ease;
95
+ .glass::before {
96
+ content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 1px; pointer-events: none;
97
+ background: linear-gradient(135deg, rgba(255,255,255,0.4), rgba(255,255,255,0.05) 40%, rgba(255,255,255,0.05) 60%, rgba(255,255,255,0.22));
98
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
99
+ -webkit-mask-composite: xor; mask-composite: exclude;
94
100
  }
95
- #blank-overlay.unlocked {
96
- opacity: 0;
97
- visibility: hidden;
98
- pointer-events: none;
101
+ /* small controls: same edge light, no blur (nothing behind them to blur) */
102
+ .glass-lite { background: rgba(255,255,255,0.07); box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), inset 0 0 0 1px rgba(255,255,255,0.08); }
103
+ .glass-lite:hover { background: rgba(255,255,255,0.12); }
104
+ .chip-on { background: rgba(232,20,47,0.92); box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 0 0 1px rgba(255,255,255,0.12); }
105
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
106
+ .glass { background: rgba(22,22,24,0.94); } .glass-sheet { background: rgba(18,18,20,0.97); }
107
+ }
108
+ @media (prefers-reduced-transparency: reduce) {
109
+ .glass, .glass-sheet { -webkit-backdrop-filter: none; backdrop-filter: none; background: #161618; }
99
110
  }
100
- </style>
101
- </head>
102
111
 
103
- <body class="bg-ink-950 text-ink-100 font-sans antialiased min-h-screen">
112
+ .no-scrollbar { scrollbar-width: none; }
113
+ .no-scrollbar::-webkit-scrollbar { display: none; }
114
+ * { scrollbar-width: thin; scrollbar-color: #2c2c30 transparent; }
115
+ *::-webkit-scrollbar { width: 8px; height: 8px; }
116
+ *::-webkit-scrollbar-thumb { background: #2c2c30; border-radius: 999px; }
104
117
 
105
- <!-- Blank Overlay Screen -->
106
- <div id="blank-overlay"></div>
118
+ /* the single page-load moment: title and subline rise in */
119
+ @keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
120
+ .rise { animation: rise 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation-delay: var(--d, 0s); }
121
+ @media (prefers-reduced-motion: reduce) { .rise { animation: none; } }
107
122
 
108
- <div class="grain"></div>
109
- <div class="spotlight"></div>
123
+ </style>
124
+ </head>
125
+
126
+ <body class="min-h-screen bg-ink-950 font-sans text-ink-50 antialiased">
110
127
 
111
128
  <!-- ================= HEADER ================= -->
112
- <header class="sticky top-0 z-40 border-b border-ink-800/80 bg-ink-950/75 backdrop-blur-md">
113
- <div class="mx-auto max-w-6xl px-4 sm:px-6 h-14 flex items-center gap-3">
114
- <a href="#" class="flex items-center gap-2 shrink-0 group">
115
- <span class="relative h-2 w-2">
116
- <span class="absolute inset-0 rounded-full bg-red-500"></span>
117
- <span class="absolute inset-0 animate-ping rounded-full bg-red-500 opacity-60"></span>
118
- </span>
119
- <span class="font-display font-semibold tracking-tight text-[15px] leading-none">mlv2</span>
129
+ <header class="pointer-events-none sticky top-0 z-40 pt-[calc(env(safe-area-inset-top)+0.75rem)]">
130
+ <div class="page-x mx-auto max-w-5xl">
131
+ <div class="glass pointer-events-auto relative flex h-14 items-center gap-4 rounded-full pl-5 pr-2">
132
+ <a href="#" class="flex shrink-0 items-center gap-2" aria-label="mlv2 home">
133
+ <span class="h-2.5 w-2.5 rounded-full bg-red-500"></span>
134
+ <span class="text-[17px] font-semibold tracking-[-0.02em]">mlv2</span>
120
135
  </a>
121
136
 
122
- <div class="flex-1 max-w-xs sm:max-w-sm relative">
123
- <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>
124
- <input id="search" type="text" placeholder="Search games…" autocomplete="off"
125
- 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" />
137
+ <div class="ml-auto flex min-w-0 flex-1 items-center justify-end gap-4">
138
+ <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>
139
+ <div class="relative w-full max-w-[16rem]">
140
+ <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>
141
+ <input id="search" type="text" placeholder="Search games" aria-label="Search games" autocomplete="off" enterkeyhint="search" class="field rounded-full pl-9" />
142
+ </div>
126
143
  </div>
127
-
128
- <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">
129
- <span id="count-num">0</span>&nbsp;games
130
- </span>
131
144
  </div>
132
-
133
- <div class="border-t border-ink-800/60">
134
- <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>
135
145
  </div>
136
146
  </header>
137
147
 
138
148
  <!-- ================= MAIN ================= -->
139
- <main class="relative z-10 mx-auto max-w-5xl px-4 sm:px-6 py-8 sm:py-12">
140
-
141
- <!-- ================= HERO ================= -->
142
- <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">
143
- <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>
144
- <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>
145
- <div class="relative">
146
- <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">
147
- <span class="h-1.5 w-1.5 rounded-full bg-red-400 shadow-[0_0_12px_rgba(255,77,97,0.9)]"></span>
148
- Play something
149
- </div>
150
- <h1 class="font-display text-4xl font-semibold tracking-[-0.05em] text-ink-50 sm:text-6xl">school sucks, <br/>games r <span class="text-red-400">better</span></h1>
151
- <p class="mx-auto mt-4 max-w-md text-sm leading-6 text-ink-300 sm:text-[15px]">we're so back 🥹</p>
152
- <div class="hero-line mx-auto mt-7 h-px w-24 bg-gradient-to-r from-transparent via-red-400 to-transparent"></div>
153
- </div>
149
+ <main class="page-x mx-auto max-w-5xl pb-24">
150
+
151
+ <!-- ================= TITLE ================= -->
152
+ <section class="pb-10 pt-14 sm:pb-14 sm:pt-24">
153
+ <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>
154
+ <p class="rise mt-4 text-[19px] text-ink-300 sm:text-[21px]" style="--d: 0.08s">we're so back</p>
154
155
  </section>
155
156
 
157
+ <!-- categories -->
158
+ <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>
159
+
156
160
  <!-- setup banner, shown only while CONFIG still has placeholder values -->
157
- <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">
158
- <p class="text-[13px] font-medium text-red-300">Not connected yet</p>
159
- <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>
161
+ <div id="setup-banner" class="mb-8 hidden rounded-xl bg-ink-900 p-4 ring-1 ring-white/10">
162
+ <p class="text-[15px] font-semibold">Not connected yet</p>
163
+ <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>
160
164
  </div>
161
165
 
162
166
  <!-- skeleton loading state -->
163
- <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">
164
- </div>
167
+ <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>
165
168
 
166
169
  <!-- empty state -->
167
170
  <div id="empty-state" class="hidden flex-col items-center justify-center py-24 text-center">
168
- <div class="h-10 w-10 rounded-full border border-ink-700 flex items-center justify-center mb-4">
169
- <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>
170
- </div>
171
- <p id="empty-title" class="font-display text-[15px] font-medium text-ink-100">No games yet</p>
172
- <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>
171
+ <p id="empty-title" class="text-[19px] font-semibold">No games yet</p>
172
+ <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>
173
173
  </div>
174
174
 
175
175
  <!-- game grid -->
176
- <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>
176
+ <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>
177
177
  </main>
178
178
 
179
179
  <!-- ================= FOOTER ================= -->
180
- <footer class="relative z-10 border-t border-ink-800/70">
181
- <div class="mx-auto max-w-6xl px-4 sm:px-6 py-6 flex items-center justify-between text-[11px] text-ink-500">
182
- <p class="font-mono">mlv2 © <span id="year"></span></p>
183
- <button id="admin-trigger" class="font-mono uppercase tracking-wider text-ink-500 hover:text-ink-200 transition-colors">Admin</button>
180
+ <footer class="border-t border-white/[0.08]">
181
+ <div class="page-x mx-auto flex max-w-5xl items-center justify-between py-4 text-[13px] text-ink-400">
182
+ <p>mlv2 © <span id="year"></span></p>
183
+ <button id="admin-trigger" class="-mr-3 rounded-lg px-3 py-2.5 transition-colors duration-200 hover:text-ink-50">Admin</button>
184
184
  </div>
185
185
  </footer>
186
186
 
187
187
  <!-- ================= PLAYER MODAL ================= -->
188
188
  <div id="player-modal" class="fixed inset-0 z-50 hidden">
189
- <div class="absolute inset-0 bg-ink-950/92 backdrop-blur-sm" data-player-close></div>
190
- <div id="player-panel" class="relative h-full w-full flex flex-col opacity-0">
191
- <div class="flex items-center justify-between gap-3 px-3 sm:px-4 h-14 border-b border-ink-800 bg-ink-950">
192
- <div class="flex items-center gap-2.5 min-w-0">
193
- <img id="player-icon" class="h-7 w-7 rounded-md object-cover border border-ink-700" alt="" />
194
- <p id="player-title" class="truncate font-display text-[14px] font-medium"></p>
195
- </div>
196
- <div class="flex items-center gap-1.5 shrink-0">
197
- <a id="player-newtab" target="_blank" rel="noopener" title="Open in new tab"
198
- 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">
199
- <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>
200
- </a>
201
- <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">
202
- <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>
203
- </button>
189
+ <div class="absolute inset-0 bg-black" data-player-close></div>
190
+ <div id="player-panel" class="relative flex h-full w-full flex-col opacity-0">
191
+ <div class="border-b border-white/[0.08] bg-black pt-[env(safe-area-inset-top)]">
192
+ <div class="flex h-14 items-center justify-between gap-3 px-3 sm:px-4">
193
+ <div class="flex min-w-0 items-center gap-3">
194
+ <img id="player-icon" class="h-8 w-8 rounded-[22.37%] object-cover" alt="" />
195
+ <p id="player-title" class="truncate text-[15px] font-semibold"></p>
196
+ </div>
197
+ <div class="flex shrink-0 items-center gap-2">
198
+ <a id="player-newtab" target="_blank" rel="noopener" title="Open in new tab" aria-label="Open in new tab" class="glass-lite icon-btn">
199
+ <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>
200
+ </a>
201
+ <button id="player-close" title="Close" aria-label="Close" class="glass-lite icon-btn">
202
+ <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>
203
+ </button>
204
+ </div>
204
205
  </div>
205
206
  </div>
206
207
  <div class="relative flex-1 bg-black">
@@ -211,40 +212,40 @@
211
212
 
212
213
  <!-- ================= ADMIN: PIN GATE ================= -->
213
214
  <div id="admin-login-modal" class="fixed inset-0 z-50 hidden">
214
- <div class="absolute inset-0 bg-ink-950/85 backdrop-blur-sm" data-admin-login-close></div>
215
+ <div class="absolute inset-0 bg-black/50 backdrop-blur-md" data-admin-login-close></div>
215
216
  <div class="absolute inset-0 flex items-center justify-center p-4">
216
- <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>
217
- <p class="font-display text-[15px] font-medium">Enter PIN</p>
218
- <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>
217
+ <form id="admin-login-form" class="glass glass-sheet relative w-full max-w-[340px] rounded-[28px] p-6 opacity-0" data-fx>
218
+ <p class="text-[19px] font-semibold">Enter PIN</p>
219
+ <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>
219
220
 
220
- <div id="pin-boxes" class="mt-4 grid grid-cols-6 gap-2">
221
+ <div id="pin-boxes" class="mt-5 grid grid-cols-6 gap-2">
221
222
  <!-- 6 single-digit inputs, filled in by JS -->
222
223
  </div>
223
224
  <input id="admin-pin" type="hidden" />
224
225
 
225
- <p id="admin-login-error" class="hidden mt-3 text-[12.5px] text-red-400"></p>
226
+ <p id="admin-login-error" class="mt-3 hidden text-[14px] text-red-400" role="alert"></p>
226
227
 
227
- <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>
228
- <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>
228
+ <button type="submit" id="admin-login-submit" class="btn btn-primary mt-5 w-full">Unlock</button>
229
+ <button type="button" data-admin-login-close class="btn btn-ghost mt-2 w-full">Cancel</button>
229
230
  </form>
230
231
  </div>
231
232
  </div>
232
233
 
233
234
  <!-- ================= ADMIN: PANEL ================= -->
234
235
  <div id="admin-panel" class="fixed inset-0 z-50 hidden">
235
- <div class="absolute inset-0 bg-ink-950/90 backdrop-blur-sm" data-admin-panel-close></div>
236
- <div class="absolute inset-0 flex items-start sm:items-center justify-center p-0 sm:p-4">
237
- <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>
236
+ <div class="absolute inset-0 bg-black/50 backdrop-blur-md" data-admin-panel-close></div>
237
+ <div class="absolute inset-0 flex items-start justify-center p-0 sm:items-center sm:p-4">
238
+ <div id="admin-panel-card" class="glass glass-sheet relative flex h-full w-full flex-col opacity-0 sm:h-[85vh] sm:max-w-2xl sm:rounded-[32px]" data-fx>
238
239
 
239
- <div class="flex items-center justify-between px-5 h-14 border-b border-ink-800 shrink-0">
240
+ <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">
240
241
  <div>
241
- <p class="font-display text-[14px] font-medium leading-none">Admin panel</p>
242
- <p class="mt-1 text-[11px] font-mono text-ink-400">Unlocked with PIN</p>
242
+ <p class="text-[17px] font-semibold leading-none">Admin</p>
243
+ <p class="mt-1 text-[13px] text-ink-400">Unlocked with PIN</p>
243
244
  </div>
244
- <div class="flex items-center gap-1.5">
245
- <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>
246
- <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">
247
- <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>
245
+ <div class="flex items-center gap-2">
246
+ <button id="admin-logout" class="btn btn-ghost">Lock</button>
247
+ <button data-admin-panel-close aria-label="Close" class="glass-lite icon-btn">
248
+ <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>
248
249
  </button>
249
250
  </div>
250
251
  </div>
@@ -252,36 +253,38 @@
252
253
  <div class="flex-1 overflow-y-auto px-5 py-5">
253
254
 
254
255
  <!-- add game form -->
255
- <form id="add-game-form" class="rounded-xl border border-ink-700 p-4">
256
- <p class="text-[11px] font-mono uppercase tracking-wider text-ink-400">Add game</p>
257
- <div class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3">
256
+ <form id="add-game-form" class="rounded-xl p-4 ring-1 ring-white/10">
257
+ <p class="text-[15px] font-semibold">Add game</p>
258
+ <div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
258
259
  <div>
259
- <label class="text-[11px] text-ink-400">Name</label>
260
- <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" />
260
+ <label for="new-name" class="text-[13px] text-ink-300">Name</label>
261
+ <input id="new-name" required class="field mt-1.5" />
261
262
  </div>
262
263
  <div>
263
- <label class="text-[11px] text-ink-400">Category (optional)</label>
264
- <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" />
264
+ <label for="new-category" class="text-[13px] text-ink-300">Category (optional)</label>
265
+ <input id="new-category" class="field mt-1.5" />
265
266
  </div>
266
267
  <div class="sm:col-span-2">
267
- <label class="text-[11px] text-ink-400">Game URL</label>
268
- <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" />
268
+ <label for="new-url" class="text-[13px] text-ink-300">Game URL</label>
269
+ <input id="new-url" required type="url" placeholder="https://" class="field mt-1.5" />
269
270
  </div>
270
271
  <div>
271
- <label class="text-[11px] text-ink-400">Icon upload</label>
272
- <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" />
272
+ <label for="new-icon-file" class="text-[13px] text-ink-300">Icon upload</label>
273
+ <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" />
273
274
  </div>
274
275
  <div>
275
- <label class="text-[11px] text-ink-400">or external icon URL</label>
276
- <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" />
276
+ <label for="new-icon-url" class="text-[13px] text-ink-300">Or external icon URL</label>
277
+ <input id="new-icon-url" type="url" placeholder="https://" class="field mt-1.5" />
277
278
  </div>
278
279
  </div>
279
- <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>
280
- <span id="add-game-status" class="ml-3 text-[12px] text-ink-400"></span>
280
+ <div class="mt-5 flex items-center gap-3">
281
+ <button type="submit" id="add-game-submit" class="btn btn-primary">Add game</button>
282
+ <span id="add-game-status" class="text-[13px] text-ink-400"></span>
283
+ </div>
281
284
  </form>
282
285
 
283
286
  <!-- existing games -->
284
- <p class="mt-6 mb-2 text-[11px] font-mono uppercase tracking-wider text-ink-400">Existing games</p>
287
+ <p class="mb-3 mt-8 text-[15px] font-semibold">Games</p>
285
288
  <div id="admin-list" class="space-y-2"></div>
286
289
  </div>
287
290
  </div>
@@ -289,51 +292,7 @@
289
292
  </div>
290
293
 
291
294
  <!-- ================= TOASTS ================= -->
292
- <div id="toast-root" class="fixed bottom-4 right-4 z-[60] flex flex-col gap-2 items-end"></div>
293
-
294
- <!-- Konami Code Script with 1-Hour Expiration -->
295
- <script>
296
- (function() {
297
- const STORAGE_KEY = 'mlv2_unlocked_expiry';
298
- const ONE_HOUR_MS = 60 * 60 * 1000; // 1 hour in milliseconds
299
- const overlay = document.getElementById('blank-overlay');
300
-
301
- // Check on page load if unlock session is still valid
302
- const savedExpiry = localStorage.getItem(STORAGE_KEY);
303
- if (savedExpiry && Date.now() < parseInt(savedExpiry, 10)) {
304
- overlay.classList.add('unlocked');
305
- }
306
-
307
- const konamiCode = [
308
- 'ArrowUp', 'ArrowUp',
309
- 'ArrowDown', 'ArrowDown',
310
- 'ArrowLeft', 'ArrowRight',
311
- 'ArrowLeft', 'ArrowRight',
312
- 'b', 'a', 'Enter'
313
- ];
314
- let konamiIndex = 0;
315
-
316
- document.addEventListener('keydown', (e) => {
317
- const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
318
- const targetKey = konamiCode[konamiIndex].length === 1
319
- ? konamiCode[konamiIndex].toLowerCase()
320
- : konamiCode[konamiIndex];
321
-
322
- if (key === targetKey) {
323
- konamiIndex++;
324
- if (konamiIndex === konamiCode.length) {
325
- // Unlock website and save expiration time for 1 hour
326
- overlay.classList.add('unlocked');
327
- const expiryTime = Date.now() + ONE_HOUR_MS;
328
- localStorage.setItem(STORAGE_KEY, expiryTime.toString());
329
- konamiIndex = 0;
330
- }
331
- } else {
332
- konamiIndex = 0;
333
- }
334
- });
335
- })();
336
- </script>
295
+ <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>
337
296
 
338
297
  <script src="https://cdn.jsdelivr.net/npm/appwrite@26.2.0"></script>
339
298
  <script type="module">
@@ -361,7 +320,9 @@ const isConfigured = () =>
361
320
  !CONFIG.projectId.includes("YOUR_") &&
362
321
  !CONFIG.databaseId.includes("YOUR_");
363
322
 
323
+ const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
364
324
  function fx(targets, keyframes, options = {}) {
325
+ if (reduceMotion) options = { ...options, duration: 0, delay: 0 };
365
326
  return animate(targets, keyframes, options);
366
327
  }
367
328
 
@@ -386,8 +347,8 @@ function iconSrcFor(game) {
386
347
  function monogramSrc(name) {
387
348
  const initials = (name || "?").trim().slice(0, 2).toUpperCase();
388
349
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
389
- <rect width="100" height="100" fill="#18181c"/>
390
- <text x="50" y="58" font-family="Sora, sans-serif" font-size="34" font-weight="600" fill="#e8142f" text-anchor="middle">${initials}</text>
350
+ <rect width="100" height="100" fill="#161618"/>
351
+ <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>
391
352
  </svg>`;
392
353
  return "data:image/svg+xml;utf8," + encodeURIComponent(svg);
393
354
  }
@@ -399,20 +360,20 @@ function escapeHtml(s) {
399
360
  function toast(message, tone = "default") {
400
361
  const root = el("toast-root");
401
362
  const node = document.createElement("div");
402
- const toneClasses = tone === "error" ? "border-red-500/40 text-red-300" : "border-ink-700 text-ink-100";
403
- 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`;
363
+ const toneClasses = tone === "error" ? "text-red-300" : "text-ink-50";
364
+ node.className = `glass glass-sheet relative rounded-full ${toneClasses} px-4 py-2.5 text-[14px] opacity-0`;
404
365
  node.textContent = message;
405
366
  root.appendChild(node);
406
- fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, easing: "ease-out" });
367
+ fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
407
368
  setTimeout(() => {
408
- fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2, easing: "ease-in" }).finished.then(() => node.remove());
369
+ fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2, ease: "easeIn" }).finished.then(() => node.remove());
409
370
  }, 2800);
410
371
  }
411
372
 
412
373
  /* -------------------------- render: skeleton -------------------------- */
413
374
  function renderSkeleton() {
414
- el("skeleton-grid").innerHTML = Array.from({ length: 10 })
415
- .map(() => `<div class="skel motion-reduce:animate-none aspect-square rounded-2xl border border-ink-800"></div>`)
375
+ el("skeleton-grid").innerHTML = Array.from({ length: 12 })
376
+ .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>`)
416
377
  .join("");
417
378
  }
418
379
  renderSkeleton();
@@ -432,8 +393,8 @@ function renderCategoryBar() {
432
393
  bar.innerHTML = cats
433
394
  .map((c) => {
434
395
  const active = c === activeCategory;
435
- return `<button data-cat="${escapeHtml(c)}" class="shrink-0 rounded-full border px-3 py-1 text-[12px] font-medium transition-colors ${
436
- 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"
396
+ return `<button data-cat="${escapeHtml(c)}" aria-pressed="${active}" class="chip ${
397
+ active ? "chip-on text-white" : "glass-lite text-ink-300 hover:text-ink-50"
437
398
  }">${escapeHtml(c)}</button>`;
438
399
  })
439
400
  .join("");
@@ -446,6 +407,7 @@ function renderCategoryBar() {
446
407
  );
447
408
  }
448
409
 
410
+ let gridShown = false;
449
411
  function renderGrid() {
450
412
  const grid = el("grid");
451
413
  const games = currentFiltered();
@@ -473,13 +435,12 @@ function renderGrid() {
473
435
  grid.innerHTML = games
474
436
  .map((g) => {
475
437
  const src = iconSrcFor(g) || monogramSrc(g.name);
476
- 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">
477
- <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)}'" />
478
- <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>
479
- <div class="absolute inset-x-0 bottom-0 p-3">
480
- <p class="truncate text-[12.5px] font-medium text-ink-50 leading-tight">${escapeHtml(g.name)}</p>
481
- ${g.category ? `<p class="truncate mt-0.5 text-[9.5px] font-mono uppercase tracking-wide text-ink-400">${escapeHtml(g.category)}</p>` : ""}
482
- </div>
438
+ return `<button data-fx data-id="${g.$id}" class="tile group block w-full text-left opacity-0">
439
+ <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]">
440
+ <img src="${src}" alt="" loading="lazy" class="tile-img h-full w-full object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
441
+ </span>
442
+ <span class="mt-2.5 block truncate text-center text-[13px] font-medium leading-tight text-ink-50">${escapeHtml(g.name)}</span>
443
+ ${g.category ? `<span class="mt-0.5 block truncate text-center text-[12px] text-ink-400">${escapeHtml(g.category)}</span>` : ""}
483
444
  </button>`;
484
445
  })
485
446
  .join("");
@@ -488,7 +449,10 @@ function renderGrid() {
488
449
  card.addEventListener("click", () => openPlayer(games.find((g) => g.$id === card.dataset.id)))
489
450
  );
490
451
 
491
- fx("#grid [data-fx]", { opacity: [0, 1], y: [10, 0] }, { duration: 0.35, delay: stagger(0.035), easing: "ease-out" });
452
+ // full entrance once; later filter/search changes only fade
453
+ fx("#grid [data-fx]", { opacity: [0, 1], y: gridShown ? 0 : [8, 0] },
454
+ { duration: gridShown ? 0.18 : 0.5, delay: gridShown ? 0 : stagger(0.03), ease: "easeOut" });
455
+ gridShown = true;
492
456
  }
493
457
 
494
458
  el("count-badge") && (el("count-num").textContent = "0");
@@ -547,10 +511,10 @@ function openPlayer(game) {
547
511
  const modal = el("player-modal");
548
512
  modal.classList.remove("hidden");
549
513
  document.documentElement.style.overflow = "hidden";
550
- fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28, easing: "ease-out" });
514
+ fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28, ease: "easeOut" });
551
515
  }
552
516
  function closePlayer() {
553
- fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18, easing: "ease-in" }).finished.then(() => {
517
+ fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18, ease: "easeIn" }).finished.then(() => {
554
518
  el("player-modal").classList.add("hidden");
555
519
  el("player-frame").src = "";
556
520
  document.documentElement.style.overflow = "";
@@ -569,7 +533,7 @@ document.addEventListener("keydown", (e) => {
569
533
  /* -------------------------------- admin -------------------------------- */
570
534
  function openAdminLogin() {
571
535
  el("admin-login-modal").classList.remove("hidden");
572
- fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, easing: "ease-out" });
536
+ fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
573
537
  resetPinBoxes();
574
538
  pinInputs[0]?.focus();
575
539
  }
@@ -579,7 +543,7 @@ function closeAdminLogin() {
579
543
  }
580
544
  function openAdminPanel() {
581
545
  el("admin-panel").classList.remove("hidden");
582
- fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28, easing: "ease-out" });
546
+ fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28, ease: "easeOut" });
583
547
  renderAdminList();
584
548
  }
585
549
  function closeAdminPanel() {
@@ -601,8 +565,8 @@ const pinBoxesEl = el("pin-boxes");
601
565
  pinBoxesEl.innerHTML = Array.from({ length: 6 })
602
566
  .map(
603
567
  (_, i) =>
604
- `<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1"
605
- 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" />`
568
+ `<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1" aria-label="Digit ${i + 1}"
569
+ 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" />`
606
570
  )
607
571
  .join("");
608
572
  const pinInputs = Array.from(pinBoxesEl.querySelectorAll("[data-pin-idx]"));
@@ -657,7 +621,7 @@ el("admin-logout").addEventListener("click", () => {
657
621
 
658
622
  async function renderAdminList() {
659
623
  const list = el("admin-list");
660
- list.innerHTML = `<p class="text-[12.5px] text-ink-400">Loading…</p>`;
624
+ list.innerHTML = `<p class="text-[14px] text-ink-400">Loading…</p>`;
661
625
  try {
662
626
  const res = await tablesDB.listRows({
663
627
  databaseId: CONFIG.databaseId,
@@ -665,24 +629,24 @@ async function renderAdminList() {
665
629
  queries: [Query.limit(200), Query.orderAsc("order")],
666
630
  });
667
631
  if (res.rows.length === 0) {
668
- list.innerHTML = `<p class="text-[12.5px] text-ink-400">No games added yet.</p>`;
632
+ list.innerHTML = `<p class="text-[14px] text-ink-400">No games added yet.</p>`;
669
633
  return;
670
634
  }
671
635
  list.innerHTML = res.rows
672
636
  .map(
673
637
  (g) => `
674
- <div class="flex items-center gap-3 rounded-xl border border-ink-700 p-2.5" data-row="${g.$id}">
675
- <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)}'" />
638
+ <div class="flex items-center gap-3 rounded-xl p-3 ring-1 ring-white/10" data-row="${g.$id}">
639
+ <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)}'" />
676
640
  <div class="min-w-0 flex-1">
677
- <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" />
678
- <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" />
641
+ <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" />
642
+ <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" />
679
643
  </div>
680
- <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" />
681
- <label class="hidden sm:flex items-center gap-1 text-[11px] text-ink-400 shrink-0">
682
- <input type="checkbox" data-field="enabled" data-id="${g.$id}" ${g.enabled === false ? "" : "checked"} class="accent-red-500" /> on
644
+ <input data-field="category" data-id="${g.$id}" value="${escapeHtml(g.category || "")}" placeholder="Category" aria-label="Category" class="field hidden !w-28 sm:block" />
645
+ <label class="hidden shrink-0 items-center gap-2 text-[13px] text-ink-300 sm:flex">
646
+ <input type="checkbox" data-field="enabled" data-id="${g.$id}" ${g.enabled === false ? "" : "checked"} class="h-4 w-4 accent-red-500" /> On
683
647
  </label>
684
- <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">
685
- <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>
648
+ <button data-delete="${g.$id}" title="Delete" aria-label="Delete" class="glass-lite icon-btn hover:!text-red-400">
649
+ <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>
686
650
  </button>
687
651
  </div>`
688
652
  )
@@ -718,7 +682,7 @@ async function renderAdminList() {
718
682
  })
719
683
  );
720
684
  } catch (err) {
721
- list.innerHTML = `<p class="text-[12.5px] text-red-400">${escapeHtml(err?.message || "Failed to load games.")}</p>`;
685
+ list.innerHTML = `<p class="text-[14px] text-red-400">${escapeHtml(err?.message || "Failed to load games.")}</p>`;
722
686
  }
723
687
  }
724
688
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlv2boii",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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";
package/holdw.py DELETED
@@ -1,21 +0,0 @@
1
- import keyboard
2
- import time
3
-
4
-
5
-
6
- def start():
7
- keyboard_pressing = True
8
- while keyboard_pressing:
9
- keyboard.press("W")
10
- time.sleep(0.1)
11
-
12
- if keyboard.is_pressed("C"):
13
- keyboard_pressing = False
14
- while not (keyboard.is_pressed("X")):
15
- time.sleep(0.1)
16
- keyboard_pressing = True; start()
17
-
18
- start()
19
-
20
-
21
-