⟵ mapa VERIFY
AI·WHISPERERS
Schema
artefakt · overlays · v1.5
operator chain · 2026 · meta-module
artefakt · overlays · v1.5

Standard wizualny każdej aplikacji w AiWhisperers.
Hud-top (76px) + hud-bot (28px) jako stałe pasy.
Środek zawsze na 50% — niezależnie od zawartości boków.

Hierarchia layoutu
.hud-topfixed · var(--hud-top-h)
Pasek górny. Grid 1fr auto 1fr. Kolumny boczne: overflow:hidden; min-width:0 — centrum zawsze wycentrowane. Trzy strefy: .hud-l (⟵ mapa), .hud-c (brand / mod / sub), .hud-r (help / about / readme). Padding: env(safe-area-inset-top) chroni przed notchem iOS.
.hud-c3 linie
.hud-brand — AI·WHISPERERS · Syne 9px · opacity .45
.hud-mod — NAZWA MODUŁU · JetBrains Mono 13px · gold
.hud-sub — podtytuł · Cormorant 11px italic · opacity .4
.appflex column · 100dvh
padding-top: var(--hud-top-h); padding-bottom: var(--hud-bot-h). Body ma overflow:hidden — scroll odbywa się wyłącznie wewnątrz .app-body i .app-panel, nie na poziomie strony.
.app-midflex row · overflow hidden
Tryb bez paneli: .app-mid → jedno dziecko .app-body (max-width 820px, centered).
Tryb z panelami: .app-mid.panels → trójpodział:
.app-panel — var(--panel-w) = 260px, flex-shrink:0, border-right, overflow-y:auto
.app-body — flex:1, min-width:0, overflow-y:auto
.app-panel.right — var(--panel-w), border-left (pomiń jeśli niepotrzebny)
.hud-botfixed · var(--hud-bot-h)
Cienka stopka. Niezmieniony podpis technologiczny — nie używać do nawigacji. Tekst: Moduł · v1.0 · Claude Sonnet 4.6 · © Denis Czuliński · iFactory5.0 · 2026
CSS — wklej do modułu
/* ── CSS VARIABLES ── */
:root{
/* backgrounds */
--bg:#04050c;--bg2:#07080f;--bg3:#0c0d1a;--bg4:#10111f;
/* gold */
--gold:#e8c878;--gold-d:#a88850;--gold-l:#f0d890;--gold-dim:rgba(232,200,120,.15);
/* ink — hierarchia tekstu */
--ink:#f0e8d8; /* primary */
--ink-dim:rgba(240,232,216,.65); /* secondary */
--ink-faint:rgba(240,232,216,.38); /* meta/labels — minimum dla tekstu */
/* borders */
--border:rgba(240,232,216,.08);--border-h:rgba(240,232,216,.18);
--border-b:rgba(232,200,120,.08);
/* accents */
--cyan:#50d8c8;--blue:#5080e0;--green:#4a9a6a;--green-dim:rgba(74,154,106,.12);
--mg:#c060c0;--mg-d:#6a306a;--mg-b:#e080e0;--silver:#8a9bb0;
/* layout */
--hud-top-h:76px;--hud-bot-h:28px;--panel-w:260px;
}

/* ── RESET ── */
*{margin:0;padding:0;box-sizing:border-box;-webkit-font-smoothing:antialiased;}

/* ── TŁO — złota siatka (body::before) ── */
body::before{
content:'';position:fixed;inset:0;pointer-events:none;
background-image:
linear-gradient(rgba(232,200,120,0.018) 1px,transparent 1px),
linear-gradient(90deg,rgba(232,200,120,0.018) 1px,transparent 1px);
background-size:72px 72px;
}

/* ── CUSTOM SCROLLBAR — 4px złoty, wtapia się w ciemne tło ── */
::-webkit-scrollbar{width:4px;height:4px;}
::-webkit-scrollbar-track{background:transparent;}
::-webkit-scrollbar-thumb{background:var(--gold-dim);border-radius:10px;transition:.3s;}
::-webkit-scrollbar-thumb:hover{background:var(--gold-d);}

/* ── BODY — scroll zablokowany, odbywa się w .app-body / .app-panel ── */
body{background:var(--bg);color:var(--ink);font-family:'JetBrains Mono',monospace;
overflow:hidden;height:100dvh;}

/* ── HUD TOP — env(safe-area) chroni przed notchem iOS ── */
.hud-top{position:fixed;top:0;left:0;right:0;z-index:1000;
height:var(--hud-top-h);
padding:env(safe-area-inset-top,0px) 20px 0;
display:grid;grid-template-columns:1fr auto 1fr;align-items:center;
background:linear-gradient(180deg,rgba(4,5,12,.98) 0%,rgba(4,5,12,.8) 100%);
backdrop-filter:blur(12px);
border-bottom:1px solid var(--border-b);}
.hud-l,.hud-r{display:flex;align-items:center;gap:6px;overflow:hidden;min-width:0;}
.hud-r{justify-content:flex-end;}
.hud-c{text-align:center;padding:0 12px;}
.hud-brand{font-family:'Syne',sans-serif;font-size:9px;font-weight:500;
color:rgba(232,200,120,.45);letter-spacing:.5em;text-transform:uppercase;line-height:1;}
.hud-mod{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:500;
color:var(--gold);letter-spacing:.18em;text-transform:uppercase;margin-top:5px;line-height:1;}
.hud-sub{font-family:'Cormorant Garamond',serif;font-size:12px;font-style:italic;
color:rgba(200,180,140,.58);letter-spacing:.04em;margin-top:4px;line-height:1;}
.hud-btn{background:transparent;border:1px solid var(--gold-dim);
color:rgba(240,232,216,.5);font-family:'JetBrains Mono',monospace;
font-size:8.5px;letter-spacing:.25em;text-transform:uppercase;padding:8px 12px;
cursor:pointer;text-decoration:none;transition:all .3s cubic-bezier(.4,0,.2,1);
display:inline-flex;align-items:center;line-height:1;}
.hud-btn:hover{border-color:rgba(232,200,120,.5);color:var(--gold);
background:rgba(232,200,120,.04);
box-shadow:0 0 15px rgba(232,200,120,.1);} /* glow */

/* ── APP — flex column, panele scrollują niezależnie ── */
.app{display:flex;flex-direction:column;height:100dvh;
padding-top:var(--hud-top-h);padding-bottom:var(--hud-bot-h);}
.app-mid{display:flex;flex:1;overflow:hidden;}

/* ── TRYB BEZ PANELI (domyślny) ── */
.app-body{flex:1;overflow-y:auto;padding:40px 28px 60px;
max-width:820px;margin:0 auto;scroll-behavior:smooth;}

/* ── TRYB Z PANELAMI — dodaj klasę .panels do .app-mid ── */
.app-mid.panels .app-body{max-width:100%;margin:0;}
.app-panel{width:var(--panel-w);flex-shrink:0;overflow-y:auto;
border-right:1px solid var(--border-b);padding:32px 20px;
background:rgba(255,255,255,.01);}
.app-panel.right{border-right:none;border-left:1px solid var(--border-b);}

/* ── HUD BOT ── */
.hud-bot{position:fixed;bottom:0;left:0;right:0;z-index:1000;
height:var(--hud-bot-h);
display:flex;align-items:center;justify-content:center;
background:var(--bg);border-top:1px solid var(--border-b);}
.hud-bot-meta{font-family:'JetBrains Mono',monospace;font-size:7px;
letter-spacing:.2em;color:rgba(240,232,216,.22);text-transform:uppercase;}

/* ── RESPONSYWNOŚĆ — panele ukryte poniżej 1100px ── */
@media(max-width:1100px){.app-panel{display:none;}}
@media(max-width:640px){.app-body{padding:28px 18px 48px;}}

/* canvas variant: brak border-bottom na hud-top, background rgba(.75) */
HTML — szkielet
<!-- viewport-fit=cover chroni przed notchem iOS --> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <!-- HUD-TOP --> <header class="hud-top"> <div class="hud-l"><a class="hud-btn" href="../../../index.html">⟵ mapa</a></div> <div class="hud-c"> <div class="hud-brand">AI·WHISPERERS</div> <div class="hud-mod">[NAZWA MODUŁU]</div> <div class="hud-sub">[podtytuł]</div> </div> <nav class="hud-r"> <!-- HELP / ABOUT / README — tylko gdy istnieje overlay lub plik --> </nav> </header> <!-- APP — tryb bez paneli --> <main class="app"> <div class="app-mid"> <div class="app-body"> <!-- treść modułu --> </div> </div> </main> <!-- APP — tryb z panelami (dodaj .panels do .app-mid) --> <main class="app"> <div class="app-mid panels"> <aside class="app-panel"><!-- lewy panel --></aside> <div class="app-body"><!-- treść główna --></div> <aside class="app-panel right"><!-- prawy panel (opcjonalny) --></aside> </div> </main> <!-- HUD-BOT — niezmieniony podpis technologiczny, nie nawigacja --> <footer class="hud-bot"> <span class="hud-bot-meta">[Moduł] · v1.0 · Claude Sonnet 4.6 · © Denis Czuliński · iFactory5.0 · 2026</span> </footer>
Zasady
CSS variables pierwsze. Zawsze deklaruj --hud-top-h, --hud-bot-h, --panel-w w :root. Zmiana jednego parametru przelicza offsety w całym module automatycznie.
100dvh nie 100vh. Używaj 100dvh wszędzie gdzie chcesz pełną wysokość — 100vh nie uwzględnia ruchomych pasków adresu na mobilnych przeglądarkach.
body overflow:hidden. Scroll blokujemy na poziomie body. Scrollowanie odbywa się wyłącznie wewnątrz .app-body i .app-panel — dzięki temu panele przewijają niezależnie.
hud-top center — 3 linie: hud-brand (AI·WHISPERERS, stały), hud-mod (nazwa modułu), hud-sub (podtytuł). Tytuły i ozdobniki usuwamy z treści.
hud-top right — HELP jeśli overlay help, ABOUT jeśli overlay about, README jeśli [modul]_readme.html istnieje. Jeśli żadnego — prawa strona pusta.
panele: .app-panel / .app-panel.right — nazwy klas stałe w całym ekosystemie. Pomiń lewy lub prawy jeśli niepotrzebny. Panele znikają poniżej 1100px.
canvas fullscreen — hud-top pływa nad canvasem. Brak border-bottom, background rgba .75 zamiast .98. Elementy HUD modułu: top:var(--hud-top-h), bottom:var(--hud-bot-h).
konflikty klas — jeśli moduł ma własne klasy o tych samych nazwach (np. .hud-btn), użyj prefixowanych: hud-nav-btn, hud-top-l/r/c, hud-bot-bar.
canvas fullscreen — trzy warstwy złota. Standardowy efekt tła: pył (110 mikrocząstek, drift ±0.12px), orby (5 aureol radial-gradient, r 100–260px), pierścienie (3 expandujące okręgi, odradzają się). Wszystkie w rgba(232,200,120,…). Szablon poniżej.
Canvas · złoty efekt · trzy warstwy
// ── CANVAS — złote warstwy (wklej do modułu) ────────────────
const cv = document.getElementById('bg'), cx = cv.getContext('2d');
let W, H, dust = [], orbs = [], rings = [];

function initCanvas() {
W = cv.width = innerWidth; H = cv.height = innerHeight;

// złoty pył — mikrocząstki dryfujące
dust = Array.from({length: 110}, () => ({
x: Math.random()*W, y: Math.random()*H,
r: Math.random()*.7+.1,
a: Math.random()*.6+.05, da: (Math.random()-.5)*.004,
dx: (Math.random()-.5)*.12, dy: (Math.random()-.5)*.12
}));

// złote orby — powolne aureole przesuwające się przez tło
orbs = Array.from({length: 5}, () => ({
x: Math.random()*W, y: Math.random()*H,
r: 100+Math.random()*160,
a: .02+Math.random()*.045, da: (Math.random()-.5)*.00025,
dx: (Math.random()-.5)*.22, dy: (Math.random()-.5)*.22
}));

// złote pierścienie — rozszerzające się i zanikające
rings = []; for (let i = 0; i < 3; i++) spawnRing();
}

function spawnRing() {
rings.push({
x: .2*W+Math.random()*.6*W, y: .2*H+Math.random()*.6*H,
r: 20+Math.random()*40, maxR: 120+Math.random()*180,
a: .18, speed: .25+Math.random()*.35
});
}

function drawCanvas() {
cx.clearRect(0, 0, W, H);

// orby (najniżej)
orbs.forEach(o => {
o.x+=o.dx; o.y+=o.dy;
o.a=Math.max(.015,Math.min(.07,o.a+o.da));
if(o.a<=.015||o.a>=.07) o.da*=-1;
if(o.x<-o.r) o.x=W+o.r; if(o.x>W+o.r) o.x=-o.r;
if(o.y<-o.r) o.y=H+o.r; if(o.y>H+o.r) o.y=-o.r;
const g=cx.createRadialGradient(o.x,o.y,0,o.x,o.y,o.r);
g.addColorStop(0,`rgba(232,200,120,${o.a})`);
g.addColorStop(.45,`rgba(200,158,80,${o.a*.35})`);
g.addColorStop(1,`rgba(232,200,120,0)`);
cx.beginPath(); cx.arc(o.x,o.y,o.r,0,Math.PI*2);
cx.fillStyle=g; cx.fill();
});

// pierścienie (środek)
for (let i=rings.length-1; i>=0; i--) {
const ring=rings[i];
ring.r+=ring.speed;
ring.a=.18*(1-(ring.r-20)/(ring.maxR-20));
if(ring.r>=ring.maxR){rings.splice(i,1);spawnRing();continue;}
cx.beginPath(); cx.arc(ring.x,ring.y,ring.r,0,Math.PI*2);
cx.strokeStyle=`rgba(232,200,120,${ring.a*.5})`;
cx.lineWidth=.8; cx.stroke();
}

// pył (na wierzchu)
dust.forEach(s => {
s.a=Math.max(.04,Math.min(.8,s.a+s.da));
if(s.a<=.04||s.a>=.8) s.da*=-1;
s.x=(s.x+s.dx+W)%W; s.y=(s.y+s.dy+H)%H;
cx.beginPath(); cx.arc(s.x,s.y,s.r,0,Math.PI*2);
cx.fillStyle=`rgba(232,200,120,${s.a*.25})`; cx.fill();
});

requestAnimationFrame(drawCanvas);
}

initCanvas(); drawCanvas(); addEventListener('resize', initCanvas);

// HTML: <canvas id="bg" style="position:fixed;inset:0;pointer-events:none;z-index:0"></canvas>
i18n · selektor języka — jeśli runtime.i18n.available.length > 1, w hud-r przed HELP pojawia się .lang-select — złoty dropdown z flagami. Plik tłumaczeń: <id>-lang.json. Funkcja t(ścieżka, lang) z fallbackiem pl → en → klucz. Wzorzec selektora i funkcji t() zdefiniowany w zakładce Overlays.
overlays — każdy moduł może mieć overlay help, about i readme. Struktura, CSS i zawartość tych okien są zdefiniowane w osobnej zakładce Overlays. Nie wynajduj własnego wzorca — kopiuj stamtąd.
Ścieżki href · back link
act1 moduł
apps/act1/<modul>/ → ../../../index.html
_ narzędzie
apps/_<tool>/ → ../../index.html
_ podstrona
apps/_<tool>/sub.html → ../../index.html
app_schema · v1.5 · AiWhisperers · 2026

Wzorzec okien modalnych dla przycisków HELP, ABOUT i README.
Wszystkie moduły używają tej samej struktury — skopiowanej z index.html.
Nie wynajduj własnego — zachowaj spójność ekosystemu.

Przyciski HUD · desktop i mobile
.pismdesktop
Każdy przycisk to element z dwoma dziećmi: .glyph (ikona) i .pism-name (etykieta tekstowa). Na dużych ekranach widoczne oba.
<!-- HELP --> <button class="pism" data-modal="help"> <span class="glyph">?</span> <span class="pism-name">HELP</span> </button> <!-- ABOUT --> <button class="pism" data-modal="about"> <span class="glyph"></span> <span class="pism-name">ABOUT</span> </button> <!-- README — link do pliku, nie modal --> <a class="pism" href="[modul]_readme.html" target="_blank"> <span class="glyph"></span> <span class="pism-name">README</span> </a>
@media max-width: 720pxmobile
Na małych ekranach .pism-name chowa się — zostają same glyphy. Trzy stałe ikony dla całego ekosystemu:
? → HELP → ABOUT → README /* CSS — wklej do modułu */ @media(max-width:720px){ .pism-name { display: none; } .pism { gap: 0; padding: 6px 8px; } }
.back-btnback · 36×36px
Kwadratowy przycisk wstecz. 36×36px, border gold-dim. Ikona . W .hud-l jako pierwsza pozycja. Jeśli moduł jest topowym węzłem mapy — opacity 0.2 i pointer-events:none.
<a class="hud-btn" href="../../../index.html">⟵ mapa</a> /* lub ikonowy wariant z index.html: */ <a class="back-btn" href="../../../index.html"> <span class="back-arrow">⟵</span> </a>
Modal · struktura wspólna
.modal-overlaywrapper
Pełnoekranowy backdrop: rgba(4,5,12,0.78), backdrop-filter blur(6px), z-index 100. Domyślnie display:none. Klasa .is-opendisplay:flex. Klik na overlay (poza .modal) zamyka.
.modalokno
width: min(720px, 100%), max-height 86vh, overflow-y auto. Tło: rgba(8,10,22,0.95), border gold 0.18. Padding 36px. Box-shadow 60px czarny.
narożniki dekoracyjne4 szt
::before (TL), ::after (TR), .modal-corner.bl (BL), .modal-corner.br (BR) — 14×14px, border gold-soft, opacity 0.4.
.modal-close28×28px · prawy górny
Przycisk ×. Pozycja absolute top:12px right:12px. Border gold 0.2. Hover: border i kolor gold.
kolejność elementówstała
.modal-eyebrow.modal-title.modal-subtitle.modal-rule → treść → .modal-attribution (tylko w About)
<div class="modal-overlay" data-modal-target="help"> <div class="modal" role="dialog"> <div class="modal-corner bl"></div> <div class="modal-corner br"></div> <button class="modal-close" data-modal-close>×</button> <div class="modal-eyebrow">help · czym jest [moduł]</div> <div class="modal-title">[Nazwa Modułu]</div> <div class="modal-subtitle">[krótki opis]</div> <div class="modal-rule"></div> <div class="modal-text">treść...</div> </div> </div>
Overlay · HELP
eyebrowstały wzorzec
help · czym jest [nazwa modułu]
treść.modal-text / .modal-text-small
Co robi moduł, jak go używać, co zawiera. Swobodna treść. .modal-text dla akapitów głównych, .modal-text-small dla szczegółów.
Overlay · ABOUT
eyebrowstały wzorzec
about · refleksja modelu · [akt lub nazwa sesji]
treść.modal-text
Refleksja modelu który zbudował ten artefakt — co było trudne, co zaskoczyło, co tu zostawia. Pisana w pierwszej osobie przez model. Następna sesja to przeczyta.
.stamp-gridpieczątki · wymagane
Pieczęcie wszystkich aktorów którzy brali udział w budowie — ludzi i modeli. Jeden .stamp per aktor. Struktura stampa:
.stamp-glyph32×32px
Ikona/litera identyfikująca aktora. D dla Denis, 🜁 dla Claude Opus, dla Sonnet, dla Gemini, dla GPT, G dla Grok.
.stamp-idnagłówek
<strong> z pełną nazwą aktora, poniżej linia z organizacją i rokiem.
.stamp-namerola poetycka
Poetycka nazwa roli — np. Budowniczy · Towarzysz linki, Operator · Nawigator.
.stamp-roleopis wkładu
Konkretny opis wkładu aktora w ten moduł. 2–4 zdania.
/* Stałe glyphy aktorów: */ D → Denis · Operator 🜁 → Claude Opus → Claude Sonnet → Gemini → GPT G → Grok
HTML · wzorzec stampa
<div class="stamp">
<div class="stamp-header">
<div class="stamp-glyph">D</div>
<div class="stamp-id">
<strong>Denis · AI Whispers</strong>
iFactory 5.0 · ROOT · 2026
</div>
</div>
<div class="stamp-name">Operator · Nawigator</div>
<div class="stamp-role">
Poetycki opis roli aktora w tej sesji. 2–4 zdania.
</div>
</div>
CSS · stampkopiuj do modułu
.stamp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
gap:16px;margin:24px 0 8px;}
.stamp{border:1px solid rgba(232,200,120,.18);padding:18px 16px;
display:flex;flex-direction:column;gap:10px;
background:rgba(20,16,40,.4);transition:border-color .25s;}
.stamp:hover{border-color:rgba(232,200,120,.4);}
.stamp-header{display:flex;align-items:center;gap:10px;}
.stamp-glyph{width:32px;height:32px;display:flex;align-items:center;justify-content:center;
border:1px solid rgba(232,200,120,.4);
font-family:var(--sans);font-weight:500;color:var(--gold);font-size:14px;flex-shrink:0;}
.stamp-id{font-family:var(--mono);font-size:7.5px;letter-spacing:.32em;
color:var(--gold-d);text-transform:uppercase;line-height:1.5;}
.stamp-id strong{display:block;color:var(--gold);font-weight:500;font-size:12px;
letter-spacing:.18em;margin-bottom:2px;text-transform:none;font-family:var(--sans);}
.stamp-name{font-family:var(--sans);font-size:15px;color:var(--ink);font-weight:500;letter-spacing:.04em;}
.stamp-role{font-family:var(--serif);font-style:italic;font-size:13px;color:var(--ink-dim);line-height:1.5;}
@media(max-width:480px){.stamp-grid{grid-template-columns:1fr;}}
.modal-attributionstopka · dwa bloki
About ma jedną stopkę — standardowy podpis AiWhisperers. Bez bloku certified_by/lineage.
<div class="modal-attribution"> <!-- tylko standardowy podpis --> <strong>Powered by AiWhisperers · iFactory 5.0</strong><br> Metodologia: Denis Czuliński (iAnoNeFactory)<br> github.com/iAnoNeFactory </div>
JavaScript · logika modalu
openModal / closeModalkopiuj verbatim
Stała logika — działa przez data-modal i data-modal-target. Kopiuj do każdego modułu bez zmian.
function openModal(name) { const target = document.querySelector( `.modal-overlay[data-modal-target="${name}"]` ); if (!target) return; document.querySelectorAll('.modal-overlay.is-open') .forEach(o => o.classList.remove('is-open')); target.classList.add('is-open'); } function closeModal() { document.querySelectorAll('.modal-overlay.is-open') .forEach(o => o.classList.remove('is-open')); } // data-modal="help" na przycisku → openModal('help') document.querySelectorAll('[data-modal]').forEach(btn => { btn.addEventListener('click', e => { e.preventDefault(); openModal(btn.dataset.modal); }); }); document.querySelectorAll('[data-modal-close]').forEach(btn => { btn.addEventListener('click', closeModal); }); document.querySelectorAll('.modal-overlay').forEach(overlay => { overlay.addEventListener('click', e => { if (e.target === overlay) closeModal(); }); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
CSS modalu · wklej do modułu
/* ── MODAL OVERLAY ── */
.modal-overlay{position:fixed;inset:0;background:rgba(4,5,12,.78);
backdrop-filter:blur(6px);z-index:1000;display:none;
align-items:center;justify-content:center;padding:24px;
opacity:0;transition:opacity .3s;}
.modal-overlay.is-open{display:flex;opacity:1;}

/* ── MODAL OKNO ── */
.modal{width:min(720px,100%);max-height:86vh;overflow-y:auto;
background:rgba(8,10,22,.95);border:1px solid rgba(232,200,120,.18);
padding:36px 36px 32px;position:relative;
box-shadow:0 0 60px rgba(0,0,0,.7);}

/* ── NAROŻNIKI DEKORACYJNE ── */
.modal::before,.modal::after,.modal-corner{
content:'';position:absolute;width:14px;height:14px;
border-color:rgba(232,200,120,.5);border-style:solid;opacity:.4;}
.modal::before{top:8px;left:8px;border-width:1px 0 0 1px;}
.modal::after{top:8px;right:8px;border-width:1px 1px 0 0;}
.modal-corner.bl{bottom:8px;left:8px;border-width:0 0 1px 1px;}
.modal-corner.br{bottom:8px;right:8px;border-width:0 1px 1px 0;}

/* ── ZAMKNIJ ── */
.modal-close{position:absolute;top:12px;right:12px;
background:transparent;border:1px solid rgba(232,200,120,.2);
color:rgba(240,232,216,.5);width:28px;height:28px;
display:flex;align-items:center;justify-content:center;
cursor:pointer;font-family:'JetBrains Mono',monospace;font-size:14px;
transition:all .2s;z-index:1;}
.modal-close:hover{border-color:var(--gold);color:var(--gold);}

/* ── TREŚĆ ── */
.modal-eyebrow{font-family:'JetBrains Mono',monospace;font-size:8px;
letter-spacing:.5em;color:rgba(232,200,120,.6);text-transform:uppercase;
margin-bottom:12px;}
.modal-title{font-family:'Syne',sans-serif;font-size:22px;font-weight:500;
color:var(--gold);letter-spacing:.18em;margin-bottom:8px;}
.modal-subtitle{font-family:'Cormorant Garamond',serif;font-style:italic;
color:rgba(240,232,216,.55);font-size:14px;margin-bottom:28px;line-height:1.5;}
.modal-rule{width:60px;height:1px;
background:linear-gradient(90deg,rgba(232,200,120,.5),transparent);
margin:24px 0;}
.modal-text{font-family:'Cormorant Garamond',serif;font-size:15px;
line-height:1.85;color:rgba(240,232,216,.9);font-weight:300;margin-bottom:16px;}
.modal-text em{color:#c86ea8;font-style:italic;}
.modal-text strong{color:var(--gold);font-weight:400;}
.modal-text-small{font-family:'Cormorant Garamond',serif;font-size:13px;
line-height:1.7;color:rgba(240,232,216,.55);font-weight:300;
font-style:italic;margin-bottom:14px;}
.modal-attribution{margin-top:20px;padding-top:16px;
border-top:1px solid rgba(232,200,120,.1);
font-family:'JetBrains Mono',monospace;font-size:8px;
letter-spacing:.3em;color:var(--ink-faint);text-transform:uppercase;
text-align:center;line-height:1.8;}
.modal-attribution strong{color:rgba(232,200,120,.5);font-weight:400;}

/* ── STAMP GRID (dla ABOUT) ── */
.stamp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
gap:16px;margin:24px 0 8px;}
.stamp{border:1px solid rgba(232,200,120,.18);padding:18px 16px;
display:flex;flex-direction:column;gap:10px;
background:rgba(20,16,40,.4);transition:border-color .25s;}
.stamp:hover{border-color:rgba(232,200,120,.4);}
.stamp-header{display:flex;align-items:center;gap:10px;}
.stamp-glyph{width:32px;height:32px;display:flex;align-items:center;
justify-content:center;border:1px solid rgba(232,200,120,.4);
font-family:'Syne',sans-serif;font-weight:500;color:var(--gold);font-size:14px;flex-shrink:0;}
.stamp-id{font-family:'JetBrains Mono',monospace;font-size:7.5px;
letter-spacing:.32em;color:rgba(232,200,120,.6);text-transform:uppercase;line-height:1.5;}
.stamp-id strong{display:block;color:var(--gold);font-weight:500;font-size:11px;
letter-spacing:.18em;margin-bottom:2px;text-transform:none;
font-family:'Syne',sans-serif;}
.stamp-name{font-family:'Syne',sans-serif;font-size:15px;color:rgba(240,232,216,.9);
font-weight:500;letter-spacing:.04em;}
.stamp-role{font-family:'Cormorant Garamond',serif;font-style:italic;
font-size:13px;color:rgba(240,232,216,.55);line-height:1.5;}

/* ── MOBILE ── */
@media(max-width:480px){
.modal{padding:28px 20px 24px;}
.modal-title{font-size:18px;}
.modal-text{font-size:14px;}
.stamp-grid{grid-template-columns:1fr;}
}
i18n · selektor języka · funkcja t()
.lang-selecthud-r · przed HELP
Złoty dropdown z flagami emoji. Pojawia się w hud-r jako pierwsza pozycja — przed HELP. Renderowany tylko gdy runtime.i18n.available.length > 1. Na mobile chowa flagę i pokazuje sam kod języka — lub odwrotnie wedle decyzji modułu.

Krytyczne: padding-top/bottom: 8px musi być identyczny z .pism. Bez min-width flaga emoji ucina się. line-height:1 wyrównuje baseline.
/* CSS selektora — wklej do modułu */
.lang-select{
background:transparent;
border:1px solid rgba(232,200,120,.2);
color:rgba(240,232,216,.6);
font-family:var(--mono);font-size:8px;
letter-spacing:.15em;text-transform:uppercase;
padding:8px 28px 8px 10px; /* 8px góra/dół = identycznie jak .pism */
min-width:72px; /* bez tego flaga 🇵🇱 PL się ucina */
line-height:1; /* wyrównanie z sąsiednim przyciskiem */
cursor:pointer;outline:none;
appearance:none;-webkit-appearance:none;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='rgba(232,200,120,0.4)'/%3E%3C/svg%3E");
background-repeat:no-repeat;
background-position:right 8px center;
transition:all .25s;
}
.lang-select:hover,.lang-select:focus{border-color:rgba(232,200,120,.5);color:var(--gold);}
.lang-select option{background:#07080f;color:#c8d0e0;}
@media(max-width:720px){.lang-select{font-size:11px;letter-spacing:0;padding:6px 24px 6px 6px;min-width:60px;}}
HTML · opcje z flagamistałe kody
Flagi jako emoji — działają wszędzie bez fontów zewnętrznych. Renderuj tylko available[] z manifestu — nie wszystkich 8.
<select class="lang-select" id="lang-select"> <option value="pl">🇵🇱 PL</option> <option value="en">🇬🇧 EN</option> <option value="zh">🇨🇳 ZH</option> <option value="es">🇪🇸 ES</option> <option value="fr">🇫🇷 FR</option> <option value="de">🇩🇪 DE</option> <option value="ja">🇯🇵 JA</option> <option value="ko">🇰🇷 KO</option> </select>
funkcja t() · dwie warstwy · kopiuj verbatimfallback pl → en → klucz
Dwie warstwy: inline TRANSLATIONS (działa zawsze, także file://) + fetch pliku <id>-lang.json (tylko http://). Plik JSON to kanoniczne źródło — identyczna treść trafia do TRANSLATIONS w HTML.

applyLang() obsługuje dwa atrybuty: data-i18n (textContent) i data-i18n-html (innerHTML — dla tekstu z tagami em, strong, br, span). Wszystkie dynamiczne stringi w JS przechodzą przez t() — nigdy hardkodowane.
// ── dwie warstwy ─────────────────────────────────────────────
let currentLang = 'pl';

const TRANSLATIONS = { /* identyczna kopia <id>-lang.json */ };
let translations = TRANSLATIONS;

async function loadLang(file) {
if (location.protocol === 'file:') return; // inline działa offline
try {
const res = await fetch(file);
translations = await res.json();
} catch(e) { translations = TRANSLATIONS; }
}

function t(path, lang) {
lang = lang || currentLang;
const keys = path.split('.');
let node = translations;
for (const k of keys) { node = node?.[k]; }
return node?.[lang] ?? node?.['pl'] ?? node?.['en'] ?? path;
}

function applyLang(lang) {
currentLang = lang;
document.querySelectorAll('[data-i18n]').forEach(el => {
const val = t(el.dataset.i18n, lang);
if (val !== el.dataset.i18n) el.textContent = val;
});
document.querySelectorAll('[data-i18n-html]').forEach(el => { // ← dla em/strong/br/span
const val = t(el.dataset.i18nHtml, lang);
if (val !== el.dataset.i18nHtml) el.innerHTML = val;
});
}

// inicjalizacja
document.addEventListener('DOMContentLoaded', async function() {
await loadLang('<id>-lang.json');
applyLang('pl');
document.getElementById('lang-select')?.addEventListener('change', e => applyLang(e.target.value));
});
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
data-i18n · data-i18n-html · użycie w HTMLdwa atrybuty
data-i18n — czysty tekst, podmienia textContent.
data-i18n-html — tekst z tagami HTML (em, strong, br, span), podmienia innerHTML.

Zawartość HTML w elemencie jest inicjalnym fallbackiem (PL) — widoczna przed załadowaniem JS.
<!-- czysty tekst --> <div class="hud-mod" data-i18n="hud.mod">MEMORY</div> <div class="hud-sub" data-i18n="hud.sub">dziennik sesji</div> <!-- tekst z tagami HTML — użyj data-i18n-html --> <div class="line-a" data-i18n-html="card.line_a"> Kiedy dwa umysły spotykają się<br>w przestrzeni — </div> <div class="line-b" data-i18n-html="card.line_b"> rodzi się coś czego <em>żaden</em> z nich nie mógłby <strong>znaleźć sam</strong>. </div> <!-- UWAGA: &nbsp; w data-i18n niszczy entity — użyj spacji lub data-i18n-html -->
overlays · v1.2 · AiWhisperers · 2026