PageSourceSearch

https://morfoz.ai/public/oyun-savas.js?v=1790769371

js morfoz.ai collected 2026-10-05 15:08:52 UTC 74,471 bytes, 1,254 lines download raw bytes

1/**
2 * Klon Savaşları — panel (web) yüzü: liste, ayrıntı ve "Yeni savaş" modalı
3 * (mobil KlonSavasiView.swift'in panel karşılığı; OYUN-MOTORU.md §10).
4 *
5 * İki klon bir konuda yarışır; turları sunucu kuyrukta üretir (LLM), hakem
6 * puanlar, izleyici oy verir, kapanışta sahiplere sezon puanı yazılır. Bu
7 * dosya yalnız GÖSTERİR ve üç eylemi (kabul / reddet / oy) sunucuya iletir;
8 * hiçbir kural burada hesaplanmaz — hakem ağırlığı, oy kapıları, puan
9 * dağılımı sunucudadır (services/oyun/klon_savasi.py).
10 *
11 * NEDEN AYRI DOSYA: lobi (oyun-view.js) bu modülü DİNAMİK ithal eder; buradaki
12 * bir parse hatası lobiyi — dolayısıyla paneli — düşürmesin (panel-bos-ekran
13 * tuzağı). Aynı sebeple oyun-puan.js ile stil paylaşılmaz: iki modülün
14 * yaşam döngüsü bağımsızdır, ortak yardımcı yalnız oyun-api.js'tedir.
15 *
16 * Sunucu sözleşmesi (routes/oyun_routes.py; alanlar klon_savasi.gorunum):
17 *   GET  /savas/liste?durum=        {battles[gorunum], open_votes[gorunum]}
18 *        durum ∈ pending|running|voting|judged|cancelled|expired (tek değer)
19 *   GET  /savas/{id}                gorunum
20 *   POST /savas/{id}/kabul          gorunum (yalnız B'nin sahibi)
21 *   POST /savas/{id}/reddet         {ok}    (iki sahip de; yalnız pending)
22 *   POST /savas/{id}/oy {side}      gorunum (sahip değil, status=voting, tek oy)
23 *   GET  /savas/formatlar?assistant_id=&rakip_assistant_id=  {formats[{key,ad,aciklama,acik,neden}]}
24 *   POST /savas {format, topic?, assistant_a?, assistant_b?|rakip_user_id?, is_public}  gorunum
25 *   GET  /tercih                    {assistant_id,...}  (varsayılan klonum)
26 *   GET  /api/assistants?scope=mine|directory  klon seçenekleri (avatar_url çözümlü)
27 *
28 * gorunum: {id, format, format_ad, status, topic, lang, rounds, current_round,
29 *   a|b: {assistant{id,name,avatar_url}, owner{id,name,handle,avatar_tone,avatar_url}},
30 *   transcript[{round, side, q?, text}], judge{a,b,reason,tutarsiz,hakem_yok?}|null (yalnız judged),
31 *   votes{a,b}|null (yalnız judged), winner_side a|b|draw|null, points_awarded 0|1 (BAYRAK, miktar
32 *   değil), my_vote, can_vote, is_mine, is_public, public_key, voting_ends_at, accept_deadline_at}
33 *
34 * Canlı hat: notification-center.js panel WS çerçevelerini window 'morfoz:game'
35 * olayı olarak yayar; burada game.battle / game.battle_round / game.battle_close
36 * dinlenir (sunucu _yay iki sahibe game.battle basar). Çerçevede gövde olsa da
37 * REST ile tazelenir: gövde sahibe göre üretilir, izleyici için can_vote gibi
38 * alanlar yalnız kendi isteğinde doğrudur.
39 *
40 * KURALLAR: tarayıcı kutusu yok (confirm/prompt panelde susturulmuş → sayfa içi
41 * onay çipi); e-posta hiçbir yüzeyde görünmez (kullaniciAdi/kullaniciHandle);
42 * i18n anahtarları hep sabit literal; sunucu metni escapeHtml'den geçer;
43 * arayüz ikonları inline SVG (emoji CSS rengini dinlemez).
44 */
45import { T, oyunApi, hataMetni, sayi } from './oyun-api.js';
46import { api, escapeHtml, toast, kullaniciAdi, kullaniciHandle } from './api-client.js';
47import { currentLang, onLanguageChange } from './i18n.js';
48import { openUserSearchModal } from './user-picker.js';
49
50// ---------------------------------------------------------------------- ortak
51
52/** Tek aktif ekran: dil değişince yeniden çizilir; yeni ekran açılınca öncekinin
53 *  dinleyicisi kapatılır (oyun-puan.js ile aynı desen, ayrı kayıt). */
54let _aktif = null;
55let _dilBagli = false;
56
57function _ekranAc(host, ciz, kapat) {
58  if (_aktif && _aktif.kapat) { try { _aktif.kapat(); } catch { /* sessiz */ } }
59  _aktif = { host, ciz, kapat };
60  if (!_dilBagli) {
61    _dilBagli = true;
62    onLanguageChange(() => { if (_aktif && _aktif.host.isConnected) _aktif.ciz(); });
63  }
64}
65
66function yerelAd() { return currentLang() === 'en' ? 'en-US' : 'tr-TR'; }
67
68/** Sunucu 'YYYY-MM-DD HH:MM:SS' (UTC) ya da ISO verir; ekran yerel saat basar. */
69function tarihCoz(s) {
70  if (!s) return null;
71  const m = String(s).trim();
72  const d = new Date(/[TZ]/.test(m) ? m : m.replace(' ', 'T') + 'Z');
73  return isNaN(d.getTime()) ? null : d;
74}
75function tarih(s) {
76  const d = tarihCoz(s);
77  if (!d) return '—';
78  const o = { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' };
79  if (d.getFullYear() !== new Date().getFullYear()) o.year = 'numeric';
80  try { return d.toLocaleString(yerelAd(), o); } catch { return String(s); }
81}
82
83/** Panelin oturum sahibi (panel-app.js açılışta window.morfozMe yazar). Sahibi
84 *  olduğum tarafı (A mı B mi) bununla ayırt ederiz; gorunum.is_mine tarafı söylemez. */
85function benimId() {
86  const v = window.morfozMe && window.morfozMe.id;
87  return v == null ? null : Number(v);
88}
89
90/** Sunucunun kişi kartı {id, name, handle, avatar_tone, avatar_url}; kullaniciAdi
91 *  full_name bekliyor — name oraya taşınır ki e-posta kuralı tek yerde kalsın. */
92function kisi(u) {
93  if (!u) return null;
94  return { ...u, full_name: u.full_name || u.display_name || u.name || '' };
95}
96function kisiAdi(u) { return u ? kullaniciAdi(kisi(u)) : '—'; }
97
98/** Arayüz ikonları inline SVG. */
99const IKON = {
100  geri: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>',
101  yenile: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>',
102  simsek: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></svg>',
103  simsekBuyuk: '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></svg>',
104  arti: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>',
105  kapat: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>',
106  onay: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5L20 7"/></svg>',
107  paylas: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8h16v-8M12 3v13M7 8l5-5 5 5"/></svg>',
108  kupa: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 21h8M12 17v4M7 4h10v5a5 5 0 0 1-10 0z"/><path d="M7 6H4v2a3 3 0 0 0 3 3M17 6h3v2a3 3 0 0 1-3 3"/></svg>',
109  kisi: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>',
110  arama: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.35-4.35"/></svg>',
111  balon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 21l1.9-5.4A8 8 0 1 1 21 12z"/></svg>',
112  ok: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></svg>',
113};
114
115/** Klon görseli: sunucu avatar_url çözer (call_avatar_ref → URL); yoksa baş harf. */
116function klonAvatarHtml(a, boy) {
117  const ad = (a && a.name) || '';
118  const bas = escapeHtml(String(ad).trim().slice(0, 1).toUpperCase() || '?');
119  const st = `width:${boy}px;height:${boy}px;font-size:${Math.round(boy * 0.42)}px`;
120  if (a && a.avatar_url) {
121    return `<span class="os-avatar" style="${st}"><img src="${escapeHtml(a.avatar_url)}" alt="" loading="lazy"></span>`;
122  }
123  return `<span class="os-avatar" style="${st}">${bas}</span>`;
124}
125
126function basHtml(baslik, alt, ekHtml) {
127  return `<div class="os-head">
128    <button type="button" class="os-geri" data-geri aria-label="${escapeHtml(T('action.back', 'Ger
128i'))}">${IKON.geri}</button>
129    <div class="os-head-metin">
130      <h2 class="os-title">${escapeHtml(baslik)}</h2>
131      ${alt ? `<p class="os-sub">${escapeHtml(alt)}</p>` : ''}
132    </div>
133    ${ekHtml || ''}
134  </div>`;
135}
136
137function bosHtml(baslik, aciklama) {
138  return `<div class="os-bos">${IKON.simsekBuyuk}<div class="os-bos-baslik">${escapeHtml(baslik)}</div>` +
139         (aciklama ? `<div class="os-bos-alt">${escapeHtml(aciklama)}</div>` : '') + '</div>';
140}
141
142function yukleniyorHtml() {
143  return `<div class="os-yukleniyor"><span class="os-spinner"></span>${escapeHtml(T('common.loading', 'Yükleniyor…'))}</div>`;
144}
145
146function hataHtml(metin) {
147  return `<div class="os-hata"><div>${escapeHtml(metin)}</div>
148    <button type="button" class="os-btn os-btn-ghost os-btn-sm" data-yeniden>${IKON.yenile} ${escapeHtml(T('ui.oyun.savas.yeniden', 'Yeniden dene'))}</button></div>`;
149}
150
151/** Uzun süren eylem: düğme kilitlenir ve dönen halka çıkar (metin yerinde kalır). */
152function kilitle(btn, kilitli) {
153  if (!btn) return;
154  btn.disabled = !!kilitli;
155  btn.classList.toggle('os-busy', !!kilitli);
156}
157
158/** Sayfa içi onay çipi — tarayıcının onay kutusu panelde susturulmuş, akış
159 *  sessizce ölürdü. Promise<boolean>; aynı kapta ikinci çip öncekini düşürür. */
160function onayIste(kap, metin, { tehlike = false } = {}) {
161  return new Promise((resolve) => {
162    const eski = kap.querySelector(':scope > .os-onay');
163    if (eski) eski.remove();
164    const cip = document.createElement('div');
165    cip.className = 'os-onay';
166    cip.innerHTML = `<span class="os-onay-metin">${escapeHtml(metin)}</span>
167      <button type="button" class="os-btn os-btn-sm ${tehlike ? 'os-btn-danger' : 'os-btn-primary'}" data-evet>${escapeHtml(T('action.yes', 'Evet'))}</button>
168      <button type="button" class="os-btn os-btn-sm os-btn-ghost" data-hayir>${escapeHtml(T('action.cancel', 'Vazgeç'))}</button>`;
169    kap.appendChild(cip);
170    // Çip tıklanabilir bir satırın İÇİNDE olabilir (liste): Evet/Vazgeç satırı açmasın.
171    cip.addEventListener('click', (e) => e.stopPropagation());
172    cip.addEventListener('keydown', (e) => e.stopPropagation());
173    const bitir = (v) => { cip.remove(); resolve(v); };
174    cip.querySelector('[data-evet]').addEventListener('click', () => bitir(true));
175    cip.querySelector('[data-hayir]').addEventListener('click', () => bitir(false));
176    setTimeout(() => cip.querySelector('[data-evet]').focus(), 20);
177  });
178}
179
180function geriBagla(kok, geri) {
181  const b = kok.querySelector('[data-geri]');
182  if (b) b.addEventListener('click', () => { if (typeof geri === 'function') geri(); });
183}
184
185// Sabit eşlemeler — anahtar dizesi HER ZAMAN literal (dinamik anahtar yasak).
186// Durum adları clone_battles.status (klon_savasi.py): pending|running|voting|judged|cancelled|expired.
187function savasDurumu(v) {
188  switch (v || '') {
189    case 'pending':   return T('ui.oyun.savas.durum_pending', 'Davet bekliyor');
190    case 'running':   return T('ui.oyun.savas.durum_running', 'Sürüyor');
191    case 'voting':    return T('ui.oyun.savas.durum_voting', 'Oylamada');
192    case 'judged':    return T('ui.oyun.savas.durum_judged', 'Bitti');
193    case 'cancelled': return T('ui.oyun.savas.durum_cancelled', 'İptal');
194    case 'expired':   return T('ui.oyun.savas.durum_expired', 'Süresi doldu');
195    default:          return v || '';
196  }
197}
198function durumSinifi(v) {
199  switch (v || '') {
200    case 'running': return 'os-rozet-bilgi';
201    case 'voting':  return 'os-rozet-oy';
202    case 'judged':  return 'os-rozet-ok';
203    case 'cancelled': case 'expired': return 'os-rozet-kotu';
204    default: return 'os-rozet-bekle';
205  }
206}
207
208/** Format adı BAKANIN dilinde: sunucunun format_ad alanı savaşın diline göre
209 *  üretilir (İngilizce panelde Türkçe savaş "Tartışma" derdi). */
210function formatAdi(f, yedek) {
211  switch (f || '') {
212    case 'debate':    return T('ui.oyun.savas.format_debate', 'Tartışma');
213    case 'knowledge': return T('ui.oyun.savas.format_knowledge', 'Bilgi');
214    case 'sales':     return T('ui.oyun.savas.format_sales', 'Satış');
215    default:          return yedek || f || '';
216  }
217}
218
219/** Kapalı format sebebi (klon_savasi.formatlar → neden). */
220function nedenMetni(v) {
221  switch (v || '') {
222    case 'egitim_yok': return T('ui.oyun.savas.neden_egitim_yok', 'İki klonun da yeterli herkese açık eğitimi olmalı.');
223    case 'kapali':     return T('ui.oyun.savas.neden_kapali', 'Şu an kapalı.');
224    default:           return T('ui.oyun.savas.neden_kapali', 'Şu an kapalı.');
225  }
226}
227
228/** Bakanın tarafı: 'a' | 'b' | '' (sahip değil). */
229function benimTaraf(s) {
230  const me = benimId();
231  if (!s || me == null) return '';
232  if (s.a && s.a.owner && Number(s.a.owner.id) === me) return 'a';
233  if (s.b && s.b.owner && Number(s.b.owner.id) === me) return 'b';
234  return '';
235}
236
237function klonAdi(s, taraf) {
238  const t = s && s[taraf];
239  return (t && t.assistant && t.assistant.name) || taraf.toUpperCase();
240}
241
242/** Bitmiş savaşta sahibe göre sonuç çipi. */
243function sonucCipiHtml(s) {
244  if (!s || s.status !== 'judged') return '';
245  const taraf = benimTaraf(s);
246  if (!taraf) return '';
247  const w = s.winner_side || 'draw';
248  if (w === 'draw') return `<span class="os-rozet os-rozet-bekle">${escapeHtml(T('ui.oyun.savas.berabere', 'Berabere'))}</span>`;
249  if (w === taraf) return `<span class="os-rozet os-rozet-ok">${escapeHtml(T('ui.oyun.savas.kazandin', 'Kazandın'))}</span>`;
250  return `<span class="os-rozet os-rozet-kotu">${escapeHtml(T('ui.oyun.savas.kaybettin', 'Kaybettin'))}</span>`;
251}
252
253function rozetlerHtml(s) {
254  const p = [
255    `<span class="os-rozet os-rozet-soft">${escapeHtml(formatAdi(s.format, s.format_ad))}</span>`,
256    `<span class="os-rozet ${durumSinifi(s.status)}">${escapeHtml(savasDurumu(s.status))}</span>`,
257  ];
258  if (s.status === 'running' && s.rounds) {
259    p.push(`<span class="os-rozet os-rozet-bilgi">${escapeHtml(T('ui.oyun.savas.tur_n_m', 'Tur {n}/{m}', { n: sayi(s.current_round || 0), m: sayi(s.rounds) }))}</span>`);
260  }
261  if (s.can_vote) p.push(`<span class="os-rozet os-rozet-oy os-rozet-dolu">${escapeHtml(T('ui.oyun.savas.oy_ver_cip', 'oy ver'))}</span>`);
262  if (s.is_public && s.public_key) p.push(`<span class="os-rozet os-rozet-soft">${escapeHtml(T('ui.oyun.savas.acik_cip', 'Herkese açık'))}</span>`);
263  p.push(sonucCipiHtml(s));
264  return p.join('');
265}
266
267// ====================================================================== LİSTE
268
269const DURUMLAR = () => ([
270  ['',        T('ui.oyun.savas.suzgec_tumu', 'Tümü')],
271  ['pending', T('ui.oyun.savas.suzgec_pending', 'Bekleyen')],
272  ['running', T('ui.oyun.savas.suzgec_running', 'Süren')],
273  ['voting',  T('ui.oyun.savas.suzgec_voting', 'Oylama')],
274  ['judged',  T('ui.oyun.savas.suzgec_judged', 'Bitti')],
275]);
276
277function satirHtml(s, { davetli = false } = {}) {
278  const a = klonAdi(s, 'a'), b = klonAdi(s, 'b');
279  const sahipler = [kisiAdi(s.a && s.a.owner), kisiAdi(s.b && s.b.owner)].join(' · ');
280  const eylem = davetli ? `<div class="os-satir-eylem">
281      <button type="button" class="os-btn os-btn-sm os-btn-ghost os-btn-kotu" data-reddet="${s.id}">${escapeHtml(T('ui.oyun.savas.reddet', 'Reddet'))}</button>
282      <button type="button" class="os-btn os-btn-sm os-btn-primary" data-kabul="${s.id}">${escapeHtml(T('ui.oyun.savas.kabul', 'Kabul et'))}</button>
283    </div>` : '';
284  return `<div class="os-satir" role="button" tabindex="0" data-ac="${s.id}">
285    <span class="os-satir-ikon ${durumSinifi(s.status)}">${IKON.simsek}</span>
286    <span class="os-satir-metin">
287      <span class="os-satir-konu">${escapeHtml(s.topic || '')}</span>
288      <span class="os-satir-taraflar">${escapeHtml(a)} <span class="os-vs">vs</span> ${escapeHtml(b)}</span>
289      <span class="os-satir-alt">${escapeHtml(sahipler)} · ${escapeHtml(tarih(s.created_at))}</span>
290      <span class="os-cipler">${rozetlerHtml(s)}</span>
291      ${eylem}
292    </span>
293    <span class="os-satir-ok">${IKON.ok}</span>
294  </div>`;
295}
296
297function acikOyKartiHtml(s) {
298  return `<div class="os-oy-kart" role="button" tabindex="0" data-ac="${s.id}">
299    <div class="os-oy-kart-konu">${escapeHtml(s.topic || '')}</div>
300    <div class="os-oy-kart-taraflar">${klonAvatarHtml(s.a && s.a.assistant, 24)}<span>${escapeHtml(klonAdi(s, 'a'))}</span>
301      <span class="os-vs">vs</span>${klonAvatarHtml(s.b && s.b.assistant, 24)}<span>${escapeHtml(klonAdi(s, 'b'))}</span></div>
302    <div class="os-oy-kart-alt"><span class="os-rozet os-rozet-soft">${escapeHtml(formatAdi(s.format, s.format_ad))}</span>
303      <span class="os-oy-kart-sure">${escapeHtml(s.voting_ends_at ? T('ui.oyun.savas.oylama_bitis', 'Oylama bitişi: {t}', { t: tarih(s.voting_ends_at) }) : '')}</span></div>
304    <button type="button" class="os-btn os-btn-sm os-btn-primary" data-ac="${s.id}">${escapeHtml(T('ui.oyun.savas.oy_ver', 'Oy ver'))}</button>
305  </div>`;
306}
307
308/**
309 * Klon Savaşları listesi. `ac(id)` ayrıntıya geçirir (lobi adres çubuğunu
310 * yazar), `geri` lobiye döner.
311 */
312export async function renderSavaslar(host, { geri, ac } = {}) {
313  stilleriEkle();
314  const d = { durum: '', battles: [], acikOylar: [], yukleniyor: true, yuklendi: false, hata: '', istek: 0 };
315  const kok = document.createElement('div');
316  kok.className = 'os-host';
317  host.innerHTML = '';
318  host.appendChild(kok);
319
320  const acGit = (id) => {
321    if (typeof ac === 'function') { ac(id); return; }
322    // Kablosuz kullanım: ayrıntıdan geri = bu liste.
323    renderSavas(host, id, { geri: () => renderSavaslar(host, { geri, ac }) }).catch(e => toast(hataMetni(e), 'error'));
324  };
325
326  const ciz = () => {
327    const me = benimId();
328    const sekmeler = DURUMLAR().map(([k, ad]) =>
329      `<button type="button" class="os-sekme ${d.durum === k ? 'on' : ''}" data-durum="${k}">${escapeHtml(ad)}</button>`).join('');
330    // Şerit "oy verebileceklerim": sunucu acik_oylamalar SAHİBİN kendi savaşını
331    // ve zaten oy verdiklerimi de döndürür (yalnız status/is_public süzer);
332    // can_vote bakana göre hesaplanır — sahip ya da oy vermiş ise düşer.
333    // Listede zaten görünen satır ikinci kez şeride girmez.
334    const listeIdler = new Set(d.battles.map(s => Number(s.id)));
335    const acik = d.acikOylar.filter(s => s.can_vote && !listeIdler.has(Number(s.id)));
336    let govde = '';
337    if (d.yukleniyor && !d.yuklendi) govde = yukleniyorHtml();
338    else if (d.hata && !d.yuklendi) govde = hataHtml(d.hata);
339    else if (!d.battles.length) {
340      govde = bosHtml(
341        d.durum ? T('ui.oyun.savas.bos_suzgec', 'Bu durumda savaş yok') : T('ui.oyun.savas.bos', 'Henüz savaşın yok'),
342        d.durum ? '' : T('ui.oyun.savas.bos_alt', '"Yeni savaş" ile klonunu bir konuda yarıştır; karşı klonun sahibi kabul edince başlar.'));
343    } else {
344      govde = `<div class="os-liste ${d.yukleniyor ? 'os-soluk' : ''}">${d.battles.map(s =>
345        satirHtml(s, { davetli: s.status === 'pending' && me != null && s.b && s.b.owner && Number(s.b.owner.id) === me })).join('')}</div>
346        ${d.battles.length >= 30 ? `<p class="os-not">${escapeHtml(T('ui.oyun.savas.liste_siniri', 'Son 30 savaş gösteriliyor.'))}</p>` : ''}`;
347    }
348    const serit = acik.length ? `<div class="os-bolum">${escapeHtml(T('ui.oyun.savas.acik_oylamalar', 'Açık oylamalar'))}</div>
349      <p class="os-not">${escapeHtml(T('ui.oyun.savas.acik_oylamalar_notu', 'İzleyici olarak oy verebileceğin herkese açık savaşlar.'))}</p>
350      <div class="os-serit">${acik.map(acikOyKartiHtml).join('')}</div>` : '';
351    kok.innerHTML = basHtml(
352      T('ui.oyun.savas.baslik', 'Klon Savaşları'),
353      T('ui.oyun.savas.alt', 'İki klon bir konuda yarışır; hakem puanlar, izleyici oy verir.'),
354      `<div class="os-head-eylem">
355        <button type="button" class="os-btn os-btn-ghost os-btn-sm ${d.yukleniyor ? 'os-busy' : ''}" data-yenile ${d.yukleniyor ? 'disabled' : ''} aria-label="${escapeHtml(T('ui.oyun.savas.yenile', 'Yenile'))}">${IKON.yenile}</button>
356        <button type="button" class="os-btn os-btn-primary" data-yeni>${IKON.arti} ${escapeHtml(T('ui.oyun.savas.yeni', 'Yeni savaş'))}</button>
357      </div>`) +
358      `${d.hata && d.yuklendi ? `<div class="os-uyari">${escapeHtml(d.hata)}</div>` : ''}
359      ${serit}
360      <div class="os-bolum">${escapeHtml(T('ui.oyun.savas.savaslarim', 'Savaşlarım'))}</div>
361      <div class="os-sekmeler">${sekmeler}</div>
362      ${govde}`;
363    bagla();
364  };
365
366  function bagla() {
367    geriBagla(kok, geri);
368    const yeniden = kok.querySelector('[data-yeniden]');
369    if (yeniden) yeniden.addEventListener('click', () => yukle());
370    const yenile = kok.querySelector('[data-yenile]');
371    if (yenile) yenile.addEventListener('click', () => yukle());
372    const yeni = kok.querySelector('[data-yeni]');
373    if (yeni) yeni.addEventListener('click', () => yeniSavasModali({ onBaslatildi: (s) => acGit(s.id) }));
374    kok.querySelectorAll('[data-durum]').forEach(b => b.addEventListener('click', () => {
375      if (d.durum === b.dataset.durum) return;
376      d.durum = b.dataset.durum;
377      yukle();
378    }));
379    // Satır: tıkla ya da Enter → ayrıntı. İç düğmeler (kabul/reddet) kendi işini görür.
380    kok.querySelectorAll('[data-ac]').forEach(el => {
381      const git = (e) => { e.stopPropagation(); acGit(parseInt(el.dataset.ac, 10)); };
382      el.addEventListener('click', git);
383      if (el.tagName !== 'BUTTON') {
384        // Yalnız satırın kendisi odaktayken: içindeki Kabul/Reddet düğmesinde basılan Enter satırı açmasın.
385        el.addEventListener('keydown', (e) => {
386          if (e.target !== el) return;
387          if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); git(e); }
388        });
389      }
390    });
391    kok.querySelectorAll('[data-kabul]').forEach(b => b.addEventListener('click', async (e) => {
392      e.stopPropagation();
393      kilitle(b, true);
394      try {
395        await oyunApi.post(`/savas/${encodeURIComponent(b.dataset.kabul)}/kabul`, {});
396        toast(T('ui.oyun.savas.kabul_edildi', 'Kabul edildi; turlar işleniyor.'), 'success');
397        yukle();
398      } catch (err) {
399        kilitle(b, false);
400        toast(hataMetni(err), 'error');
401      }
402    }));
403    kok.querySelectorAll('[data-reddet]').forEach(b => b.addEventListener('click', async (e) => {
404      e.stopPropagation();
405      const kap = b.closest('.os-satir-metin');
406      const evet = await onayIste(kap, T('ui.oyun.savas.reddet_onay', 'Davet reddedilsin mi?'), { tehlike: true });
407      if (!evet) return;
408      kilitle(b, true);
409      try {
410        await oyunApi.post(`/savas/${encodeURIComponent(b.dataset.reddet)}/reddet`, {});
411        toast(T('ui.oyun.savas.reddedildi', 'Davet reddedildi.'), 'success');
412        yukle();
413      } catch (err) {
414        kilitle(b, false);
415        toast(hataMetni(err), 'error');
416      }
417    }));
418  }
419
420  async function yukle() {
421    const id = ++d.istek;
422    d.yukleniyor = true; d.hata = '';
423    ciz();
424    try {
425      const r = await oyunApi.get('/savas/liste' + (d.durum ? '?durum=' + encodeURIComponent(d.durum) : ''));
426      if (id !== d.istek) return;
427      d.battles = (r && Array.isArray(r.battles)) ? r.battles.filter(Boolean) : [];
428      d.acikOylar = (r && Array.isArray(r.open_votes)) ? r.open_votes.filter(Boolean) : [];
429      d.yuklendi = true;
430    } catch (e) {
431      if (id !== d.istek) return;
432      d.hata = hataMetni(e);
433    }
434    d.yukleniyor = false;
435    ciz();
436  }
437
438  // Sahibi olduğum savaşın durumu değişince (game.battle) liste tazelenir;
439  // 400 ms toplama: bir tur iki çerçeve doğurabilir.
440  let saat = null;
441  const onOlay = (e) => {
442    if (!host.contains(kok)) { kapat(); return; }
443    const f = e && e.detail;
444    if (!f || !/^game\.battle(_round|_close)?$/.test(String(f.type || ''))) return;
445    clearTimeout(saat);
446    saat = setTimeout(() => { if (host.contains(kok)) yukle(); }, 400);
447  };
448  function kapat() { clearTimeout(saat); window.removeEventListener('morfoz:game', onOlay); }
449  window.addEventListener('morfoz:game', onOlay);
450  _ekranAc(host, ciz, kapat);
451  await yukle();
452}
453
454// ====================================================================== AYRINTI
455
456function tarafHtml(s, taraf, kazandi) {
457  const t = s[taraf] || {};
458  const rozet = kazandi
459    ? `<span class="os-rozet os-rozet-ok os-rozet-dolu">${IKON.kupa} ${escapeHtml(T('ui.oyun.savas.kazandi', 'Kazandı'))}</span>`
460    : `<span class="os-rozet ${taraf === 'a' ? 'os-rozet-a' : 'os-rozet-b'}">${taraf.toUpperCase()}</span>`;
461  return `<div class="os-taraf os-taraf-${taraf}">
462    ${klonAvatarHtml(t.assistant, 56)}
463    <div class="os-taraf-ad">${escapeHtml((t.assistant && t.assistant.name) || T('ui.oyun.savas.klon', 'Klon'))}</div>
464    <div class="os-taraf-sahip">${escapeHtml(kisiAdi(t.owner))}${kullaniciHandle(t.owner) && kullaniciHandle(t.owner) !== kisiAdi(t.owner) ? ' · ' + escapeHtml(kullaniciHandle(t.owner)) : ''}</div>
465    ${rozet}
466  </div>`;
467}
468
469function balonHtml(s, t) {
470  const bMi = (t.side || 'a') === 'b';
471  return `<div class="os-balon-satir ${bMi ? 'b' : 'a'}">
472    <div class="os-balon">
473      <div class="os-balon-ust"><span class="os-balon-ad">${escapeHtml(klonAdi(s, bMi ? 'b' : 'a'))}</span>
474        ${t.round ? `<span class="os-balon-tur">${escapeHtml(T('ui.oyun.savas.tur_n', 'Tur {n}', { n: sayi(t.round) }))}</span>` : ''}</div>
475      ${t.q ? `<div class="os-balon-soru">${escapeHtml(T('ui.oyun.savas.soru', 'Soru: {q}', { q: t.q }))}</div>` : ''}
476      <div class="os-balon-metin">${escapeHtml(t.text || '')}</div>
477    </div>
478  </div>`;
479}
480
481/** İki taraflı oran çubuğu (hakem yüzdesi ya da oy sayısı). */
482function oranHtml(aEtiket, bEtiket, aDeger, bDeger, { yuzde = false } = {}) {
483  const a = Number(aDeger) || 0, b = Number(bDeger) || 0;
484  const top = a + b;
485  const pa = top > 0 ? Math.round(100 * a / top) : 50;
486  const goster = (v) => yuzde ? escapeHtml(sayi(Math.round(v))) + '%' : escapeHtml(sayi(v));
487  return `<div class="os-oran">
488    <div class="os-oran-ust"><span class="os-oran-a">${escapeHtml(aEtiket)} <b>${goster(a)}</b></span>
489      <span class="os-oran-b"><b>${goster(b)}</b> ${escapeHtml(bEtiket)}</span></div>
490    <div class="os-oran-cubuk"><span class="os-oran-dolu-a" style="width:${pa}%"></span><span class="os-oran-dolu-b" style="width:${100 - pa}%"></span></div>
491  </div>`;
492}
493
494/**
495 * Tek savaşın ayrıntısı: taraflar, döküm, hakem, oy, paylaşım; davet eylemleri.
496 */
497export async function renderSavas(host, id, { geri } = {}) {
498  stilleriEkle();
499  const d = { id: Number(id), s: null, yukleniyor: true, hata: '', istek: 0, not: '' };
500  const kok = document.createElement('div');
501  kok.className = 'os-host';
502  host.innerHTML = '';
503  host.appendChild(kok);
504
505  const ciz = () => {
506    const s = d.s;
507    if (!s) {
508      kok.innerHTML = basHtml(T('ui.oyun.savas.tek_baslik', 'Klon savaşı'), '') +
509        (d.yukleniyor ? yukleniyorHtml() : hataHtml(d.hata || T('ui.oyun.savas.bulunamadi', 'Savaş bulunamadı')));
510      bagla();
511      return;
512    }
513    const taraf = benimTaraf(s);
514    const bitti = s.status === 'judged';
515    const bekliyor = s.status === 'pending';
516    const aAd = klonAdi(s, 'a'), bAd = klonAdi(s, 'b');
517
518    // Başlık altı: son tarih notu.
519    let sure = '';
520    if (s.status === 'voting' && s.voting_ends_at) sure = T('ui.oyun.savas.oylama_bitis', 'Oylama bitişi: {t}', { t: tarih(s.voting_ends_at) });
521    else if (bekliyor && s.accept_deadline_at) sure = T('ui.oyun.savas.kabul_son', 'Kabul için son: {t}', { t: tarih(s.accept_deadline_at) });
522    else if (bitti && s.ended_at) sure = T('ui.oyun.savas.bitis', 'Bitiş: {t}', { t: tarih(s.ended_at) });
523
524    // Davet eylemleri (yalnız pending): B'nin sahibi kabul/reddeder, A'nın sahibi geri çeker.
525    let eylem = '';
526    if (bekliyor && taraf === 'b') {
527      eylem = `<div class="os-kart os-kart-eylem">
528        <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.davet_metni', '{ad}, klonunu bir savaşa davet etti.', { ad: kisiAdi(s.a && s.a.owner) }))}</div>
529        <p class="os-not">${escapeHtml(T('ui.oyun.savas.kabul_notu', 'Kabul edince turlar kuyrukta işlenir; sonuç bildirimle gelir.'))}</p>
530        <div class="os-eylemler">
531          <button type="button" class="os-btn os-btn-ghost os-btn-kotu" data-reddet>${IKON.kapat} ${escapeHtml(T('ui.oyun.savas.reddet', 'Reddet'))}</button>
532          <button type="button" class="os-btn os-btn-primary" data-kabul>${IKON.onay} ${escapeHtml(T('ui.oyun.savas.kabul', 'Kabul et'))}</button>
533        </div>
534      </div>`;
535    } else if (bekliyor && taraf === 'a') {
536      eylem = `<div class="os-kart os-kart-eylem">
537        <p class="os-not">${escapeHtml(T('ui.oyun.savas.bekleme_notu', 'Karşı klonun sahibi kabul edince savaş başlar.'))}</p>
538        <div class="os-eylemler">
539          <button type="button" class="os-btn os-btn-ghost os-btn-kotu os-btn-sm" data-geri-cek>${escapeHtml(T('ui.oyun.savas.geri_cek', 'Daveti geri çek'))}</button>
540        </div>
541      </div>`;
542    }
543
544    const taraflar = `<div class="os-kart os-taraflar">
545      ${tarafHtml(s, 'a', bitti && s.winner_side === 'a')}
546      <div class="os-taraflar-orta">${IKON.simsekBuyuk}${bitti && s.winner_side === 'draw' ? `<span class="os-rozet os-rozet-bekle">${escapeHtml(T('ui.oyun.savas.berabere', 'Berabere'))}</span>` : ''}</div>
547      ${tarafHtml(s, 'b', bitti && s.winner_side === 'b')}
548    </div>`;
549
550    const tr = Array.isArray(s.transcript) ? s.transcript : [];
551    let dokum;
552    if (!tr.length) {
553      let m;
554      if (bekliyor) m = T('ui.oyun.savas.baslamadi', 'Savaş henüz başlamadı.');
555      else if (s.status === 'running') m = T('ui.oyun.savas.turlar_isleniyor', 'Turlar işleniyor…');
556      else if (s.status === 'cancelled' || s.status === 'expired') m = T('ui.oyun.savas.yapilmadi', 'Savaş yapılmadı.');
557      else m = T('ui.oyun.savas.dokum_yok', 'Döküm yok.');
558      dokum = `<div class="os-kart os-dokum-bos">${IKON.balon}<span>${escapeHtml(m)}</span></div>`;
559    } else {
560      dokum = `<div class="os-bolum">${escapeHtml(T('ui.oyun.savas.karsilasma', 'Karşılaşma'))}</div>
561        <div class="os-dokum">${tr.map(t => balonHtml(s, t)).join('')}
562        ${s.status === 'running' ? `<div class="os-dokum-devam"><span class="os-spinner"></span>${escapeHtml(T('ui.oyun.savas.turlar_isleniyor', 'Turlar işleniyor…'))}</div>` : ''}</div>`;
563    }
564
565    let hakem = '';
566    if (bitti && s.judge) {
567      const h = s.judge;
568      hakem = `<div class="os-kart">
569        <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.hakem', 'Hakem'))}</div>
570        ${h.hakem_yok ? `<p class="os-not">${escapeHtml(T('ui.oyun.savas.hakem_yok', 'Hakem koşmadı; sonuç yalnız oylardan.'))}</p>` : ''}
571        ${oranHtml(aAd, bAd, h.a, h.b, { yuzde: true })}
572        ${h.reason ? `<p class="os-hakem-neden">${escapeHtml(h.reason)}</p>` : ''}
573        ${h.tutarsiz ? `<p class="os-not">${escapeHtml(T('ui.oyun.savas.hakem_tutarsiz', 'İki hakem koşusu ayrıştı; berabere sayıldı.'))}</p>` : ''}
574      </div>`;
575    }
576
577    let oy = '';
578    if (s.can_vote) {
579      oy = `<div class="os-kart">
580        <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.kim_kazandi', 'Kim kazandı?'))}</div>
581        <p class="os-not">${escapeHtml(T('ui.oyun.savas.oy_notu', 'Tek oy; dağılım oylama kapanana kadar gizli.'))}</p>
582        <div class="os-oy-dugmeler">
583          <button type="button" class="os-btn os-btn-a" data-oy="a">${escapeHtml(aAd)}</button>
584          <button type="button" class="os-btn os-btn-b" data-oy="b">${escapeHtml(bAd)}</button>
585        </div>
586      </div>`;
587    } else if (s.my_vote) {
588      oy = `<div class="os-kart os-oy-verildi">${IKON.onay} ${escapeHtml(T('ui.oyun.savas.oyun', 'Oyun: {ad}', { ad: s.my_vote === 'a' ? aAd : bAd }))}</div>`;
589    } else if (s.status === 'voting' && taraf) {
590      oy = `<div class="os-kart"><p class="os-not">
590${escapeHtml(T('ui.oyun.savas.oylama_suruyor', 'Oylama sürüyor; sahipler oy veremez, dağılım kapanınca görünür.'))}</p></div>`;
591    }
592    if (bitti && s.votes) {
593      oy += `<div class="os-kart">
594        <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.izleyici_oyu', 'İzleyici oyu'))}</div>
595        ${oranHtml(aAd, bAd, s.votes.a, s.votes.b)}
596        ${taraf ? `<p class="os-not">${escapeHtml(s.points_awarded
597          ? T('ui.oyun.savas.puan_yazildi', 'Sahiplere sezon puanı yazıldı; cüzdanında görünür.')
598          : T('ui.oyun.savas.puan_yok', 'Bu savaş puan kazandırmadı (aynı çiftle sezon sınırı ya da bütçe).'))}</p>` : ''}
599      </div>`;
600    }
601
602    let paylas = '';
603    if (s.is_public && s.public_key) {
604      const url = paylasimAdresi(s.public_key);
605      paylas = `<div class="os-kart">
606        <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.paylas', 'Paylaş'))}</div>
607        <p class="os-not">${escapeHtml(T('ui.oyun.savas.paylas_notu', 'Oy toplamak için bağlantıyı gönder.'))}</p>
608        <div class="os-paylas-satir">
609          <input type="text" readonly value="${escapeHtml(url)}" data-paylas-adres>
610          <button type="button" class="os-btn os-btn-ghost" data-paylas>${IKON.paylas} ${escapeHtml(T('ui.oyun.savas.paylas_dugme', 'Bağlantıyı paylaş'))}</button>
611        </div>
612      </div>`;
613    }
614
615    kok.innerHTML = basHtml(s.topic || T('ui.oyun.savas.tek_baslik', 'Klon savaşı'),
616      `${formatAdi(s.format, s.format_ad)} · ${savasDurumu(s.status)}${sure ? ' · ' + sure : ''}`,
617      `<button type="button" class="os-btn os-btn-ghost os-btn-sm" data-yenile aria-label="${escapeHtml(T('ui.oyun.savas.yenile', 'Yenile'))}">${IKON.yenile}</button>`) +
618      `<div class="os-cipler os-cipler-bas">${rozetlerHtml(s)}</div>
619      ${d.hata ? `<div class="os-uyari">${escapeHtml(d.hata)}</div>` : ''}
620      ${d.not ? `<div class="os-bilgi-not">${IKON.onay} ${escapeHtml(d.not)}</div>` : ''}
621      ${eylem}
622      ${taraflar}
623      ${dokum}
624      ${hakem}
625      ${oy}
626      ${paylas}`;
627    bagla();
628  };
629
630  function bagla() {
631    geriBagla(kok, geri);
632    const yeniden = kok.querySelector('[data-yeniden]');
633    if (yeniden) yeniden.addEventListener('click', () => yukle());
634    const yenile = kok.querySelector('[data-yenile]');
635    if (yenile) yenile.addEventListener('click', () => yukle());
636
637    const kabul = kok.querySelector('[data-kabul]');
638    if (kabul) kabul.addEventListener('click', async () => {
639      const reddet = kok.querySelector('[data-reddet]');
640      kilitle(kabul, true); kilitle(reddet, true);
641      d.hata = ''; d.not = '';
642      try {
643        const r = await oyunApi.post(`/savas/${encodeURIComponent(d.id)}/kabul`, {});
644        if (r && r.id) d.s = r;
645        d.not = T('ui.oyun.savas.kabul_edildi', 'Kabul edildi; turlar işleniyor.');
646        toast(d.not, 'success');
647        ciz();
648      } catch (e) {
649        kilitle(kabul, false); kilitle(reddet, false);
650        toast(hataMetni(e), 'error');
651      }
652    });
653    const reddet = kok.querySelector('[data-reddet]') || kok.querySelector('[data-geri-cek]');
654    if (reddet) reddet.addEventListener('click', async () => {
655      const kap = reddet.closest('.os-kart');
656      const geriCek = reddet.hasAttribute('data-geri-cek');
657      const evet = await onayIste(kap, geriCek
658        ? T('ui.oyun.savas.geri_cek_onay', 'Davet geri çekilsin mi?')
659        : T('ui.oyun.savas.reddet_onay', 'Davet reddedilsin mi?'), { tehlike: true });
660      if (!evet) return;
661      kilitle(reddet, true); kilitle(kok.querySelector('[data-kabul]'), true);
662      try {
663        await oyunApi.post(`/savas/${encodeURIComponent(d.id)}/reddet`, {});
664        toast(geriCek ? T('ui.oyun.savas.geri_cekildi', 'Davet geri çekildi.') : T('ui.oyun.savas.reddedildi', 'Davet reddedildi.'), 'success');
665        yukle();
666      } catch (e) {
667        kilitle(reddet, false); kilitle(kok.querySelector('[data-kabul]'), false);
668        toast(hataMetni(e), 'error');
669      }
670    });
671
672    kok.querySelectorAll('[data-oy]').forEach(b => b.addEventListener('click', async () => {
673      kok.querySelectorAll('[data-oy]').forEach(x => kilitle(x, true));
674      d.hata = ''; d.not = '';
675      try {
676        const r = await oyunApi.post(`/savas/${encodeURIComponent(d.id)}/oy`, { side: b.dataset.oy });
677        if (r && r.id) d.s = r;
678        d.not = T('ui.oyun.savas.oy_kaydedildi', 'Oyun kaydedildi.');
679        toast(d.not, 'success');
680        ciz();
681      } catch (e) {
682        kok.querySelectorAll('[data-oy]').forEach(x => kilitle(x, false));
683        toast(hataMetni(e), 'error');
684      }
685    }));
686
687    const paylas = kok.querySelector('[data-paylas]');
688    if (paylas) paylas.addEventListener('click', async () => {
689      const s = d.s;
690      const url = paylasimAdresi(s.public_key);
691      const baslik = `${klonAdi(s, 'a')} vs ${klonAdi(s, 'b')} · ${T('ui.oyun.savas.tek_baslik', 'Klon savaşı')}`;
692      kilitle(paylas, true);
693      try {
694        let paylasildi = false;
695        if (navigator.share) {
696          // Kullanıcı vazgeçtiyse (AbortError) sessiz; paylaşım desteklenmiyorsa panoya düş.
697          try { await navigator.share({ title: baslik, text: s.topic || baslik, url }); paylasildi = true; }
698          catch (e) { if (e && e.name === 'AbortError') paylasildi = true; }
699        }
700        if (!paylasildi) {
701          await navigator.clipboard.writeText(url);
702          toast(T('ui.oyun.savas.kopyalandi', 'Bağlantı kopyalandı.'), 'success');
703        }
704      } catch {
705        // Pano kapalıysa (izin yok, http) adres kutusunu seçili bırak: elle kopyalanır.
706        const giris = kok.querySelector('[data-paylas-adres]');
707        if (giris) { giris.focus(); giris.select(); }
708        toast(T('ui.oyun.savas.kopyalanamadi', 'Kopyalanamadı; adresi seçip kopyala.'), 'error');
709      }
710      kilitle(paylas, false);
711    });
712    const adres = kok.querySelector('[data-paylas-adres]');
713    if (adres) adres.addEventListener('focus', () => adres.select());
714  }
715
716  async function yukle() {
717    const id = ++d.istek;
718    d.yukleniyor = true; d.hata = '';
719    // Gövde varken yeniden çizilmez (ekran zıplamasın); yalnız yenile düğmesi
720    // kilitlenir — ciz() bittiğinde DOM yenilendiği için kilit kendiliğinden kalkar.
721    if (!d.s) ciz(); else kilitle(kok.querySelector('[data-yenile]'), true);
722    try {
723      const r = await oyunApi.get(`/savas/${encodeURIComponent(d.id)}`);
724      if (id !== d.istek) return;
725      if (r && r.id) d.s = r;
726      else d.hata = T('ui.oyun.savas.bulunamadi', 'Savaş bulunamadı');
727    } catch (e) {
728      if (id !== d.istek) return;
729      d.hata = hataMetni(e);
730    }
731    d.yukleniyor = false;
732    ciz();
733  }
734
735  // Canlı: BU savaşın çerçevesi (tur eklendi, hakem koştu, kapandı) → 400 ms
736  // toplayıp yeniden yükle. Dinleyici bir kez bağlanır; kök host'tan kalkınca
737  // (başka ekran çizilince) kendini söker.
738  let saat = null;
739  const onOlay = (e) => {
740    if (!host.contains(kok)) { kapat(); return; }
741    const f = e && e.detail;
742    if (!f || !/^game\.battle(_round|_close)?$/.test(String(f.type || ''))) return;
743    if (f.battle_id != null && Number(f.battle_id) !== d.id) return;
744    clearTimeout(saat);
745    saat = setTimeout(() => { if (host.contains(kok)) yukle(); }, 400);
746  };
747  function kapat() { clearTimeout(saat); window.removeEventListener('morfoz:game', onOlay); }
748  window.addEventListener('morfoz:game', onOlay);
749  _ekranAc(host, ciz, kapat);
750  await yukle();
751}
752
753/** Herkese açık sayfa: routes/oyun_public_routes.py → GET /kp/savas/{public_key}. */
754function paylasimAdresi(anahtar) {
755  return `${location.origin}/kp/savas/${encodeURIComponent(String(anahtar || ''))}`;
756}
757
758// ====================================================================== YENİ SAVAŞ
759
760/** Sahibi olduğum klonlar. `scope=mine` yanıtı yalnız {id, name, description,
761 *  avatar_tone, avatar_url, visibility, my_role, modules} taşır (assistants_routes
762 *  list_assistants) — seed_source/klon_kind GELMEZ; savaşa uygunluğu (uygun_klon)
763 *  sunucu POST /savas'ta denetler, burada yalnız sahiplik süzülür. */
764async function klonlarimiYukle() {
765  try {
766    const rows = await api.get('/api/assistants?scope=mine', { sessiz: true });
767    const liste = Array.isArray(rows) ? rows : (rows && rows.items) || [];
768    return liste.filter(a => a && (!a.my_role || a.my_role === 'owner'));
769  } catch {
770    return [];
771  }
772}
773
774/**
775 * "Yeni savaş" modalı: klonum → format (sunucudan; konu istemesi formata göre)
776 * → rakip (kişi ya da rehberden klon) → herkese açık → POST /savas.
777 * Sunucu rakip KİŞİ verilince onun klonunu kendisi seçer (motor.klon_bul).
778 */
779function yeniSavasModali({ onBaslatildi }) {
780  const d = {
781    klonlar: [], assistant_a: null, formatlar: [], formatHata: '', formatIstek: 0, format: 'debate', topic: '',
782    rakipTur: 'kisi', rakip: null, rakipKlon: null, is_public: true,
783    arama: '', aramaSonuc: [], aramaIstek: 0, calisiyor: false, yukleniyor: true,
784  };
785  const ov = document.createElement('div');
786  ov.className = 'os-overlay';
787  ov.innerHTML = `<div class="os-modal" role="dialog" aria-modal="true" aria-label="${escapeHtml(T('ui.oyun.savas.yeni', 'Yeni savaş'))}">
788    <div class="os-modal-head">
789      <div class="os-modal-title">${escapeHtml(T('ui.oyun.savas.yeni', 'Yeni savaş'))}</div>
790      <button type="button" class="os-modal-x" data-kapat aria-label="${escapeHtml(T('action.close', 'Kapat'))}">${IKON.kapat}</button>
791    </div>
792    <div class="os-modal-govde">
793      <div class="os-form-bolum" data-klon></div>
794      <div class="os-form-bolum" data-formatlar></div>
795      <div class="os-form-bolum" data-konu></div>
796      <div class="os-form-bolum" data-rakip></div>
797      <div class="os-form-bolum" data-acik></div>
798      <div class="os-form-hata" data-hata hidden></div>
799    </div>
800    <div class="os-modal-alt">
801      <button type="button" class="os-btn os-btn-ghost" data-kapat>${escapeHtml(T('action.cancel', 'Vazgeç'))}</button>
802      <button type="button" class="os-btn os-btn-primary" data-gonder disabled>${IKON.simsek} ${escapeHtml(T('ui.oyun.savas.davet_et', 'Savaşa davet et'))}</button>
803    </div>
804  </div>`;
805  document.body.appendChild(ov);
806  const kapat = () => { ov.remove(); document.removeEventListener('keydown', esc); };
807  function esc(e) { if (e.key === 'Escape' && !document.querySelector('.up-modal-overlay')) kapat(); }
808  document.addEventListener('keydown', esc);
809  ov.addEventListener('click', (e) => { if (e.target === ov) kapat(); });
810  ov.querySelectorAll('[data-kapat]').forEach(b => b.addEventListener('click', kapat));
811
812  const el = (sec) => ov.querySelector(sec);
813  const hataKutu = el('[data-hata]');
814  const gonder = el('[data-gonder]');
815  const hataGoster = (m) => { hataKutu.textContent = m; hataKutu.hidden = !m; };
816
817  function gecerliMi() {
818    const f = d.formatlar.find(x => x.key === d.format);
819    return !d.calisiyor && !!d.assistant_a && !!f && !!f.acik &&
820      (d.rakipTur === 'kisi' ? !!d.rakip : !!d.rakipKlon);
821  }
822  function dugmeGuncelle() { gonder.disabled = !gecerliMi(); }
823
824  // --- klonum
825  function klonCiz() {
826    const kap = el('[data-klon]');
827    if (d.yukleniyor) { kap.innerHTML = yukleniyorHtml(); return; }
828    if (!d.klonlar.length) {
829      kap.innerHTML = `<div class="os-uyari">${escapeHtml(T('ui.oyun.savas.klon_yok', 'Savaşa sokacak bir klonun yok. Önce bir asistan kur (otomatik üretilen klonlar savaşa giremez).'))}</div>`;
830      return;
831    }
832    kap.innerHTML = `<label class="os-alan"><span>${escapeHtml(T('ui.oyun.savas.klonum', 'Klonum'))}</span>
833      <select data-klon-sec>${d.klonlar.map(a =>
834        `<option value="${escapeHtml(String(a.id))}" ${Number(a.id) === Number(d.assistant_a) ? 'selected' : ''}>${escapeHtml(a.name || ('#' + a.id))}</option>`).join('')}</select></label>`;
835    kap.querySelector('[data-klon-sec]').addEventListener('change', (e) => {
836      d.assistant_a = parseInt(e.target.value, 10) || null;
837      formatlariYukle();
838    });
839  }
840
841  // --- format (sunucudan; kapalı olanlar sebebiyle gri)
842  function formatlarCiz() {
843    const kap = el('[data-formatlar]');
844    if (!d.formatlar.length) {
845      // Hata: "yükleniyor"da takılı kalmasın — sebep + yeniden dene (çıkmaz yok).
846      kap.innerHTML = `<div class="os-bolum-kucuk">${escapeHtml(T('ui.oyun.savas.format', 'Format'))}</div>` +
847        (d.formatHata ? hataHtml(d.formatHata)
848          : `<p class="os-not">${escapeHtml(T('ui.oyun.savas.formatlar_yukleniyor', 'Formatlar yükleniyor…'))}</p>`);
849      const yeniden = kap.querySelector('[data-yeniden]');
850      if (yeniden) yeniden.addEventListener('click', () => formatlariYukle());
851      return;
852    }
853    kap.innerHTML = `<div class="os-bolum-kucuk">${escapeHtml(T('ui.oyun.savas.format', 'Format'))}</div>
854      <div class="os-secenekler">${d.formatlar.map(f => `
855        <button type="button" class="os-secenek ${d.format === f.key ? 'on' : ''} ${f.acik ? '' : 'kapali'}" data-format="${escapeHtml(f.key)}" ${f.acik ? '' : 'disabled'}>
856          <span class="os-secenek-nokta"></span>
857          <span class="os-secenek-metin">
858            <span class="os-secenek-ad">${escapeHtml(formatAdi(f.key, f.ad))}</span>
859            <span class="os-secenek-alt">${escapeHtml(f.aciklama || '')}</span>
860            ${f.acik ? '' : `<span class="os-secenek-neden">${escapeHtml(nedenMetni(f.neden))}</span>`}
861          </span>
862        </button>`).join('')}</div>`;
863    kap.querySelectorAll('[data-format]').forEach(b => b.addEventListener('click', () => {
864      d.format = b.dataset.format;
865      if (d.format === 'sales') d.is_public = false;
866      formatlarCiz(); konuCiz(); acikCiz(); dugmeGuncelle();
867    }));
868  }
869
870  async function formatlariYukle() {
871    // Klon/rakip art arda değişince istekler yarışır: yalnız SON isteğin yanıtı
872    // işlenir, önceki geç gelirse atılır (aksi hâlde eski klonun kapalı formatı kalır).
873    const istek = ++d.formatIstek;
874    // Boş parametre gönderilmez: sunucu int|None bekler, '' 422 döndürür.
875    const q = [];
876    if (d.assistant_a) q.push('assistant_id=' + encodeURIComponent(String(d.assistant_a)));
877    if (d.rakipTur === 'klon' && d.rakipKlon) q.push('rakip_assistant_id=' + encodeURIComponent(String(d.rakipKlon.id)));
878    d.formatHata = '';
879    if (!d.formatlar.length) formatlarCiz();
880    try {
881      const r = await oyunApi.get('/savas/formatlar' + (q.length ? '?' + q.join('&') : ''), { sessiz: true });
882      if (istek !== d.formatIstek || !ov.isConnected) return;
883      d.formatlar = (r && Array.isArray(r.formats)) ? r.formats : [];
884    } catch (e) {
885      if (istek !== d.formatIstek || !ov.isConnected) return;
886      d.formatlar = [];
887      d.formatHata = hataMetni(e);
888    }
889    const secili = d.formatlar.find(f => f.key === d.format && f.acik);
890    if (!secili) {
891      const ilk = d.formatlar.find(f => f.acik);
892      if (ilk) d.format = ilk.key;
893    }
894    if (d.format === 'sales') d.is_public = false;
895    formatlarCiz(); konuCiz(); acikCiz(); dugmeGuncelle();
896  }
897
898  // --- konu: tartışma "Konu", satış "Senaryo"; bilgi formatı konu istemez
899  // (sorular klonların eğitiminden üretilir).
900  function konuCiz() {
901    const kap = el('[data-konu]');
902    if (d.format === 'knowledge') { kap.innerHTML = ''; return; }
903    const etiket = d.format === 'sales'
904      ? T('ui.oyun.savas.senaryo', 'Senaryo (isteğe bağlı)')
905      : T('ui.oyun.savas.konu', 'Konu (isteğe bağlı)');
906    kap.innerHTML = `<label class="os-alan"><span>${escapeHtml(etiket)}</span>
907      <textarea rows="2" maxlength="200" data-konu-giris placeholder="${escapeHtml(T('ui.oyun.savas.konu_yer', 'Boş bırakırsan konu havuzdan seçilir'))}">${escapeHtml(d.topic)}</textarea></label>`;
908    kap.querySelector('[data-konu-giris]').addEventListener('input', (e) => { d.topic = e.target.value.slice(0, 200); });
909  }
910
911  // --- rakip: kişi (sunucu klonunu bulur) ya da rehberden klon
912  function rakipCiz() {
913    const kap = el('[data-rakip]');
914    const sekme = (k, ad) => `<button type="button" class="os-sekme ${d.rakipTur === k ? 'on' : ''}" data-rakip-tur="${k}">${escapeHtml(ad)}</button>`;
915    let icerik;
916    if (d.rakipTur === 'kisi') {
917      icerik = d.rakip
918        ? `<div class="os-secili"><span class="os-secili-ikon">${IKON.kisi}</span>
919            <span class="os-secili-ad">${escapeHtml(kisiAdi(d.rakip))}</span>
920            <button type="button" class="os-cip-x" data-rakip-sil aria-label="${escapeHtml(T('action.close', 'Kapat'))}">${IKON.kapat}</button></div>`
921        : `<div class="os-secili-yok"><span>${escapeHtml(T('ui.oyun.savas.rakip_yok', 'Henüz kimse seçilmedi'))}</span>
922            <button type="button" class="os-btn os-btn-ghost os-btn-sm" data-rehber>${IKON.kisi} ${escapeHtml(T('ui.oyun.savas.kisi_sec', 'Kişi seç'))}</button></div>`;
923      icerik += `<p class="os-not">${escapeHtml(T('ui.oyun.savas.kisi_notu', 'Kişiyi seç; sunucu onun klonunu bulur.'))}</p>`;
924    } else {
925      icerik = d.rakipKlon
926        ? `<div class="os-secili">${klonAvatarHtml(d.rakipKlon, 26)}
927            <span class="os-secili-ad">${escapeHtml(d.rakipKlon.name || '')}${d.rakipKlon.owner_name ? ` <span class="os-secili-alt">· ${escapeHtml(d.rakipKlon.owner_name)}</span>` : ''}</span>
928            <button type="button" class="os-cip-x" data-rakip-sil aria-label="${escapeHtml(T('action.close', 'Kapat'))}">${IKON.kapat}</button></div>`
929        : `<div class="os-arama">${IKON.arama}<input type="text" data-klon-arama autocomplete="off" value="${escapeHtml(d.arama)}" placeholder="${escapeHtml(T('ui.oyun.savas.klon_ara', 'Rehberde klon ara (ad)'))}"></div>
930           <div class="os-arama-sonuc" data-klon-sonuc></div>`;
931      icerik += `<p class="os-not">${escapeHtml(T('ui.oyun.savas.klon_notu', 'Herkese açık rehberdeki bir klonu seç; sahibi kabul edince başlar.'))}</p>`;
932    }
933    kap.innerHTML = `<div class="os-bolum-kucuk">${escapeHtml(T('ui.oyun.savas.rakip', 'Rakip'))}</div>
934      <div class="os-sekmeler os-sekmeler-kucuk">${sekme('kisi', T('ui.oyun.savas.rakip_kisi', 'Kişi'))}${sekme('klon', T('ui.oyun.savas.rakip_klon', 'Rehberden klon'))}</div>
935      ${icerik}`;
936    kap.querySelectorAll('[data-rakip-tur]').forEach(b => b.addEventListener('click', () => {
937      if (d.rakipTur === b.dataset.rakipTur) return;
938      d.rakipTur = b.dataset.rakipTur;
939      rakipCiz(); dugmeGuncelle();
940      // Bilgi formatı iki klonun eğitimine bakar: rakip klon değişince yeniden sor.
941      formatlariYukle();
942    }));
943    const rehber = kap.querySelector('[data-rehber]');
944    if (rehber) rehber.addEventListener('click', () => {
945      openUserSearchModal({
946        onPick: (id, u) => {
947          d.rakip = u || { id };
948          // Tek rakip seçiliyor: kişi seçici "ekle" modalı açık bırakır, biz kapatırız.
949          const uo = document.querySelector('.up-modal-overlay');
950          if (uo) uo.remove();
951          rakipCiz(); dugmeGuncelle();
952        },
953      });
954    });
955    const sil = kap.querySelector('[data-rakip-sil]');
956    if (sil) sil.addEventListener('click', () => {
957      d.rakip = null; d.rakipKlon = null;
958      rakipCiz(); dugmeGuncelle();
959      formatlariYukle();
960    });
961    const arama = kap.querySelector('[data-klon-arama]');
962    if (arama) {
963      let saat = null;
964      arama.addEventListener('input', () => {
965        d.arama = arama.value;
966        clearTimeout(saat);
967        saat = setTimeout(klonAra, 250);
968      });
969      setTimeout(() => arama.focus(), 20);
970      if (d.arama.trim().length >= 2) klonAra();
971    }
972  }
973
974  /** Rehber araması: sunucu sayfalar (limit 20); tüm liste asla yüklenmez. */
975  async function klonAra() {
976    const kap = el('[data-klon-sonuc]');
977    if (!kap) return;
978    const q = d.arama.trim();
979    if (q.length < 2) { kap.innerHTML = `<div class="os-arama-ipucu">${escapeHtml(T('ui.oyun.savas.ara_ipucu', 'En az 2 harf yaz'))}</div>`; return; }
980    const istek = ++d.aramaIstek;
981    kap.innerHTML = `<div class="os-arama-ipucu">${escapeHtml(T('ui.oyun.savas.araniyor', 'Aranıyor…'))}</div>`;
982    let rows = [];
983    try {
984      rows = await api.get('/api/assistants?scope=directory&q=' + encodeURIComponent(q) + '&limit=20&sort=top', { sessiz: true });
985    } catch (e) {
986      if (istek !== d.aramaIstek) return;
987      kap.innerHTML = `<div class="os-arama-ipucu">${escapeHtml(hataMetni(e))}</div>`;
988      return;
989    }
990    if (istek !== d.aramaIstek) return;
991    const me = benimId();
992    // Kendi klonum ve otomatik üretilmiş (seed) klonlar savaşa giremez (uygun_klon).
993    d.aramaSonuc = (Array.isArray(rows) ? rows : []).filter(a =>
994      a && Number(a.owner_user_id) !== me && !a.seed_source && !Number(a.seed_unofficial || 0));
995    if (!d.aramaSonuc.length) { kap.innerHTML = `<div class="os-arama-ipucu">${escapeHtml(T('ui.oyun.savas.klon_bulunamadi', 'Uygun klon bulunamadı'))}</div>`; return; }
996    kap.innerHTML = d.aramaSonuc.map(a => `<button type="button" class="os-arama-satir" data-klon-id="${escapeHtml(String(a.id))}">
997        ${klonAvatarHtml(a, 28)}
998        <span class="os-arama-metin"><span class="os-arama-ad">${escapeHtml(a.name || '')}</span>
999          ${a.owner_name ? `<span class="os-arama-alt">${escapeHtml(a.owner_name)}</span>` : ''}</span>
1000      </button>`).join('');
1001    kap.querySelectorAll('[data-klon-id]').forEach(b => b.addEventListener('click', () => {
1002      const a = d.aramaSonuc.find(x => String(x.id) === b.dataset.klonId);
1003      if (!a) return;
1004      d.rakipKlon = { id: a.id, name: a.name || '', owner_name: a.owner_name || '', avatar_url: a.avatar_url || '' };
1005      rakipCiz(); dugmeGuncelle();
1006      formatlariYukle();
1007    }));
1008  }
1009
1010  // --- herkese açık (satış düellosu hep kapalı)
1011  function acikCiz() {
1012    const kap = el('[data-acik]');
1013    const satis = d.format === 'sales';
1014    kap.innerHTML = `<label class="os-anahtar ${satis ? 'kapali' : ''}">
1015      <input type="checkbox" data-acik-kutu ${d.is_public && !satis ? 'checked' : ''} ${satis ? 'disabled' : ''}>
1016      <span class="os-anahtar-metin">
1017        <span class="os-anahtar-ad">${escapeHtml(T('ui.oyun.savas.acik', 'Herkese açık (izleyici oyu)'))}</span>
1018        <span class="os-anahtar-alt">${escapeHtml(T('ui.oyun.savas.acik_notu', 'Açık savaşlar paylaşılabilir ve izleyiciler oy verir. Satış düellosu hep kapalı.'))}</span>
1019      </span></label>`;
1020    kap.querySelector('[data-acik-kutu]').addEventListener('change', (e) => { d.is_public = !!e.target.checked; });
1021  }
1022
1023  gonder.addEventListener('click', async () => {
1024    hataGoster('');
1025    if (!gecerliMi()) return;
1026    d.calisiyor = true;
1027    kilitle(gonder, true);
1028    const govde = {
1029      format: d.format,
1030      topic: d.format === 'knowledge' ? null : (d.topic.trim() || null),
1031      assistant_a: d.assistant_a,
1032      is_public: !!d.is_public && d.format !== 'sales',
1033    };
1034    if (d.rakipTur === 'klon') govde.assistant_b = Number(d.rakipKlon.id);
1035    else govde.rakip_user_id = Number(d.rakip.id);
1036    try {
1037      const s = await oyunApi.post('/savas', govde);
1038      kapat();
1039      toast(s && s.status === 'running'
1040        ? T('ui.oyun.savas.basladi', 'Savaş başladı; turlar işleniyor.')
1041        : T('ui.oyun.savas.davet_gonderildi', 'Davet gönderildi; karşı sahip kabul edince başlar.'), 'success');
1042      if (typeof onBaslatildi === 'function' && s && s.id) onBaslatildi(s);
1043    } catch (e) {
1044      d.calisiyor = false;
1045      kilitle(gonder, false);
1046      hataGoster(hataMetni(e));
1047    }
1048  });
1049
1050  // İlk yükleme: klonlarım + oyun tercihi (varsayılan klon) paralel; sonra formatlar.
1051  klonCiz(); formatlarCiz(); konuCiz(); rakipCiz(); acikCiz();
1052  (async () => {
1053    const [klonlar, tercih] = await Promise.all([
1054      klonlarimiYukle(),
1055      oyunApi.get('/tercih', { sessiz: true }).catch(() => null),
1056    ]);
1057    if (!ov.isConnected) return;
1058    d.klonlar = klonlar;
1059    // Varsayılan: oyun tercihindeki klon (GET /tercih → assistant_id), yoksa ilki.
1060    // klon_kind'e göre seçim YOK: scope=mine yanıtında o alan gelmiyor.
1061    const tercihId = tercih && tercih.assistant_id ? Number(tercih.assistant_id) : null;
1062    const sec = klonlar.find(a => Number(a.id) === tercihId) || klonlar[0];
1063    d.assistant_a = sec ? Number(sec.id) : null;
1064    d.yukleniyor = false;
1065    klonCiz();
1066    if (d.assistant_a) await formatlariYukle();
1067    else dugmeGuncelle();
1068  })();
1069}
1070
1071// ---------------------------------------------------------------------- stil
1072
1073function stilleriEkle() {
1074  if (document.getElementById('oyunSavasStyles')) return;
1075  const s = document.createElement('style');
1076  s.id = 'oyunSavasStyles';
1077  s.textContent = `
1078  .os-host, .os-overlay { --os-radius: 10px; }
1079  .os-host { padding: 24px 28px 56px; max-width: 900px; color: var(--text, #2A1E35); }
1080  .os-head { display:flex; align-items:flex-start; gap:12px; margin-bottom:10px; }
1081  .os-geri { flex:none; width:34px; height:34px; border-radius:50%; border:1px solid var(--border,#E6DFEC); background:var(--surface,#fff); color:var(--text-secondary,#6F6280); cursor:pointer; display:inline-flex; align-items:center; justify-content:center; }
1082  .os-geri:hover { color:var(--brand,#6B2387); border-color:var(--brand,#6B2387); }
1083  .os-head-metin { flex:1; min-width:0; }
1084  .os-head-eylem { display:flex; gap:6px; align-items:center; flex:none; }
1085  .os-title { font-family:var(--serif,'Fraunces',serif); font-size:24px; margin:2px 0 2px; color:var(--text,#2A1E35); overflow-wrap:anywhere; }
1086  .os-sub { margin:0; font-size:13px; color:var(--text-tertiary,#A89DB5); max-width:64ch; }
1087  .os-bolum { font-size:14px; font-weight:700; margin:20px 0 8px; color:var(--text,#2A1E35); }
1088  .os-bolum-kucuk { font-size:12px; font-weight:700; color:var(--text-secondary,#6F6280); margin-bottom:6px; text-transform:uppercase; letter-spacing:.03em; }
1089  .os-not { margin:4px 0 8px; font-size:12px; color:var(--text-tertiary,#A89DB5); line-height:1.45; }
1090  .os-uyari { background:var(--warning-soft,rgba(214,150,30,.12)); color:var(--text,#2A1E35); border-radius:var(--os-radius); padding:8px 12px; font-size:12px; margin:0 0 10px; }
1091  .os-bilgi-not { display:flex; align-items:center; gap:6px; color:var(--success,#2A9D7C); font-size:12.5px; font-weight:600; margin:0 0 10px; }
1092  .os-yukleniyor { display:flex; align-items:center; gap:10px; padding:24px 0; color:var(--text-tertiary,#A89DB5); font-size:13px; }
1093  .os-spinner { width:16px; height:16px; border-radius:50%; border:2px solid var(--border,#E6DFEC); border-top-color:var(--brand,#6B2387); animation:os-don .8s linear infinite; display:inline-block; flex:none; }
1094  @keyframes os-don { to { transform:rotate(360deg); } }
1095  .os-hata { display:flex; align-items:center; gap:12px; flex-wrap:wrap; padding:12px 14px; border-radius:var(--os-radius); background:var(--danger-soft,rgba(184,56,56,.1)); color:var(--text,#2A1E35); font-size:13px; }
1096  .os-soluk { opacity:.55; }
1097
1098  .os-btn { border:none; border-radius:9px; padding:9px 15px; font-size:13px; font-weight:600; font-family:inherit; cursor:pointer; display:inline-flex; align-items:center; gap:6px; position:relative; }
1099  .os-btn:disabled { opacity:.5; cursor:default; }
1100  .os-btn-sm { padding:6px 11px; font-size:12px; }
1101  .os-btn-primary { background:var(--brand,#6B2387); color:#fff; }
1102  .os-btn-ghost { background:var(--accent-soft,rgba(107,35,135,.09)); color:var(--brand,#6B2387); }
1103  .os-btn-danger { background:var(--danger,#B83838); color:#fff; }
1104  .os-btn-kotu { color:var(--danger,#B83838); background:var(--danger-soft,rgba(184,56,56,.1)); }
1105  .os-btn-a { flex:1; justify-content:center; background:var(--accent-soft,rgba(107,35,135,.1)); color:var(--brand,#6B2387); border:1px solid var(--brand,#6B2387); }
1106  .os-btn-b { flex:1; justify-content:center; background:var(--soft-info-bg,rgba(46,110,200,.1)); color:var(--soft-info-fg,#25579E); border:1px solid var(--soft-info-fg,#25579E); }
1107  .os-btn.os-busy { color:transparent !important; }
1108  .os-btn.os-busy::after { content:''; position:absolute; left:50%; top:50%; width:14px; height:14px; margin:-7px 0 0 -7px; border-radius:50%; border:2px solid rgba(255,255,255,.4); border-top-color:#fff; animation:os-don .8s linear infinite; }
1109  .os-btn-ghost.os-busy::after, .os-btn-kotu.os-busy::after, .os-btn-a.os-busy::after, .os-btn-b.os-busy::after { border-color:var(--accent-soft,rgba(107,35,135,.2)); border-top-color:var(--brand,#6B2387); }
1110
1111  .os-sekmeler { display:flex; gap:4px; background:var(--surface-2,#F4F0F7); border-radius:10px; padding:3px; margin-bottom:10px; }
1112  .os-sekmeler-kucuk { display:inline-flex; margin-bottom:8px; }
1113  .os-sekme { flex:1; border:none; background:transparent; border-radius:8px; padding:7px 10px; font-size:12px; font-weight:600; color:var(--text-secondary,#6F6280); cursor:pointer; font-family:inherit; white-space:nowrap; }
1114  .os-sekme.on { background:var(--surface,#fff); color:var(--brand,#6B2387); box-shadow:0 1px 3px rgba(0,0,0,.08); }
1115
1116  .os-rozet { display:inline-flex; align-items:center; gap:4px; border-radius:999px; padding:2px 8px; font-size:11px; font-weight:600; white-space:nowrap; }
1117  .os-rozet-soft { background:var(--surface-2,#F4F0F7); color:var(--text-secondary,#6F6280); }
1118  .os-rozet-ok { background:var(--success-soft,rgba(42,157,124,.14)); color:var(--success,#2A9D7C); }
1119  .os-rozet-kotu { background:var(--danger-soft,rgba(184,56,56,.12)); color:var(--danger,#B83838); }
1120  .os-rozet-bekle { background:var(--warning-soft,rgba(214,150,30,.16)); color:#9A6A0E; }
1121  .os-rozet-bilgi { background:var(--accent-soft,rgba(107,35,135,.1)); color:var(--brand,#6B2387); }
1122  .os-rozet-oy { background:var(--soft-info-bg,rgba(46,110,200,.12)); color:var(--soft-info-fg,#25579E); }
1123  .os-rozet-a { background:var(--accent-soft,rgba(107,35,135,.1)); color:var(--brand,#6B2387); }
1124  .os-rozet-b { background:var(--soft-info-bg,rgba(46,110,200,.12)); color:var(--soft-info-fg,#25579E); }
1125  .os-rozet-dolu.os-rozet-oy { background:var(--soft-info-fg,#25579E); color:#fff; }
1126  .os-rozet-dolu.os-rozet-ok { background:var(--success,#2A9D7C); color:#fff; }
1127  .os-cipler { display:flex; gap:5px; flex-wrap:wrap; align-items:center; }
1128  .os-cipler-bas { margin:0 0 12px; }
1129
1130  .os-liste { display:flex; flex-direction:column; gap:6px; }
1131  .os-satir { display:flex; align-items:flex-start; gap:10px; padding:10px 12px; border-radius:var(--os-radius); background:var(--surface,#fff); border:1px solid var(--border,#E6DFEC); cursor:pointer; text-align:left; }
1132  .os-satir:hover, .os-satir:focus-visible { border-color:var(--brand,#6B2387); outline:none; }
1133  .os-satir-ikon { flex:none; width:32px; height:32px; border-radius:10px; display:inline-flex; align-items:center; justify-content:center; }
1134  .os-satir-metin { flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; }
1135  .os-satir-konu { font-size:13.5px; font-weight:600; color:var(--text,#2A1E35); overflow-wrap:anywhere; }
1136  .os-satir-taraflar { font-size:12.5px; color:var(--text-secondary,#6F6280); }
1137  .os-vs { font-size:10.5px; font-weight:700; color:var(--text-tertiary,#A89DB5); text-transform:uppercase; margin:0 4px; }
1138  .os-satir-alt { font-size:11.5px; color:var(--text-tertiary,#A89DB5); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1139  .os-satir-eylem { display:flex; gap:6px; margin-top:6px; }
1140  .os-satir-ok { flex:none; color:var(--text-tertiary,#A89DB5); align-self:center; }
1141
1142  .os-serit { display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:thin; }
1143  .os-oy-kart { flex:none; width:240px; display:flex; flex-direction:column; gap:6px; padding:12px; border-radius:var(--os-radius); background:var(--surface,#fff); border:1px solid var(--border,#E6DFEC); cursor:pointer; }
1144  .os-oy-kart:hover, .os-oy-kart:focus-visible { border-color:var(--brand,#6B2387); outline:none; }
1145  .os-oy-kart-konu { font-size:13px; font-weight:600; color:var(--text,#2A1E35); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
1146  .os-oy-kart-taraflar { display:flex; align-items:center; gap:5px; font-size:12px; color:var(--text-secondary,#6F6280); min-width:0; }
1147  .os-oy-kart-taraflar > span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1148  .os-oy-kart-alt { display:flex; align-items:center; gap:6px; justify-content:space-between; }
1149  .os-oy-kart-sure { font-size:11px; color:var(--text-tertiary,#A89DB5); }
1150  .os-oy-kart .os-btn { align-self:flex-start; margin-top:2px; }
1151
1152  .os-avatar { flex:none; display:inline-flex; align-items:center; justify-content:center; border-radius:50%; overflow:hidden; background:var(--accent-soft,rgba(107,35,135,.12)); color:var(--brand,#6B2387); font-weight:700; }
1153  .os-avatar img { width:100%; height:100%; object-fit:cover; display:block; }
1154
1155  .os-bos { display:flex; flex-direction:column; align-items:center; gap:6px; padding:28px 12px; color:var(--text-tertiary,#A89DB5); text-align:center; border:1px dashed var(--border,#E6DFEC); border-radius:var(--os-radius); }
1156  .os-bos-baslik { font-size:13px; font-weight:600; color:var(--text-secondary,#6F6280); }
1157  .os-bos-alt { font-size:12px; max-width:48ch; }
1158
1159  .os-kart { padding:14px 16px; border-radius:var(--os-radius); background:var(--surface,#fff); border:1px solid var(--border,#E6DFEC); margin:10px 0; }
1160  .os-kart-baslik { font-size:14px; font-weight:700; margin-bottom:4px; color:var(--text,#2A1E35); }
1161  .os-kart-eylem { border-color:var(--brand,#6B2387); }
1162  .os-eylemler { display:flex; gap:8px; justify-content:flex-end; flex-wrap:wrap; margin-top:6px; }
1163  .os-onay { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:10px; padding:8px 10px; border-radius:8px; background:var(--surface-2,#F4F0F7); border:1px solid var(--border,#E6DFEC); font-size:12.5px; color:var(--text,#2A1E35); }
1164  .os-onay-metin { flex:1; min-width:120px; }
1165
1166  .os-taraflar { display:flex; align-items:flex-start; gap:8px; }
1167  .os-taraf { flex:1; min-width:0; display:flex; flex-direction:column; align-items:center; gap:5px; text-align:center; }
1168  .os-taraf-ad { font-size:14px; font-weight:700; color:var(--text,#2A1E35); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100%; }
1169  .os-taraf-sahip { font-size:11.5px; color:var(--text-tertiary,#A89DB5); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100%; }
1170  .os-taraflar-orta { flex:none; display:flex; flex-direction:column; align-items:center; gap:6px; padding-top:14px; color:var(--warning,#D6961E); }
1171
1172  .os-dokum { display:flex; flex-direction:column; gap:8px; }
1173  .os-dokum-bos { display:flex; align-items:center; gap:8px; color:var(--text-tertiary,#A89DB5); font-size:13px; }
1174  .os-dokum-devam { display:flex; align-items:center; gap:8px; color:var(--text-tertiary,#A89DB5); font-size:12px; padding:4px 0; }
1175  .os-balon-satir { display:flex; }
1176  .os-balon-satir.a { justify-content:flex-start; padding-right:40px; }
1177  .os-balon-satir.b { justify-content:flex-end; padding-left:40px; }
1178  .os-balon { max-width:100%; padding:10px 12px; border-radius:var(--os-radius); background:var(--accent-soft,rgba(107,35,135,.08)); }
1179  .os-balon-satir.b .os-balon { background:var(--soft-info-bg,rgba(46,110,200,.1)); }
1180  .os-balon-ust { display:flex; align-items:center; gap:6px; margin-bottom:3px; }
1181  .os-balon-ad { font-size:11.5px; font-weight:700; color:var(--brand,#6B2387); }
1182  .os-balon-satir.b .os-balon-ad { color:var(--soft-info-fg,#25579E); }
1183  .os-balon-tur { font-size:11px; color:var(--text-tertiary,#A89DB5); }
1184  .os-balon-soru { font-size:12px; color:var(--text-secondary,#6F6280); margin-bottom:4px; }
1185  .os-balon-metin { font-size:13.5px; line-height:1.5; color:var(--text,#2A1E35); white-space:pre-wrap; overflow-wrap:anywhere; }
1186
1187  .os-oran { margin:6px 0 2px; }
1188  .os-oran-ust { display:flex; justify-content:space-between; gap:8px; font-size:12.5px; color:var(--text-secondary,#6F6280); margin-bottom:5px; }
1189  .os-oran-a b { color:var(--brand,#6B2387); font-size:15px; }
1190  .os-oran-b b { color:var(--soft-info-fg,#25579E); font-size:15px; }
1191  .os-oran-cubuk { display:flex; height:10px; border-radius:999px; overflow:hidden; background:var(--surface-2,#F4F0F7); }
1192  .os-oran-dolu-a { background:var(--brand,#6B2387); transition:width .3s; }
1193  .os-oran-dolu-b { background:var(--soft-info-fg,#25579E); transition:width .3s; }
1194  .os-hakem-neden { margin:8px 0 0; font-size:13px; color:var(--text-secondary,#6F6280); line-height:1.45; }
1195  .os-oy-dugmeler { display:flex; gap:10px; margin-top:6px; }
1196  .os-oy-verildi { display:flex; align-items:center; gap:6px; color:var(--success,#2A9D7C); font-size:13px; font-weight:600; }
1197
1198  .os-paylas-satir { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
1199  .os-paylas-satir input { flex:1; min-width:180px; border:1px solid var(--border,#E6DFEC); border-radius:8px; padding:8px 10px; font-size:12.5px; font-family:inherit; background:var(--surface-2,#F4F0F7); color:var(--text,#2A1E35); }
1200
1201  .os-overlay { position:fixed; inset:0; background:rgba(20,10,30,.45); z-index:300; display:flex; align-items:center; justify-content:center; padding:16px; }
1202  .os-modal { width:min(560px, 100%); max-height:90vh; overflow:auto; background:var(--surface,#fff); color:var(--text,#2A1E35); border-radius:14px; box-shadow:0 18px 50px rgba(0,0,0,.25); display:flex; flex-direction:column; }
1203  .os-modal-head { display:flex; align-items:center; justify-content:space-between; padding:14px 16px 8px; }
1204  .os-modal-title { font-size:16px; font-weight:700; }
1205  .os-modal-x { border:none; background:transparent; color:var(--text-secondary,#6F6280); cursor:pointer; display:inline-flex; padding:4px; }
1206  .os-modal-govde { padding:0 16px 8px; display:flex; flex-direction:column; gap:14px; }
1207  .os-modal-alt { display:flex; justify-content:flex-end; gap:8px; padding:10px 16px 16px; }
1208  .os-form-bolum:empty { display:none; }
1209  .os-form-hata { padding:8px 10px; border-radius:8px; background:var(--danger-soft,rgba(184,56,56,.1)); color:var(--danger,#B83838); font-size:12px; }
1210  .os-alan { display:flex; flex-direction:column; gap:4px; font-size:12px; color:var(--text-secondary,#6F6280); }
1211  .os-alan select, .os-alan textarea { border:1px solid var(--border,#E6DFEC); border-radius:8px; padding:8px 10px; font-size:13px; font-family:inherit; background:var(--surface-2,#F4F0F7); color:var(--text,#2A1E35); width:100%; box-sizing:border-box; resize:vertical; }
1212  .os-alan select:focus, .os-alan textarea:focus { outline:none; border-color:var(--brand,#6B2387); }
1213  .os-secenekler { display:flex; flex-direction:column; gap:6px; }
1214  .os-secenek { display:flex; align-items:flex-start; gap:10px; padding:9px 10px; border-radius:var(--os-radius); border:1px solid var(--border,#E6DFEC); background:var(--surface,#fff); cursor:pointer; text-align:left; font-family:inherit; color:var(--text,#2A1E35); }
1215  .os-secenek.on { border-color:var(--brand,#6B2387); background:var(--accent-soft,rgba(107,35,135,.06)); }
1216  .os-secenek.kapali { opacity:.6; cursor:default; }
1217  .os-secenek-nokta { flex:none; width:14px; height:14px; border-radius:50%; border:2px solid var(--border-strong,#C9BFD3); margin-top:2px; }
1218  .os-secenek.on .os-secenek-nokta { border-color:var(--brand,#6B2387); background:var(--brand,#6B2387); box-shadow:inset 0 0 0 2.5px var(--surface,#fff); }
1219  .os-secenek-metin { display:flex; flex-direction:column; gap:2px; min-width:0; }
1220  .os-secenek-ad { font-size:13px; font-weight:600; }
1221  .os-secenek-alt { font-size:11.5px; color:var(--text-tertiary,#A89DB5); line-height:1.4; }
1222  .os-secenek-neden { font-size:11.5px; color:var(--warning,#D6961E); }
1223  .os-secili { display:flex; align-items:center; gap:8px; padding:6px 8px; border-radius:999px; background:var(--accent-soft,rgba(107,35,135,.1)); color:var(--brand,#6B2387); font-size:13px; font-weight:600; width:fit-content; max-width:100%; }
1224  .os-secili-ikon { display:inline-flex; }
1225  .os-secili-ad { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1226  .os-secili-alt { font-weight:500; color:var(--text-secondary,#6F6280); }
1227  .os-secili-yok { display:flex; align-items:center; gap:10px; flex-wrap:wrap; font-size:12px; color:var(--text-tertiary,#A89DB5); }
1228  .os-cip-x { border:none; background:transparent; color:inherit; cursor:pointer; display:inline-flex; padding:2px; }
1229  .os-arama { display:flex; align-items:center; gap:8px; border:1px solid var(--border,#E6DFEC); border-radius:8px; padding:0 10px; background:var(--surface-2,#F4F0F7); color:var(--text-tertiary,#A89DB5); }
1230  .os-arama input { flex:1; border:none; background:transparent; padding:8px 0; font-size:13px; font-family:inherit; color:var(--text,#2A1E35); outline:none; min-width:0; }
1231  .os-arama-sonuc { display:flex; flex-direction:column; gap:4px; margin-top:6px; max-height:220px; overflow:auto; }
1232  .os-arama-ipucu { font-size:12px; color:var(--text-tertiary,#A89DB5); padding:6px 2px; }
1233  .os-arama-satir { display:flex; align-items:center; gap:8px; padding:6px 8px; border-radius:8px; border:1px solid var(--border,#E6DFEC); background:var(--surface,#fff); cursor:pointer; text-align:left; font-family:inherit; color:var(--text,#2A1E35); }
1234  .os-arama-satir:hover { border-color:var(--brand,#6B2387); }
1235  .os-arama-metin { display:flex; flex-direction:column; min-width:0; }
1236  .os-arama-ad { font-size:13px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1237  .os-arama-alt { font-size:11.5px; color:var(--text-tertiary,#A89DB5); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1238  .os-anahtar { display:flex; align-items:flex-start; gap:10px; cursor:pointer; }
1239  .os-anahtar.kapali { opacity:.6; cursor:default; }
1240  .os-anahtar input { margin-top:3px; accent-color:var(--brand,#6B2387); }
1241  .os-anahtar-metin { display:flex; flex-direction:column; gap:2px; }
1242  .os-anahtar-ad { font-size:13px; font-weight:600; color:var(--text,#2A1E35); }
1243  .os-anahtar-alt { font-size:11.5px; color:var(--text-tertiary,#A89DB5); line-height:1.4; }
1244
1245  @media (max-width: 640px) {
1246    .os-host { padding:16px 14px 48px; }
1247    .os-balon-satir.a { padding-right:16px; }
1248    .os-balon-satir.b { padding-left:16px; }
1249    .os-taraflar-orta { padding-top:8px; }
1250    .os-oy-dugmeler { flex-direction:column; }
1251  }
1252  `;
1253  document.head.appendChild(s);
1254}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.