1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>séance — a p2p haunted-house party game</title>
<style>
:root {
--bg: #0d0a14; --panel: #171224; --panel-2: #1d1730; --border: #2c2140;
--text: #e8dbc5; --muted: #8a7d99; --amber: #ffb347; --ghost: #9fe8dc;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg); color: var(--text);
font: 16px/1.65 Georgia, 'Iowan Old Style', 'Times New Roman', serif;
padding: 40px 20px 80px;
}
main { max-width: 780px; margin: 0 auto; }
h1 { font-size: 36px; letter-spacing: 8px; color: var(--amber); text-shadow: 0 0 22px #ffb34766; }
.tag { color: var(--muted); margin: 6px 0 30px; font-size: 17px; }
h2 { margin: 44px 0 12px; font-size: 22px; color: var(--amber); letter-spacing: 1px; }
h3 { margin: 24px 0 8px; font-size: 17px; color: var(--ghost); }
p, li { color: #cdc0ad; }
ul, ol { padding-left: 22px; margin: 10px 0; }
li { margin: 5px 0; }
a { color: var(--ghost); }
pre {
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
padding: 14px 16px; overflow-x: auto; margin: 12px 0;
font: 13.5px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
}
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 14px;
background: var(--panel-2); border-radius: 5px; padding: 1px 6px; }
pre code { background: none; padding: 0; }
.card {
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
padding: 18px 20px; margin: 14px 0;
}
.btns { display: flex; gap: 10px; flex-wrap: wrap; margin: 18px 0 6px; }
.btn {
display: inline-block; padding: 10px 18px; border-radius: 8px; text-decoration: none;
background: #241a30; border: 1px solid #4a3860; color: var(--amber);
letter-spacing: 1px; font-size: 14px;
}
.btn:hover { background: #31234a; box-shadow: 0 0 14px #ffb34744; }
footer { margin-top: 60px; color: var(--muted); font-size: 13px; }
.langs {
position: fixed; top: 12px; right: 14px; color: var(--muted); font-size: 13px;
letter-spacing: .06em; user-select: none;
}
.langs button {
font: inherit; color: inherit; background: none; border: 0; padding: 3px 4px;
cursor: pointer; opacity: .55;
}
.langs button.on { color: var(--amber); opacity: 1; }
</style>
</head>
<body>
<nav class="langs" id="langs"></nav>
<main data-lang-block lang="en">
<h1>SÉANCE</h1>
<p class="tag">a haunted parlour game for one house, sixteen phones and three real
rooms — this page is also the git repo.</p>
<div class="btns">
<a class="btn" href="https://game4.ardegazu.ro/">🕯 hold a séance</a>
<a class="btn" href="browse/index.html">browse the code</a>
<a class="btn" href="#clone">clone the repo</a>
</div>
<h2 id="clone">Clone</h2>
<p>This site serves the repository over git's dumb-HTTP protocol as plain static
files (plain files — there is no git server here, or any server at all):</p>
<pre><code>git clone https://git.ardegazu.ro/game4.git
cd seance</code></pre>
<h2>What it is</h2>
<p>A party game you play with your feet. Gather 1–16 people in a home, agree
which real room is <em>the Study</em>, <em>the Parlour</em> and <em>the
Cellar</em>, and open one shared link on every phone. Each time the bell tolls
you have ~20 seconds to physically walk to a room and tap it. A ghost sweeps
one room — and it favours wherever the crowd stood last bell. Everyone caught
loses a candle; lose all three and you fade. Last lit candle wins the séance.</p>
<p>Every bell, one player is secretly the <strong>medium</strong>: their phone
alone names a room the spirit will shun — truthfully. They can share it,
keep it, or lie about it out loud. Where you <em>actually</em> stand is the
honor system; the walking is the point, not the enforcement. Installable PWA,
works from an iOS home screen.</p>
<h2>How it works</h2>
<ul>
<li><strong>No game server.</strong> Browsers are
<a href="https://libp2p.io">js-libp2p</a> peers: they dial a shared
circuit-relay-v2 peer over WSS, discover each other through gossipsub,
and upgrade to direct WebRTC — the circuit is signaling plus a
rate-limited fallback path. The room secret lives in the URL fragment
(<code>#…</code>) and never leaves the browser.</li>
<li><strong>Rooms are cryptographic.</strong> Discovery is app-wide, so a
room is enforced by a room-scoped protocol id
(<code>/seance/2/<HKDF(secret)></code> — wrong rooms fail protocol
negotiation) plus a mutual sealed hello: one AES-256-GCM envelope keyed
from the room secret and bound to both noise-authenticated peer ids.
After the hello, every pair's frames ride libp2p's noise encryption,
end-to-end even when routed through the relay. The p2p layer
(<code>client/src/net/</code>) is copied from
<a href="https://git.ardegazu.ro/game1/">neon-grid</a>.</li>
<li><strong>One peer conducts.</strong> The lowest peer id runs the bell
timers and the ghost's dice, collects secret room picks, and broadcasts
tiny phase frames; everyone renders from them. Séances are seat-indexed so
guests joining or leaving can't corrupt one in flight. If the conductor's
phone dies the séance is voided and the next-lowest id takes over; late
joiners get a snapshot and watch from the veil until the next séance.</li>
<li><strong>Hosting without an origin server.</strong> The client is a static
Vite build served as plain files — there is no backend anywhere. This page
you're reading works the same way.</li>
</ul>
<h2>What's in the repo</h2>
<div class="card">
<h3>client/src/game/ — the game</h3>
<p>TypeScript, no framework: typed wire protocol, host-authoritative séance
logic (bells, crowd-hungry ghost, medium hints, candle elimination),
WebAudio bell tolls, candlelit DOM/CSS scenes.</p>
</div>
<div class="card">
<h3>client/src/net/ — the p2p layer</h3>
<p>js-libp2p assembly copied from neon-grid: relay bootstrap (identity
fetched from <code>/.well-known/ap2p</code> with a compiled-in fallback),
room crypto, the sealed-hello membership gate, and one long-lived
varint-framed stream per pair. Only the protocol id is game-specific.</p>
</div>
<div class="card">
<h3>deploy/ — the publish pipeline</h3>
<p><code>publish-repo.sh</code> builds this very site: landing page + bare
mirror exploded to loose objects (static file servers and git's dumb-HTTP protocol
agree on "every path must exist"), with an anonymity gate that refuses to
publish identity-bearing bytes.</p>
</div>
<h2>Run it yourself</h2>
<pre><code>cd client && npm install && npm run dev # :4173
# open two tabs on the same #room URL and haunt yourself (keys 1/2/3 pick rooms)</code></pre>
<p>Deploy: <code>npm run build</code>, then publish <code>client/dist/</code> as
static files anywhere — IPFS, a CDN, a folder behind nginx. It needs nothing but
a signaling relay it's allowed to talk to (see <code>src/game/config.ts</code>).</p>
<h2>Guarantees & limits, honestly</h2>
<ul>
<li>The relay operator sees connection metadata, peer ids, and (until the
WebRTC upgrade lands) opaque noise ciphertext and its sizes/timing. Not
content, not names, not who the ghost took.</li>
<li>Anyone with the room link can join and play — capability model. The
16-guest cap is enforced by the conductor (the mesh is O(n²); ~12 is the
practical ceiling).</li>
<li>The conductor is one of the players; a cheating conductor could load the
ghost's dice. Between friends that's the same trust as letting someone
shuffle the deck. Other players' picks can't be forged.</li>
<li>Your tapped room is taken on faith — the game can't see your feet.
That's a feature: the sprint to the cellar is the game.</li>
<li>MIT licensed. Built on js-libp2p, WebRTC and WebCrypto.</li>
</ul>
<footer>
Live game: <a href="https://game4.ardegazu.ro/">game4.ardegazu.ro</a> ·
this repo: <code>git clone https://git.ardegazu.ro/game4.git</code> ·
p2p core from <a href="https://git.ardegazu.ro/chat/">sueta</a>
</footer>
</main>
<main data-lang-block lang="ro" hidden>
<h1>SÉANCE</h1>
<p class="tag">un joc de salon bântuit pentru o casă, șaisprezece telefoane și trei
încăperi adevărate — pagina asta e și repo-ul git.</p>
<div class="btns">
<a class="btn" href="https://game4.ardegazu.ro/">🕯 ține o ședință</a>
<a class="btn" href="browse/index.html">răsfoiește codul</a>
<a class="btn" href="#clone-ro">clonează repo-ul</a>
</div>
<h2 id="clone-ro">Clonare</h2>
<p>Situl servește repository-ul prin protocolul dumb-HTTP al lui git, ca simple
fișiere statice (chiar simple fișiere — nu există aici niciun server git, de fapt
niciun server):</p>
<pre><code>git clone https://git.ardegazu.ro/game4.git
cd seance</code></pre>
<h2>Ce este</h2>
<p>Un joc de petrecere pe care îl joci cu picioarele. Adună 1–16 oameni într-o
casă, stabiliți care încăpere e <em>Biroul</em>, care <em>Salonul</em> și care
<em>Pivnița</em>, și deschideți același link pe fiecare telefon. De fiecare dată
când bate clopotul ai ~20 de secunde să mergi fizic într-o încăpere și să o
atingi. O fantomă mătură o încăpere — și o atrage locul unde a stat mulțimea la
clopotul trecut. Toți cei prinși pierd o lumânare; le pierzi pe toate trei și te
stingi. Ultima lumânare aprinsă câștigă ședința.</p>
<p>La fiecare clopot, un jucător e în secret <strong>mediumul</strong>: doar
telefonul lui numește o încăpere pe care spiritul o va ocoli — adevărat. Poate
să spună, să tacă sau să mintă cu voce tare. Unde stai <em>de fapt</em> rămâne
pe cuvânt de onoare; mersul e miza, nu controlul. PWA instalabil, merge de pe
ecranul de start al unui iPhone.</p>
<h2>Cum funcționează</h2>
<ul>
<li><strong>Fără server de joc.</strong> Browserele sunt peers
<a href="https://libp2p.io">js-libp2p</a>: sună un peer circuit-relay-v2
comun prin WSS, se descoperă prin gossipsub și trec pe WebRTC direct —
circuitul e semnalizare plus o cale de rezervă cu limită de trafic.
Secretul camerei stă în fragmentul URL-ului (<code>#…</code>) și nu pleacă
niciodată din browser.</li>
<li><strong>Camerele sunt criptografice.</strong> Descoperirea e la nivel de
aplicație, așa că o cameră e impusă printr-un id de protocol derivat din
cameră (<code>/seance/2/<HKDF(secret)></code> — camerele greșite pică
la negocierea protocolului) plus un hello sigilat reciproc: un plic
AES-256-GCM cu cheia din secretul camerei, legat de ambele peer id-uri
autentificate prin noise. După hello, cadrele fiecărei perechi merg pe
criptarea noise a lui libp2p, cap-la-cap chiar și prin releu. Stratul p2p
(<code>client/src/net/</code>) e copiat din
<a href="https://git.ardegazu.ro/game1/">neon-grid</a>.</li>
<li><strong>Un peer conduce.</strong> Cel mai mic peer id ține ceasul
clopotelor și zarurile fantomei, strânge alegerile secrete de încăperi și
difuzează cadre de fază minuscule; restul doar randează. Ședințele sunt
indexate pe scaune, așa că oaspeții care vin sau pleacă nu pot corupe una
în desfășurare. Dacă telefonul maestrului moare, ședința se anulează și
preia următorul id; cine intră târziu primește un snapshot și privește de
dincolo de văl până la ședința următoare.</li>
<li><strong>Găzduire fără server de origine.</strong> Clientul e un build Vite
static servit ca simple fișiere — nu există backend nicăieri. Pagina pe care
o citești funcționează la fel.</li>
</ul>
<h2>Ce e în repo</h2>
<div class="card">
<h3>client/src/game/ — jocul</h3>
<p>TypeScript, fără framework: protocol de rețea tipat, logică de ședință
autoritativă la gazdă (clopote, fantomă atrasă de aglomerație, șoapte pentru
medium, eliminare pe lumânări), dangăte WebAudio, scene DOM/CSS la lumina
lumânării.</p>
</div>
<div class="card">
<h3>client/src/net/ — stratul p2p</h3>
<p>Asamblaj js-libp2p copiat din neon-grid: bootstrap de releu (identitatea
citită din <code>/.well-known/ap2p</code> cu rezervă compilată), criptografia
camerei, poarta de apartenență cu hello sigilat, și un singur stream
persistent cu cadre varint per pereche. Doar id-ul de protocol e specific
jocului.</p>
</div>
<div class="card">
<h3>deploy/ — conducta de publicare</h3>
<p><code>publish-repo.sh</code> construiește chiar situl ăsta: pagină de
prezentare + oglindă bare explodată în loose objects (serverele de fișiere
statice și dumb-HTTP-ul lui git cad de acord că „orice cale trebuie să
existe"), cu o poartă de anonimat care refuză să publice octeți care poartă
identitate.</p>
</div>
<h2>Rulează-l singur</h2>
<pre><code>cd client && npm install && npm run dev # :4173
# deschide două taburi pe același URL #cameră și bântuie-te singur (tastele 1/2/3 aleg încăperile)</code></pre>
<p>Deploy: <code>npm run build</code>, apoi publică <code>client/dist/</code> ca
fișiere statice oriunde — IPFS, un CDN, un director în spatele lui nginx. N-are
nevoie decât de un releu de semnalizare cu care are voie să vorbească (vezi
<code>src/game/config.ts</code>).</p>
<h2>Garanții și limite, pe bune</h2>
<ul>
<li>Operatorul releului vede metadate de conexiune, peer id-uri și (până se
face trecerea pe WebRTC) ciphertext noise opac cu dimensiunile și ritmul
lui. Nu conținut, nu nume, nu pe cine a luat fantoma.</li>
<li>Oricine are linkul camerei poate intra și juca — model de capabilitate.
Plafonul de 16 oaspeți îl impune maestrul (mesh-ul e O(n²); ~12 e plafonul
practic).</li>
<li>Maestrul e unul dintre jucători; un maestru care trișează poate măslui
zarurile fantomei. Între prieteni e aceeași încredere ca atunci când lași
pe cineva să amestece cărțile. Alegerile celorlalți nu pot fi falsificate.</li>
<li>Încăperea atinsă e luată pe cuvânt — jocul nu-ți vede picioarele. E o
calitate: sprintul spre pivniță e chiar jocul.</li>
<li>Licență MIT. Construit pe js-libp2p, WebRTC și WebCrypto.</li>
</ul>
<footer>
Jocul live: <a href="https://game4.ardegazu.ro/">game4.ardegazu.ro</a> ·
repo-ul: <code>git clone https://git.ardegazu.ro/game4.git</code> ·
nucleul p2p din <a href="https://git.ardegazu.ro/chat/">sueta</a>
</footer>
</main>
<main data-lang-block lang="hu" hidden>
<h1>SÉANCE</h1>
<p class="tag">kísértetjárta szalonjáték egy házra, tizenhat telefonra és három
igazi szobára — ez az oldal egyben a git repó is.</p>
<div class="btns">
<a class="btn" href="https://game4.ardegazu.ro/">🕯 tarts szeánszot</a>
<a class="btn" href="browse/index.html">böngészd a kódot</a>
<a class="btn" href="#clone-hu">repó klónozása</a>
</div>
<h2 id="clone-hu">Klónozás</h2>
<p>Az oldal a repót a git dumb-HTTP protokollján át szolgálja ki, sima statikus
fájlokként (tényleg sima fájlok — nincs itt git szerver, sőt semmilyen szerver):</p>
<pre><code>git clone https://git.ardegazu.ro/game4.git
cd seance</code></pre>
<h2>Mi ez</h2>
<p>Egy partijáték, amit a lábaddal játszol. Gyűjts össze 1–16 embert egy házban,
beszéljétek meg, melyik igazi szoba <em>a dolgozószoba</em>, melyik <em>a
szalon</em> és melyik <em>a pince</em>, aztán nyissátok meg ugyanazt a linket
minden telefonon. Ahányszor megkondul a harang, ~20 másodperced van ténylegesen
átsétálni egy szobába és rákoppintani. Egy szellem végigsöpör egy szobán — és
oda húz, ahol a tömeg állt az előző harangszónál. Akit elkap, elveszít egy
gyertyát; ha mindhármat elveszted, elenyészel. Az utolsó égő gyertya nyeri a
szeánszot.</p>
<p>Minden harangszónál egy játékos titokban a <strong>médium</strong>: egyedül
az ő telefonja nevez meg egy szobát, amit a szellem elkerül — igazat mondva.
Elmondhatja, magában tarthatja, vagy hazudhat róla fennhangon. Hogy
<em>valójában</em> hol állsz, becsületbeli ügy; a séta a lényeg, nem az
ellenőrzés. Telepíthető PWA, iOS kezdőképernyőről is megy.</p>
<h2>Hogyan működik</h2>
<ul>
<li><strong>Nincs játékszerver.</strong> A böngészők
<a href="https://libp2p.io">js-libp2p</a> peerek: egy közös circuit-relay-v2
peert hívnak WSS-en, gossipsubbal fedezik fel egymást, majd közvetlen
WebRTC-re váltanak — a circuit jelzésre való, plusz egy korlátozott sávú
tartalék útvonal. A szoba titka az URL fragmentben él (<code>#…</code>),
és soha nem hagyja el a böngészőt.</li>
<li><strong>A szobák kriptográfiaiak.</strong> A felfedezés app-szintű, ezért
a szobát egy szoba-hatókörű protokoll-id kényszeríti ki
(<code>/seance/2/<HKDF(secret)></code> — rossz szobával megbukik a
protokoll-egyeztetés), plusz egy kölcsönös lepecsételt hello: egyetlen
AES-256-GCM boríték a szoba titkából származtatott kulccsal, mindkét
noise-hitelesített peer id-hez kötve. A hello után minden pár keretei a
libp2p noise-titkosításán mennek, végponttól végpontig, a relén át is. A
p2p réteg (<code>client/src/net/</code>) a
<a href="https://git.ardegazu.ro/game1/">neon-grid</a>ből van másolva.</li>
<li><strong>Az egyik peer vezényel.</strong> A legkisebb peer id kezeli a
harang időzítőit és a szellem kockáit, begyűjti a titkos szobaválasztásokat,
és apró fázis-kereteket sugároz; a többiek ezekből rajzolnak. A szeánszok
szék-indexeltek, így az érkező-távozó vendégek nem ronthatnak el egy
folyamatban lévőt. Ha a mester telefonja kimúlik, a szeánsz érvénytelen, és
a következő legkisebb id veszi át; a későn érkezők pillanatképet kapnak, és
a fátyol mögül néznek a következő szeánszig.</li>
<li><strong>Hosztolás origin-szerver nélkül.</strong> A kliens statikus Vite
build, sima fájlokként kiszolgálva — nincs sehol backend. Ez az oldal is
pontosan így működik.</li>
</ul>
<h2>Mi van a repóban</h2>
<div class="card">
<h3>client/src/game/ — a játék</h3>
<p>TypeScript, framework nélkül: típusos wire-protokoll, hoszt-mérvadó
szeánsz-logika (harangok, tömegre éhes szellem, médium-súgások,
gyertya-kiesés), WebAudio harangkondulás, gyertyafényes DOM/CSS jelenetek.</p>
</div>
<div class="card">
<h3>client/src/net/ — a p2p réteg</h3>
<p>A neon-gridből másolt js-libp2p összeállítás: relé-bootstrap (az identitás
a <code>/.well-known/ap2p</code>-ből jön, beforgatott tartalékkal),
szoba-kriptográfia, a lepecsételt hello tagsági kapu, és páronként egyetlen
hosszú életű, varint-keretezett stream. Csak a protokoll-id játékspecifikus.</p>
</div>
<div class="card">
<h3>deploy/ — a publikáló futószalag</h3>
<p>A <code>publish-repo.sh</code> építi ezt az oldalt: nyitólap + bare tükör
loose objectekre bontva (a statikus fájlszerverek és a git dumb-HTTP-je
egyetértenek abban, hogy „minden útvonalnak léteznie kell"), és egy
anonimitás-kapu, amely nem enged identitáshordozó bájtokat publikálni.</p>
</div>
<h2>Futtasd magad</h2>
<pre><code>cd client && npm install && npm run dev # :4173
# nyiss két fület ugyanarra a #szoba URL-re, és kísértsd magad (az 1/2/3 billentyű szobát választ)</code></pre>
<p>Telepítés: <code>npm run build</code>, aztán tedd ki a
<code>client/dist/</code>-et statikus fájlokként bárhová — IPFS, CDN, egy mappa
nginx mögött. Semmi másra nincs szüksége, csak egy jelző-relére, amivel
beszélhet (lásd <code>src/game/config.ts</code>).</p>
<h2>Garanciák és korlátok, őszintén</h2>
<ul>
<li>A relé üzemeltetője kapcsolati metaadatokat, peer id-kat és (amíg a
WebRTC-váltás meg nem történik) átlátszatlan noise-ciphertextet lát, annak
méretével és ütemével. Tartalmat, neveket, a szellem áldozatát nem.</li>
<li>Bárki, akinél ott a szoba linkje, beléphet és játszhat — capability
modell. A 16 vendéges plafont a mester tartatja be (a mesh O(n²); ~12 a
gyakorlati plafon).</li>
<li>A mester az egyik játékos; egy csaló mester cinkelheti a szellem
kockáit. Barátok közt ez ugyanaz a bizalom, mint amikor hagyod, hogy valaki
megkeverje a paklit. A többiek választásait nem lehet hamisítani.</li>
<li>A rákoppintott szobát elhisszük — a játék nem látja a lábadat. Ez nem
hiba: a sprint a pincéig maga a játék.</li>
<li>MIT licenc. js-libp2p-re, WebRTC-re és WebCryptóra épül.</li>
</ul>
<footer>
Az élő játék: <a href="https://game4.ardegazu.ro/">game4.ardegazu.ro</a> ·
a repó: <code>git clone https://git.ardegazu.ro/game4.git</code> ·
a p2p mag a <a href="https://git.ardegazu.ro/chat/">suetá</a>ból
</footer>
</main>
<script>
(function () {
var LANGS = ["en", "ro", "hu"];
var KEY = "ardegazu-site:lang";
var lang = null;
try { lang = localStorage.getItem(KEY); } catch (e) {}
if (LANGS.indexOf(lang) < 0) {
var cands = navigator.languages || [navigator.language];
for (var i = 0; i < cands.length; i++) {
var p = (cands[i] || "").slice(0, 2).toLowerCase();
if (LANGS.indexOf(p) >= 0) { lang = p; break; }
}
}
if (LANGS.indexOf(lang) < 0) lang = "en";
var nav = document.getElementById("langs");
function apply(l) {
lang = l;
document.documentElement.lang = l;
try { localStorage.setItem(KEY, l); } catch (e) {}
var blocks = document.querySelectorAll("[data-lang-block]");
for (var i = 0; i < blocks.length; i++) blocks[i].hidden = blocks[i].getAttribute("lang") !== l;
var btns = nav.querySelectorAll("button");
for (var j = 0; j < btns.length; j++) btns[j].className = btns[j].textContent === l ? "on" : "";
}
LANGS.forEach(function (l, i) {
if (i) nav.appendChild(document.createTextNode(" · "));
var b = document.createElement("button");
b.type = "button";
b.textContent = l;
b.addEventListener("click", function () { apply(l); });
nav.appendChild(b);
});
apply(lang);
})();
</script>
</body>
</html>
|