beta3 commited on
Commit
88319df
·
verified ·
1 Parent(s): e1cc8d3

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +979 -19
index.html CHANGED
@@ -1,19 +1,979 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ASCII Font Explorer · beta3</title>
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>✦</text></svg>" />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet" />
10
+
11
+ <style>
12
+ /* ═══ RESET & TOKENS ══════════════════════════════════════════════ */
13
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
14
+
15
+ :root {
16
+ --bg: #07070f;
17
+ --s0: #0c0c18;
18
+ --s1: #111120;
19
+ --s2: #181828;
20
+ --s3: #202035;
21
+ --bd: rgba(255,255,255,.065);
22
+ --bd-hi: rgba(255,255,255,.14);
23
+ --txt: #eaeaf5;
24
+ --m1: #55556e;
25
+ --m2: #7a7a98;
26
+
27
+ --v: #7c3aed;
28
+ --c: #06b6d4;
29
+ --a: #f59e0b;
30
+ --pk: #ec4899;
31
+
32
+ --gv: linear-gradient(135deg,#7c3aed,#6366f1);
33
+ --gc: linear-gradient(135deg,#0891b2,#06b6d4);
34
+ --ga: linear-gradient(135deg,#d97706,#f59e0b);
35
+ --gm: linear-gradient(135deg,#7c3aed,#06b6d4);
36
+ --galt:linear-gradient(135deg,#ec4899,#f59e0b);
37
+
38
+ --glow-v: rgba(124,58,237,.22);
39
+ --glow-c: rgba(6,182,212,.22);
40
+
41
+ --r: 14px;
42
+ --rsm: 8px;
43
+ --rxs: 5px;
44
+
45
+ --mono: 'JetBrains Mono','Fira Code','Cascadia Code',monospace;
46
+ --sans: 'Inter',system-ui,sans-serif;
47
+ }
48
+
49
+ html { scroll-behavior: smooth; }
50
+ body {
51
+ font-family: var(--sans);
52
+ background: var(--bg);
53
+ color: var(--txt);
54
+ min-height: 100dvh;
55
+ overflow-x: hidden;
56
+ -webkit-font-smoothing: antialiased;
57
+ }
58
+
59
+ /* ═══ NOISE GRAIN ═════════════════════════════════════════════════ */
60
+ body::before {
61
+ content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
62
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.025'/%3E%3C/svg%3E");
63
+ opacity: .6;
64
+ }
65
+
66
+ /* ═══ BACKGROUND ORBS ════════════════════════════════════════════ */
67
+ .bg-canvas { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
68
+ .orb { position: absolute; border-radius: 50%; filter: blur(110px); animation: orb-drift 22s ease-in-out infinite; }
69
+ .oa { width:700px;height:700px;background:#7c3aed;opacity:.065;top:-280px;left:-220px;animation-delay:0s; }
70
+ .ob { width:600px;height:600px;background:#06b6d4;opacity:.055;bottom:-240px;right:-180px;animation-delay:-8s; }
71
+ .oc { width:420px;height:420px;background:#f59e0b;opacity:.04;top:42%;left:38%;animation-delay:-15s; }
72
+ .od { width:320px;height:320px;background:#ec4899;opacity:.04;top:12%;right:16%;animation-delay:-5s; }
73
+ @keyframes orb-drift {
74
+ 0%,100%{transform:translate(0,0)scale(1)}
75
+ 25%{transform:translate(28px,-32px)scale(1.06)}
76
+ 50%{transform:translate(-18px,24px)scale(0.94)}
77
+ 75%{transform:translate(38px,10px)scale(1.03)}
78
+ }
79
+
80
+ /* ═══ LAYOUT ══════════════════════════════════════════════════════ */
81
+ .wrap {
82
+ position: relative; z-index: 1;
83
+ max-width: 1480px; margin: 0 auto; padding: 0 28px 80px;
84
+ }
85
+
86
+ /* ═══ KEYFRAMES ═══════════════════════════════════════════════════ */
87
+ @keyframes sd { from{opacity:0;transform:translateY(-18px)} to{opacity:1;transform:none} }
88
+ @keyframes su { from{opacity:0;transform:translateY(18px)} to{opacity:1;transform:none} }
89
+ @keyframes fi { from{opacity:0} to{opacity:1} }
90
+ @keyframes cpop{ from{opacity:0;transform:scale(.6)} to{opacity:1;transform:scale(1)} }
91
+ @keyframes shim{ from{background-position:200% 0} to{background-position:-200% 0} }
92
+ @keyframes spin{ to{transform:rotate(360deg)} }
93
+ @keyframes pulse-d{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.65)}}
94
+
95
+ /* ═══ LOADER ══════════════════════════════════════════════════════ */
96
+ #loader {
97
+ position: fixed; inset: 0; z-index: 300; background: var(--bg);
98
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px;
99
+ transition: opacity .4s;
100
+ }
101
+ #loader.out { opacity: 0; pointer-events: none; }
102
+ .l-ring {
103
+ width: 44px; height: 44px; border-radius: 50%;
104
+ border: 3px solid var(--bd); border-top-color: var(--v);
105
+ animation: spin .65s linear infinite;
106
+ }
107
+ .l-msg { font-size: 14px; color: var(--m2); }
108
+ .l-sub { font-size: 12px; color: var(--m1); font-family: var(--mono); }
109
+
110
+ /* ═══ HEADER ══════════════════════════════════════════════════════ */
111
+ header { text-align: center; padding: 68px 0 52px; }
112
+
113
+ .pill {
114
+ display: inline-flex; align-items: center; gap: 8px;
115
+ background: rgba(124,58,237,.11); border: 1px solid rgba(124,58,237,.28);
116
+ border-radius: 100px; padding: 5px 14px 5px 10px;
117
+ font-size: 12px; font-weight: 500; color: #a78bfa; letter-spacing: .04em;
118
+ margin-bottom: 26px;
119
+ animation: sd .6s cubic-bezier(.16,1,.3,1) both;
120
+ }
121
+ .pill-dot {
122
+ width: 6px; height: 6px; border-radius: 50%;
123
+ background: var(--v); box-shadow: 0 0 8px var(--v);
124
+ animation: pulse-d 2.2s ease infinite;
125
+ }
126
+ h1 {
127
+ font-size: clamp(2.6rem,6.5vw,4.8rem); font-weight: 800;
128
+ letter-spacing: -.035em; line-height: 1.06;
129
+ animation: sd .6s .07s cubic-bezier(.16,1,.3,1) both;
130
+ }
131
+ .gt {
132
+ background: var(--gm); -webkit-background-clip: text;
133
+ -webkit-text-fill-color: transparent; background-clip: text;
134
+ }
135
+ .subtitle {
136
+ margin-top: 16px; color: var(--m2); font-size: 1.05rem;
137
+ line-height: 1.65; animation: sd .6s .14s cubic-bezier(.16,1,.3,1) both;
138
+ }
139
+
140
+ /* Stat strip */
141
+ .stat-strip {
142
+ display: inline-flex; margin-top: 36px;
143
+ border: 1px solid var(--bd); border-radius: var(--r); overflow: hidden;
144
+ animation: sd .6s .2s cubic-bezier(.16,1,.3,1) both;
145
+ }
146
+ .si {
147
+ padding: 13px 28px; text-align: center; background: var(--s0);
148
+ border-right: 1px solid var(--bd);
149
+ }
150
+ .si:last-child { border-right: none; }
151
+ .si-n {
152
+ font-size: 1.5rem; font-weight: 800;
153
+ background: var(--gm); -webkit-background-clip: text;
154
+ -webkit-text-fill-color: transparent; background-clip: text;
155
+ }
156
+ .si-l { font-size: 11px; color: var(--m1); text-transform: uppercase; letter-spacing: .09em; margin-top: 2px; }
157
+
158
+ /* ═══ CARD ════════════════════════════════════════════════════════ */
159
+ .card { background: var(--s0); border: 1px solid var(--bd); border-radius: var(--r); }
160
+ .card-body { padding: 22px 26px; }
161
+
162
+ /* ═══ SECTION HEAD ════════════════════════════════════════════════ */
163
+ .sh { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
164
+ .sh-label { font-size: 11px; font-weight: 600; color: var(--m1); text-transform: uppercase; letter-spacing: .1em; white-space: nowrap; }
165
+ .sh-line { flex: 1; height: 1px; background: var(--bd); }
166
+ .badge {
167
+ font-size: 11px; font-weight: 700; padding: 3px 9px;
168
+ border-radius: 100px; white-space: nowrap;
169
+ }
170
+ .bv { background: rgba(124,58,237,.17); color: #c4b5fd; border: 1px solid rgba(124,58,237,.3); }
171
+ .bc { background: rgba(6,182,212,.13); color: #67e8f9; border: 1px solid rgba(6,182,212,.24); }
172
+ .ba { background: rgba(245,158,11,.13); color: #fcd34d; border: 1px solid rgba(245,158,11,.25); }
173
+
174
+ /* ═══ FONT SECTION ════════════════════════════════════════════════ */
175
+ .font-section { margin-bottom: 22px; animation: su .6s .28s cubic-bezier(.16,1,.3,1) both; }
176
+
177
+ /* Controls */
178
+ .ctrl-row { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
179
+ .srch-wrap { position: relative; flex: 1; min-width: 180px; }
180
+ .srch-ico { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: var(--m1); pointer-events: none; }
181
+ .srch-in {
182
+ width: 100%; padding: 9px 12px 9px 33px;
183
+ background: var(--s1); border: 1px solid var(--bd); border-radius: var(--rsm);
184
+ font-family: var(--sans); font-size: 13px; color: var(--txt); outline: none;
185
+ transition: border-color .18s, box-shadow .18s;
186
+ }
187
+ .srch-in:focus { border-color: var(--c); box-shadow: 0 0 0 3px var(--glow-c); }
188
+ .srch-in::placeholder { color: var(--m1); }
189
+
190
+ .btn {
191
+ display: inline-flex; align-items: center; gap: 6px;
192
+ padding: 9px 14px; border-radius: var(--rsm);
193
+ font-family: var(--sans); font-size: 13px; font-weight: 500;
194
+ cursor: pointer; border: 1px solid var(--bd); transition: all .14s;
195
+ white-space: nowrap; color: var(--txt); background: var(--s1);
196
+ }
197
+ .btn:hover { border-color: var(--bd-hi); background: var(--s2); }
198
+ .btn-grad { background: var(--gm); border-color: transparent; color: #fff; font-weight: 600; }
199
+ .btn-grad:hover { opacity: .84; transform: translateY(-1px); }
200
+ .btn-dim { background: transparent; color: var(--m1); }
201
+ .btn-dim:hover { border-color: rgba(239,68,68,.3); color: #fca5a5; }
202
+
203
+ /* Chips */
204
+ .chips-row { display: flex; flex-wrap: wrap; gap: 7px; min-height: 30px; margin-bottom: 14px; align-items: center; }
205
+ .chip-hint { font-size: 12px; color: var(--m1); font-style: italic; }
206
+ .chip {
207
+ display: flex; align-items: center; gap: 5px;
208
+ border-radius: 100px; padding: 4px 10px 4px 12px;
209
+ font-size: 12px; font-family: var(--mono);
210
+ animation: cpop .22s cubic-bezier(.34,1.56,.64,1) both;
211
+ }
212
+ .chip-font { background: rgba(124,58,237,.12); border: 1px solid rgba(124,58,237,.3); color: #c4b5fd; }
213
+ .chip-x {
214
+ background: none; border: none; cursor: pointer; opacity: .5; font-size: 15px;
215
+ line-height: 1; padding: 0; transition: opacity .12s; color: inherit;
216
+ }
217
+ .chip-x:hover { opacity: 1; }
218
+
219
+ /* Font grid */
220
+ #font-grid {
221
+ display: grid;
222
+ grid-template-columns: repeat(auto-fill, minmax(144px, 1fr));
223
+ gap: 7px; max-height: 290px; overflow-y: auto; padding-right: 4px;
224
+ scrollbar-width: thin; scrollbar-color: var(--bd) transparent;
225
+ }
226
+ #font-grid::-webkit-scrollbar { width: 4px; }
227
+ #font-grid::-webkit-scrollbar-thumb { background: var(--bd); border-radius: 10px; }
228
+
229
+ .fcard {
230
+ position: relative; padding: 10px 13px; border-radius: var(--rsm);
231
+ border: 1px solid var(--bd); background: var(--s1);
232
+ cursor: pointer; user-select: none; overflow: hidden;
233
+ transition: border-color .13s, box-shadow .13s, transform .13s;
234
+ }
235
+ .fcard::after {
236
+ content: ''; position: absolute; inset: 0;
237
+ background: var(--gm); opacity: 0; transition: opacity .13s;
238
+ }
239
+ .fcard:hover:not(.fd) { border-color: var(--bd-hi); transform: translateY(-1px); }
240
+ .fcard:hover:not(.fd)::after { opacity: .055; }
241
+ .fcard.fs {
242
+ border-color: var(--v);
243
+ box-shadow: 0 0 0 1px var(--v), 0 4px 16px var(--glow-v);
244
+ }
245
+ .fcard.fs::after { opacity: .09; }
246
+ .fcard.fd { opacity: .28; cursor: not-allowed; pointer-events: none; }
247
+ .fcard-n {
248
+ position: relative; z-index: 1;
249
+ font-family: var(--mono); font-size: 11.5px; color: var(--txt); word-break: break-all; line-height: 1.4;
250
+ }
251
+ .fcard.fs .fcard-n { color: #c4b5fd; }
252
+ .fcard-i { position: relative; z-index: 1; font-size: 10px; color: var(--m1); margin-top: 3px; opacity: .5; }
253
+
254
+ .skel {
255
+ background: linear-gradient(90deg,var(--s1) 25%,var(--s2) 50%,var(--s1) 75%);
256
+ background-size: 200% 100%; animation: shim 1.4s ease infinite;
257
+ border-color: transparent !important; cursor: default; pointer-events: none;
258
+ }
259
+
260
+ /* ═══ LETTER SECTION ══════════════════════════════════════════════ */
261
+ .letter-section { margin-bottom: 22px; animation: su .6s .36s cubic-bezier(.16,1,.3,1) both; }
262
+
263
+ .alpha-grid {
264
+ display: flex; flex-wrap: wrap; gap: 6px;
265
+ }
266
+ .lkey {
267
+ width: 38px; height: 38px; border-radius: var(--rsm);
268
+ display: flex; align-items: center; justify-content: center;
269
+ font-family: var(--mono); font-size: 14px; font-weight: 600;
270
+ border: 1px solid var(--bd); background: var(--s1);
271
+ cursor: pointer; user-select: none;
272
+ transition: border-color .13s, background .13s, box-shadow .13s, transform .13s;
273
+ color: var(--m2);
274
+ }
275
+ .lkey:hover:not(.ld) { border-color: var(--bd-hi); background: var(--s2); color: var(--txt); transform: translateY(-1px); }
276
+ .lkey.ls {
277
+ border-color: var(--c);
278
+ background: rgba(6,182,212,.12);
279
+ color: #67e8f9;
280
+ box-shadow: 0 0 0 1px var(--c), 0 4px 14px var(--glow-c);
281
+ }
282
+ .lkey.ld { opacity: .28; cursor: not-allowed; }
283
+
284
+ /* ═══ ERROR BOX ═══════════════════════════════════════════════════ */
285
+ .err-box {
286
+ background: rgba(239,68,68,.09); border: 1px solid rgba(239,68,68,.3);
287
+ border-radius: var(--rsm); padding: 11px 15px; font-size: 13px;
288
+ color: #fca5a5; margin-bottom: 14px; display: none;
289
+ }
290
+
291
+ /* ═══ MATRIX PREVIEW ══════════════════════════════════════════════ */
292
+ #matrix-wrap { margin-top: 28px; animation: su .4s ease both; }
293
+
294
+ /*
295
+ CSS Grid matrix: rows = letters (max 2), cols = fonts (max 3).
296
+ */
297
+ #matrix-grid {
298
+ display: grid;
299
+ gap: 16px;
300
+ /* columns injected by JS */
301
+ }
302
+
303
+ .row-label {
304
+ display: flex; flex-direction: column; justify-content: center; align-items: center;
305
+ gap: 4px; padding: 0 4px;
306
+ }
307
+ .row-letter {
308
+ width: 36px; height: 36px; border-radius: var(--rsm);
309
+ display: flex; align-items: center; justify-content: center;
310
+ font-family: var(--mono); font-size: 15px; font-weight: 700;
311
+ border: 1px solid var(--c); color: #67e8f9;
312
+ background: rgba(6,182,212,.1);
313
+ }
314
+
315
+ /* Each cell in the matrix */
316
+ .mcell {
317
+ background: var(--s0); border: 1px solid var(--bd); border-radius: var(--r);
318
+ overflow: hidden; transition: border-color .18s, box-shadow .18s;
319
+ min-width: 0;
320
+ }
321
+ .mcell:hover { border-color: var(--bd-hi); box-shadow: 0 6px 24px rgba(0,0,0,.3); }
322
+
323
+ .mcell.col-0 .cell-pre { background: linear-gradient(100deg,#c4b5fd,#93c5fd); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
324
+ .mcell.col-1 .cell-pre { background: linear-gradient(100deg,#67e8f9,#6ee7b7); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
325
+ .mcell.col-2 .cell-pre { background: linear-gradient(100deg,#fcd34d,#fb923c); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
326
+
327
+ .cell-head {
328
+ display: flex; align-items: center; justify-content: space-between;
329
+ padding: 10px 14px; background: var(--s1); border-bottom: 1px solid var(--bd);
330
+ }
331
+ .cell-font-name {
332
+ font-family: var(--mono); font-size: 12px; font-weight: 500; color: var(--m2);
333
+ }
334
+ .mcell.col-0 .cell-font-name { background: var(--gv); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
335
+ .mcell.col-1 .cell-font-name { background: var(--gc); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
336
+ .mcell.col-2 .cell-font-name { background: var(--ga); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
337
+
338
+ .cell-lbl {
339
+ font-size: 11px; font-weight: 600; font-family: var(--mono);
340
+ padding: 2px 7px; border-radius: 4px;
341
+ }
342
+ .mcell.col-0 .cell-lbl { background: rgba(124,58,237,.18); color: #c4b5fd; }
343
+ .mcell.col-1 .cell-lbl { background: rgba(6,182,212,.15); color: #67e8f9; }
344
+ .mcell.col-2 .cell-lbl { background: rgba(245,158,11,.15); color: #fcd34d; }
345
+
346
+ .cell-acts { display: flex; gap: 5px; align-items: center; }
347
+ .ib {
348
+ width: 26px; height: 26px; border-radius: var(--rxs);
349
+ background: rgba(255,255,255,.04); border: 1px solid var(--bd);
350
+ color: var(--m1); cursor: pointer; display: flex; align-items: center;
351
+ justify-content: center; transition: all .12s; position: relative; flex-shrink: 0;
352
+ }
353
+ .ib:hover { background: rgba(255,255,255,.09); color: var(--txt); }
354
+ .ib.ok { border-color: #10b981; color: #10b981; }
355
+ .tip {
356
+ position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
357
+ background: var(--s3); border: 1px solid var(--bd);
358
+ border-radius: 5px; padding: 3px 7px; font-size: 10px; color: var(--txt);
359
+ white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity .12s; z-index: 20;
360
+ }
361
+ .ib:hover .tip { opacity: 1; }
362
+
363
+ .cell-body {
364
+ padding: 18px 16px; overflow-x: auto; min-height: 88px;
365
+ display: flex; align-items: flex-start;
366
+ scrollbar-width: thin; scrollbar-color: var(--bd) transparent;
367
+ }
368
+ .cell-body::-webkit-scrollbar { height: 4px; }
369
+ .cell-body::-webkit-scrollbar-thumb { background: var(--bd); border-radius: 10px; }
370
+
371
+ .cell-pre {
372
+ font-family: var(--mono); font-size: 12px; line-height: 1.25;
373
+ white-space: pre; margin: 0; animation: fi .3s ease;
374
+ }
375
+
376
+ .cell-spin {
377
+ display: flex; align-items: center; gap: 8px;
378
+ color: var(--m1); font-size: 12px; width: 100%;
379
+ }
380
+ .spin-sm {
381
+ width: 13px; height: 13px; border-radius: 50%;
382
+ border: 2px solid var(--bd); border-top-color: var(--v);
383
+ animation: spin .55s linear infinite; flex-shrink: 0;
384
+ }
385
+
386
+ /* Column header (font name) above each column */
387
+ .col-header {
388
+ text-align: center; padding-bottom: 2px;
389
+ }
390
+ .col-header-name {
391
+ font-family: var(--mono); font-size: 12px; color: var(--m2); font-weight: 500;
392
+ }
393
+
394
+ /* Platform links in stat strip */
395
+ .si-links { padding: 12px 22px; }
396
+ .platform-link {
397
+ display: inline-flex; align-items: center; gap: 5px;
398
+ font-size: 12px; font-weight: 600; text-decoration: none;
399
+ padding: 5px 10px; border-radius: var(--rxs);
400
+ border: 1px solid; transition: all .15s;
401
+ line-height: 1;
402
+ }
403
+ .hf-link {
404
+ color: #f0c060;
405
+ border-color: rgba(240,192,96,.3);
406
+ background: rgba(240,192,96,.08);
407
+ }
408
+ .hf-link:hover {
409
+ background: rgba(240,192,96,.16);
410
+ border-color: rgba(240,192,96,.55);
411
+ transform: translateY(-1px);
412
+ box-shadow: 0 4px 12px rgba(240,192,96,.15);
413
+ }
414
+ .kg-link {
415
+ color: #5ac8fa;
416
+ border-color: rgba(90,200,250,.3);
417
+ background: rgba(90,200,250,.08);
418
+ }
419
+ .kg-link:hover {
420
+ background: rgba(90,200,250,.16);
421
+ border-color: rgba(90,200,250,.55);
422
+ transform: translateY(-1px);
423
+ box-shadow: 0 4px 12px rgba(90,200,250,.15);
424
+ }
425
+
426
+ /* ═══ FOOTER ══════════════════════════════════════════════════════ */
427
+ footer {
428
+ text-align: center; padding: 30px 0 0; margin-top: 58px;
429
+ border-top: 1px solid var(--bd); color: var(--m1); font-size: 12px; line-height: 1.9;
430
+ }
431
+ footer a { color: #a78bfa; text-decoration: none; }
432
+ footer a:hover { text-decoration: underline; }
433
+
434
+ /* ═══ RESPONSIVE ═══════════════════════════════════════���══════════ */
435
+ @media(max-width:640px) {
436
+ .wrap { padding: 0 14px 48px; }
437
+ header { padding: 44px 0 34px; }
438
+ .stat-strip { flex-direction: column; width: 100%; }
439
+ .si { border-right: none; border-bottom: 1px solid var(--bd); }
440
+ .si:last-child { border-bottom: none; }
441
+ #font-grid { grid-template-columns: repeat(auto-fill,minmax(120px,1fr)); }
442
+ .alpha-grid .lkey { width: 33px; height: 33px; font-size: 12px; }
443
+ }
444
+ </style>
445
+ </head>
446
+ <body>
447
+
448
+ <!-- Loader -->
449
+ <div id="loader">
450
+ <div class="l-ring"></div>
451
+ <div class="l-msg">Loading font index…</div>
452
+ <div class="l-sub" id="l-sub">connecting to Hugging Face</div>
453
+ </div>
454
+
455
+ <!-- BG orbs -->
456
+ <div class="bg-canvas">
457
+ <div class="orb oa"></div><div class="orb ob"></div>
458
+ <div class="orb oc"></div><div class="orb od"></div>
459
+ </div>
460
+
461
+ <div class="wrap">
462
+
463
+ <!-- ═══ HEADER ═══════════════════════════════════════════════════ -->
464
+ <header>
465
+ <div class="pill"><div class="pill-dot"></div>beta3 · ASCII Alphabet Dataset</div>
466
+ <h1>Explore <span class="gt">571 Fonts</span><br>side by side</h1>
467
+ <p class="subtitle">
468
+ Pick up to 3 fonts · Choose up to 3 letters<br>
469
+ Compare how each font renders them in a live matrix
470
+ </p>
471
+ <div class="stat-strip">
472
+ <div class="si"><div class="si-n" id="sn-f">571</div><div class="si-l">Fonts</div></div>
473
+ <div class="si"><div class="si-n">26</div><div class="si-l">Letters A–Z</div></div>
474
+ <div class="si si-links">
475
+ <div class="si-l" style="margin-bottom:8px;font-size:10px;">Available on</div>
476
+ <div style="display:flex;gap:8px;justify-content:center;align-items:center;">
477
+ <a href="https://huggingface.co/datasets/beta3/ASCII_Alphabet_Dataset_571_Fonts" target="_blank" rel="noopener" class="platform-link hf-link">
478
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.514 2 12 6.486 2 12 2zm-1 5v2H9v2h2v6h2v-6h2V9h-2V7h-2z"/></svg>
479
+ Hugging Face
480
+ </a>
481
+ <span style="color:var(--bd-hi);font-size:11px;">·</span>
482
+ <a href="https://www.kaggle.com/datasets/beta3logic/ascii-alphabet-dataset-571-fonts" target="_blank" rel="noopener" class="platform-link kg-link">
483
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M18.825 23.859c-.022.092-.117.141-.281.141h-3.139c-.187 0-.351-.082-.492-.248l-5.178-6.589-1.448 1.374v5.111c0 .235-.117.352-.351.352H5.505c-.236 0-.354-.117-.354-.352V.353c0-.233.118-.353.354-.353h2.431c.234 0 .351.12.351.353v14.343l6.203-6.272c.165-.165.33-.246.495-.246h3.239c.144 0 .236.06.285.18.046.149.034.254-.036.315l-6.555 6.344 6.836 8.507c.095.104.117.208.07.311z"/></svg>
484
+ Kaggle
485
+ </a>
486
+ </div>
487
+ </div>
488
+ </div>
489
+ </header>
490
+
491
+ <!-- ═══ FONT SELECTOR ════════════════════════════════════════════ -->
492
+ <div class="font-section card">
493
+ <div class="card-body">
494
+
495
+ <div class="sh">
496
+ <span class="sh-label">1 · Select fonts</span>
497
+ <div class="sh-line"></div>
498
+ <span class="badge bv" id="sel-badge">0 / 3 selected</span>
499
+ <span class="badge bc" id="tot-badge">571 available</span>
500
+ </div>
501
+
502
+ <div class="err-box" id="err-box"></div>
503
+
504
+ <div class="ctrl-row">
505
+ <div class="srch-wrap">
506
+ <svg class="srch-ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
507
+ <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
508
+ </svg>
509
+ <input id="srch" class="srch-in" type="text" placeholder="Search 571 fonts…" autocomplete="off" />
510
+ </div>
511
+ <button class="btn btn-grad" id="btn-random">
512
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
513
+ <polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/>
514
+ <polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/>
515
+ </svg>
516
+ Random
517
+ </button>
518
+ <button class="btn btn-dim" id="btn-clear-fonts">
519
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
520
+ <polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/>
521
+ </svg>
522
+ Clear
523
+ </button>
524
+ </div>
525
+
526
+ <!-- Font chips -->
527
+ <div class="chips-row" id="font-chips">
528
+ <span class="chip-hint" id="font-chips-hint">← select fonts from the grid below</span>
529
+ </div>
530
+
531
+ <!-- Font grid -->
532
+ <div id="font-grid"></div>
533
+
534
+ </div>
535
+ </div>
536
+
537
+ <!-- ═══ LETTER SELECTOR ══════════════════════════════════════════ -->
538
+ <div class="letter-section card" style="margin-top:18px;">
539
+ <div class="card-body">
540
+
541
+ <div class="sh">
542
+ <span class="sh-label">2 · Select letters</span>
543
+ <div class="sh-line"></div>
544
+ <span class="badge ba" id="let-badge">0 / 3 selected</span>
545
+ </div>
546
+
547
+ <div class="alpha-grid" id="alpha-grid"></div>
548
+
549
+ <!-- Letter chips -->
550
+ <div class="chips-row" id="let-chips" style="margin-top:14px;margin-bottom:0;">
551
+ <span class="chip-hint" id="let-chips-hint">← pick up to 3 letters</span>
552
+ </div>
553
+
554
+ </div>
555
+ </div>
556
+
557
+ <!-- ═══ MATRIX PREVIEW ══════════════════════════════════════════ -->
558
+ <div id="matrix-wrap" style="display:none;">
559
+ <div class="sh" style="margin-bottom:14px;">
560
+ <span class="sh-label">Live matrix</span>
561
+ <div class="sh-line"></div>
562
+ <span class="badge bc" id="matrix-dims">—</span>
563
+ </div>
564
+ <div id="matrix-grid"></div>
565
+ </div>
566
+
567
+ <!-- ═══ FOOTER ══════════════════════════════════════════════════ -->
568
+ <footer>
569
+ <p>
570
+ Dataset →
571
+ <a href="https://huggingface.co/datasets/beta3/ASCII_Alphabet_Dataset_571_Fonts" target="_blank" rel="noopener">
572
+ beta3/ASCII_Alphabet_Dataset_571_Fonts
573
+ </a>
574
+ &nbsp;·&nbsp; 571 PyFiglet fonts &nbsp;·&nbsp; Letters A–Z uppercase
575
+ </p>
576
+ <p style="opacity:.5;margin-top:3px;">Static Hugging Face Space · fully client-side · no backend</p>
577
+ </footer>
578
+
579
+ </div><!-- /wrap -->
580
+
581
+ <script>
582
+ /* ================================================================
583
+ CONFIG
584
+ ================================================================ */
585
+ const OWNER = 'beta3';
586
+ const DATASET = 'ASCII_Alphabet_Dataset_571_Fonts';
587
+ const BRANCH = 'main';
588
+ const RAW = `https://huggingface.co/datasets/${OWNER}/${DATASET}/resolve/${BRANCH}`;
589
+ const API_DIR = `https://huggingface.co/api/datasets/${OWNER}/${DATASET}/tree/${BRANCH}/uppercase`;
590
+
591
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
592
+
593
+ /* ================================================================
594
+ UTILS
595
+ ================================================================ */
596
+ const toFile = ch =>
597
+ `letter_${String(ch.charCodeAt(0) - 64).padStart(2,'0')}.txt`;
598
+
599
+ const esc = s =>
600
+ s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
601
+
602
+ const IC_COPY = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
603
+ const IC_CHECK = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>`;
604
+ const IC_X = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
605
+
606
+ function pickRandom(arr, n) {
607
+ const a = [...arr];
608
+ for (let i = a.length - 1; i > 0; i--) {
609
+ const j = Math.floor(Math.random() * (i + 1));
610
+ [a[i], a[j]] = [a[j], a[i]];
611
+ }
612
+ return a.slice(0, n);
613
+ }
614
+
615
+ /* ================================================================
616
+ STATE
617
+ ================================================================ */
618
+ let allFonts = [];
619
+ let filteredFonts = [];
620
+ let selFonts = [];
621
+ let selLetters = [];
622
+ let renderTimer = null;
623
+ const cache = {};
624
+
625
+ /* ================================================================
626
+ DOM REFS
627
+ ================================================================ */
628
+ const $loader = document.getElementById('loader');
629
+ const $lSub = document.getElementById('l-sub');
630
+ const $srch = document.getElementById('srch');
631
+ const $fontGrid = document.getElementById('font-grid');
632
+ const $fontChips = document.getElementById('font-chips');
633
+ const $fontHint = document.getElementById('font-chips-hint');
634
+ const $letChips = document.getElementById('let-chips');
635
+ const $letHint = document.getElementById('let-chips-hint');
636
+ const $alphaGrid = document.getElementById('alpha-grid');
637
+ const $selBadge = document.getElementById('sel-badge');
638
+ const $totBadge = document.getElementById('tot-badge');
639
+ const $letBadge = document.getElementById('let-badge');
640
+ const $errBox = document.getElementById('err-box');
641
+ const $matWrap = document.getElementById('matrix-wrap');
642
+ const $matGrid = document.getElementById('matrix-grid');
643
+ const $matDims = document.getElementById('matrix-dims');
644
+ const $snF = document.getElementById('sn-f');
645
+
646
+ /* ================================================================
647
+ FONT LOADING (paginated HF Tree API)
648
+ ================================================================ */
649
+ async function loadFonts() {
650
+ const fonts = [];
651
+ let url = API_DIR + '?type=directory';
652
+
653
+ while (url) {
654
+ const res = await fetch(url);
655
+ if (!res.ok) throw new Error(`HF API ${res.status} ${res.statusText}`);
656
+
657
+ const items = await res.json();
658
+ items.forEach(it => {
659
+ if (it.type === 'directory') {
660
+ fonts.push(it.path.split('/').pop());
661
+ }
662
+ });
663
+
664
+ const link = res.headers.get('Link') || '';
665
+ const next = link.match(/<([^>]+)>;\s*rel="next"/);
666
+ url = next ? next[1] : null;
667
+
668
+ $lSub.textContent = `${fonts.length} fonts discovered…`;
669
+ }
670
+
671
+ return fonts.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
672
+ }
673
+
674
+ /* ================================================================
675
+ INIT
676
+ ================================================================ */
677
+ async function init() {
678
+ renderSkeleton(24);
679
+ buildAlphaGrid();
680
+
681
+ try {
682
+ allFonts = await loadFonts();
683
+ } catch (err) {
684
+ showErr(`Could not load font list: ${err.message}`);
685
+ hideLoader();
686
+ return;
687
+ }
688
+
689
+ if (!allFonts.length) {
690
+ showErr('No font directories found. Dataset structure may have changed.');
691
+ hideLoader();
692
+ return;
693
+ }
694
+
695
+ filteredFonts = [...allFonts];
696
+ $snF.textContent = allFonts.length;
697
+ $totBadge.textContent = `${allFonts.length} available`;
698
+
699
+ renderFontGrid();
700
+ hideLoader();
701
+
702
+ // Default: 1 random font, letter A
703
+ const starter = pickRandom(allFonts, 1)[0];
704
+ toggleFont(starter);
705
+ toggleLetter('A');
706
+ }
707
+
708
+ function hideLoader() {
709
+ $loader.classList.add('out');
710
+ setTimeout(() => ($loader.style.display = 'none'), 420);
711
+ }
712
+
713
+ /* ================================================================
714
+ FONT GRID
715
+ ================================================================ */
716
+ function renderSkeleton(n) {
717
+ $fontGrid.innerHTML = '';
718
+ for (let i = 0; i < n; i++) {
719
+ const d = document.createElement('div');
720
+ d.className = 'fcard skel'; d.style.height = '52px';
721
+ $fontGrid.appendChild(d);
722
+ }
723
+ }
724
+
725
+ function renderFontGrid() {
726
+ $fontGrid.innerHTML = '';
727
+ if (!filteredFonts.length) {
728
+ $fontGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:var(--m1);padding:24px 0;font-size:13px;">No fonts match your search.</div>';
729
+ return;
730
+ }
731
+ const frag = document.createDocumentFragment();
732
+ filteredFonts.forEach(font => {
733
+ const isSel = selFonts.includes(font);
734
+ const isDis = !isSel && selFonts.length >= 3;
735
+ const el = document.createElement('div');
736
+ el.className = `fcard${isSel ? ' fs' : ''}${isDis ? ' fd' : ''}`;
737
+ el.dataset.font = font;
738
+ el.innerHTML = `<div class="fcard-n">${font}</div><div class="fcard-i">#${allFonts.indexOf(font)+1}</div>`;
739
+ el.addEventListener('click', () => toggleFont(font));
740
+ frag.appendChild(el);
741
+ });
742
+ $fontGrid.appendChild(frag);
743
+ }
744
+
745
+ function patchFontGrid() {
746
+ $fontGrid.querySelectorAll('.fcard').forEach(el => {
747
+ const font = el.dataset.font;
748
+ if (!font) return;
749
+ const isSel = selFonts.includes(font);
750
+ const isDis = !isSel && selFonts.length >= 3;
751
+ el.className = `fcard${isSel ? ' fs' : ''}${isDis ? ' fd' : ''}`;
752
+ });
753
+ }
754
+
755
+ /* ================================================================
756
+ FONT TOGGLE
757
+ ================================================================ */
758
+ function toggleFont(font) {
759
+ if (selFonts.includes(font)) {
760
+ selFonts = selFonts.filter(f => f !== font);
761
+ } else {
762
+ if (selFonts.length >= 3) return;
763
+ selFonts.push(font);
764
+ }
765
+ patchFontGrid();
766
+ renderFontChips();
767
+ $selBadge.textContent = `${selFonts.length} / 3 selected`;
768
+ scheduleRender();
769
+ }
770
+
771
+ function renderFontChips() {
772
+ [...$fontChips.children].forEach(el => { if (el !== $fontHint) el.remove(); });
773
+ $fontHint.style.display = selFonts.length ? 'none' : '';
774
+ selFonts.forEach(font => {
775
+ const chip = document.createElement('div');
776
+ chip.className = 'chip chip-font';
777
+ chip.innerHTML = `<span>${font}</span><button class="chip-x">×</button>`;
778
+ chip.querySelector('.chip-x').addEventListener('click', () => toggleFont(font));
779
+ $fontChips.appendChild(chip);
780
+ });
781
+ }
782
+
783
+ /* ================================================================
784
+ ALPHABET GRID
785
+ ================================================================ */
786
+ function buildAlphaGrid() {
787
+ $alphaGrid.innerHTML = '';
788
+ ALPHABET.forEach(letter => {
789
+ const btn = document.createElement('button');
790
+ btn.className = 'lkey';
791
+ btn.dataset.letter = letter;
792
+ btn.textContent = letter;
793
+ btn.addEventListener('click', () => toggleLetter(letter));
794
+ $alphaGrid.appendChild(btn);
795
+ });
796
+ }
797
+
798
+ function patchAlphaGrid() {
799
+ $alphaGrid.querySelectorAll('.lkey').forEach(btn => {
800
+ const l = btn.dataset.letter;
801
+ const isSel = selLetters.includes(l);
802
+ const isDis = !isSel && selLetters.length >= 3;
803
+ btn.className = `lkey${isSel ? ' ls' : ''}${isDis ? ' ld' : ''}`;
804
+ });
805
+ }
806
+
807
+ function toggleLetter(letter) {
808
+ if (selLetters.includes(letter)) {
809
+ selLetters = selLetters.filter(l => l !== letter);
810
+ } else {
811
+ if (selLetters.length >= 3) return;
812
+ selLetters.push(letter);
813
+ }
814
+ patchAlphaGrid();
815
+ renderLetterChips();
816
+ $letBadge.textContent = `${selLetters.length} / 3 selected`;
817
+ scheduleRender();
818
+ }
819
+
820
+ function renderLetterChips() {
821
+ [...$letChips.children].forEach(el => { if (el !== $letHint) el.remove(); });
822
+ $letHint.style.display = selLetters.length ? 'none' : '';
823
+ selLetters.forEach(l => {
824
+ const chip = document.createElement('div');
825
+ chip.className = 'chip';
826
+ chip.style.cssText = 'background:rgba(6,182,212,.1);border:1px solid rgba(6,182,212,.3);color:#67e8f9;';
827
+ chip.innerHTML = `<span>${l}</span><button class="chip-x">×</button>`;
828
+ chip.querySelector('.chip-x').addEventListener('click', () => toggleLetter(l));
829
+ $letChips.appendChild(chip);
830
+ });
831
+ }
832
+
833
+ /* ================================================================
834
+ LETTER FETCHER
835
+ ================================================================ */
836
+ async function fetchLetter(font, ch) {
837
+ const key = `${font}/${toFile(ch)}`;
838
+ if (key in cache) return cache[key];
839
+ try {
840
+ const res = await fetch(`${RAW}/uppercase/${font}/${toFile(ch)}`);
841
+ cache[key] = res.ok ? await res.text() : null;
842
+ } catch {
843
+ cache[key] = null;
844
+ }
845
+ return cache[key];
846
+ }
847
+
848
+ /* ================================================================
849
+ MATRIX RENDER
850
+ ================================================================ */
851
+ function scheduleRender() {
852
+ clearTimeout(renderTimer);
853
+ renderTimer = setTimeout(renderMatrix, 180);
854
+ }
855
+
856
+ async function renderMatrix() {
857
+ const fonts = selFonts;
858
+ const letters = selLetters;
859
+
860
+ if (!fonts.length || !letters.length) {
861
+ $matWrap.style.display = 'none';
862
+ return;
863
+ }
864
+
865
+ $matWrap.style.display = 'block';
866
+ $matDims.textContent = `${letters.length} row${letters.length>1?'s':''} × ${fonts.length} col${fonts.length>1?'s':''}`;
867
+
868
+ /*
869
+ Grid layout (CSS grid):
870
+ - 1 column per font
871
+ - 1 row per letter
872
+ Each cell = (letter, font) pair showing the raw ASCII art
873
+ */
874
+ $matGrid.style.gridTemplateColumns = `repeat(${fonts.length}, 1fr)`;
875
+ $matGrid.innerHTML = '';
876
+
877
+ const promises = [];
878
+
879
+ letters.forEach((letter, ri) => {
880
+ fonts.forEach((font, ci) => {
881
+ const cell = document.createElement('div');
882
+ cell.className = `mcell col-${ci}`;
883
+ const rowTag = ci === 0
884
+ ? `<div class="cell-lbl" style="margin-right:4px;">${letter}</div>`
885
+ : '';
886
+
887
+ cell.innerHTML = `
888
+ <div class="cell-head">
889
+ <div style="display:flex;align-items:center;gap:6px;">
890
+ ${rowTag}
891
+ <div class="cell-font-name">${font}</div>
892
+ </div>
893
+ <div class="cell-acts">
894
+ <button class="ib copy-btn">${IC_COPY}<span class="tip">Copy</span></button>
895
+ </div>
896
+ </div>
897
+ <div class="cell-body" id="cb-${ri}-${ci}">
898
+ <div class="cell-spin"><div class="spin-sm"></div>Loading…</div>
899
+ </div>
900
+ `;
901
+ $matGrid.appendChild(cell);
902
+
903
+ const body = cell.querySelector('.cell-body');
904
+ const copyBtn = cell.querySelector('.copy-btn');
905
+
906
+ const p = fetchLetter(font, letter).then(txt => {
907
+ const ascii = txt
908
+ ? txt.replace(/\r\n/g,'\n').replace(/\r/g,'\n')
909
+ : `(${letter} not found in ${font})`;
910
+
911
+ body.innerHTML = `<pre class="cell-pre">${esc(ascii)}</pre>`;
912
+
913
+ copyBtn.addEventListener('click', () => {
914
+ navigator.clipboard?.writeText(ascii).then(() => {
915
+ copyBtn.classList.add('ok');
916
+ copyBtn.innerHTML = IC_CHECK + '<span class="tip">Copied!</span>';
917
+ setTimeout(() => {
918
+ copyBtn.classList.remove('ok');
919
+ copyBtn.innerHTML = IC_COPY + '<span class="tip">Copy</span>';
920
+ }, 1800);
921
+ });
922
+ });
923
+ });
924
+
925
+ promises.push(p);
926
+ });
927
+ });
928
+
929
+ await Promise.all(promises);
930
+ }
931
+
932
+ /* ================================================================
933
+ EVENTS
934
+ ================================================================ */
935
+ $srch.addEventListener('input', () => {
936
+ const q = $srch.value.toLowerCase().trim();
937
+ filteredFonts = q
938
+ ? allFonts.filter(f => f.toLowerCase().includes(q))
939
+ : [...allFonts];
940
+ $totBadge.textContent = `${filteredFonts.length} available`;
941
+ renderFontGrid();
942
+ });
943
+
944
+ document.getElementById('btn-random').addEventListener('click', () => {
945
+ const avail = allFonts.filter(f => !selFonts.includes(f));
946
+ const slots = 3 - selFonts.length;
947
+ if (!slots || !avail.length) return;
948
+ pickRandom(avail, Math.min(slots, avail.length)).forEach(f => {
949
+ if (selFonts.length < 3) selFonts.push(f);
950
+ });
951
+ patchFontGrid();
952
+ renderFontChips();
953
+ $selBadge.textContent = `${selFonts.length} / 3 selected`;
954
+ scheduleRender();
955
+ });
956
+
957
+ document.getElementById('btn-clear-fonts').addEventListener('click', () => {
958
+ selFonts = [];
959
+ patchFontGrid();
960
+ renderFontChips();
961
+ $selBadge.textContent = '0 / 3 selected';
962
+ scheduleRender();
963
+ });
964
+
965
+ /* ================================================================
966
+ HELPERS
967
+ ================================================================ */
968
+ function showErr(msg) {
969
+ $errBox.textContent = msg;
970
+ $errBox.style.display = 'block';
971
+ }
972
+
973
+ /* ================================================================
974
+ BOOT
975
+ ================================================================ */
976
+ init();
977
+ </script>
978
+ </body>
979
+ </html>