GoogleGPT 🤖

Додає відповіді штучного інтелекту в Google Search (на базі Google Gemma + GPT-4o!)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name                     GoogleGPT 🤖
// @name:zh-CN               GoogleGPT 🤖
// @description              Add AI answers to Google Search (powered by Google Gemma + GPT-4o!)
// @description:af           Voeg AI-antwoorde by Google Search (aangedryf deur Google Gemma + GPT-4o!)
// @description:am           የ Google Search ውስጥ AI መልቀቅን አድርግ፣ (Google Gemma + GPT-4o በመሣሪያዎቹ ውስጥ!)
// @description:ar           يضيف إجابات AI إلى Google Search (مدعوم بواسطة Google Gemma + GPT-4o!)
// @description:as           Google Search-লৈ AI উত্তৰ যোগ দিয়ে (Google Gemma + GPT-4o দ্বাৰা পাওৱা হৈছে!)
// @description:az           Google Search-ya AI cavablarını əlavə edir (Google Gemma + GPT-4o tərəfindən dəstəklənir!)
// @description:be           Дадае ІА адказы на Google Search (падтрымліваецца Google Gemma + GPT-4o!)
// @description:bg           Добавя ИИ отговори в Google Search (поддържан от Google Gemma + GPT-4o!)
// @description:bn           Google Search-ত AI উত্তর যোগ করে (Google Gemma + GPT-4o দ্বারা প্রচালিত!)
// @description:bs           Dodaje AI odgovore na Google Search (pokreće Google Gemma + GPT-4o!)
// @description:ca           Afegeix respostes d'IA a Google Search (impulsat per Google Gemma + GPT-4o!)
// @description:ceb          Nagdugang ug mga tubag AI ngadto sa Google Search (gipadagan sa Google Gemma + GPT-4o!)
// @description:co           Aggiunge risposte AI a Google Search (supportate da Google Gemma + GPT-4o!)
// @description:cs           Přidává AI odpovědi do Google Search (poháněno Google Gemma + GPT-4o!)
// @description:cy           Ychwanegu atebion AI i Google Search (a yrrir gan Google Gemma + GPT-4o!)
// @description:da           Tilføjer AI-svar til Google Search (drevet af Google Gemma + GPT-4o!)
// @description:de           Fügt AI-Antworten zu Google Search hinzu (betrieben von Google Gemma + GPT-4o!)
// @description:el           Προσθέτει απαντήσεις AI στο Google Search (τροφοδοτούμενο από Google Gemma + GPT-4o!)
// @description:en           Add AI answers to Google Search (powered by Google Gemma + GPT-4o!)
// @description:eo           Aldonas AI-respondojn al Google Search (ebligita de Google Gemma + GPT-4o!)
// @description:es           Añade respuestas de IA a Google Search (impulsado por Google Gemma + GPT-4o!)
// @description:et           Lisab AI-vastused Google Search'le (juhitud Google Gemma + GPT-4o-ga!)
// @description:eu           Gehitu IA erantzunak Google Search-n (Google Gemma + GPT-4o-k bultzatuta!)
// @description:fa           پاسخهای هوشمصنوعی به Google Search اضافه میشود (توسط Google Gemma + GPT-4o پشتیبانی میشود!)
// @description:fi           Lisää tekoälyvastauksia Google Search:hun (ohjattu Google Gemma + GPT-4o:lla!)
// @description:fil          Nagdaragdag ng mga sagot ng AI sa Google Search (pinapagana ng Google Gemma + GPT-4o!)
// @description:fo           Bætir AI svar við Google Search (drifin af Google Gemma + GPT-4o!)
// @description:fr           Ajoute des réponses IA à Google Search (propulsé par Google Gemma + GPT-4o!)
// @description:fr-CA        Ajoute des réponses IA à Google Search (propulsé par Google Gemma + GPT-4o!)
// @description:fy           Foeget AI-antwurden ta oan Google Search (dreaun troch Google Gemma + GPT-4o!)
// @description:ga           Cuirtear freagraí AI le Google Search (dírítear ag Google Gemma + GPT-4o!)
// @description:gd           Cur freagairtichean AI ris an Google Search (air a thug seachad le Google Gemma + GPT-4o!)
// @description:gl           Engade respostas de IA a Google Search (impulsado por Google Gemma + GPT-4o!)
// @description:gu           Google Search માટે AI જવાબો ઉમેરે છે (Google Gemma + GPT-4o દ્વારા પોવરેડ!)
// @description:ha           Ƙaddara takardun AI zu Google Search (da aka fi Google Gemma + GPT-4o!)
// @description:haw          Hoʻohui aku i nā hoʻopiʻi AI iā Google Search (hoʻohui ʻia e Google Gemma + GPT-4o!)
// @description:he           מוסיף תשובות AI ל-Google Search (מופעל על ידי Google Gemma + GPT-4o!)
// @description:hi           Google Search में AI उत्तर जोड़ता है (Google Gemma + GPT-4o द्वारा संचालित!)
// @description:hmn          Ntxig AI nruab nruab rau Google Search (pab cuam Google Gemma + GPT-4o!)
// @description:hr           Dodaje AI odgovore na Google Search (pokreće Google Gemma + GPT-4o!)
// @description:ht           Ajoute repons AI nan Google Search (pòte pa Google Gemma + GPT-4o!)
// @description:hu           AI válaszokat ad hozzá a Google Search-hoz (Google Gemma + GPT-4o által hajtva!)
// @description:hy           Ավելացնում է AI պատասխաններ Google Search-ում (աջակցված է Google Gemma + GPT-4o-ով!)
// @description:ia           Adde responas AI a Google Search (propulsate per Google Gemma + GPT-4o!)
// @description:id           Menambahkan jawaban AI ke Google Search (didukung oleh Google Gemma + GPT-4o!)
// @description:ig           Tinye ihe ndekọ AI n'ụzọ ọgụgụ Google Search (n'efu na Google Gemma + GPT-4o!)
// @description:ii           Google Search ᐸᔦᒪᔪᐃᓃᑦ AI ᓇᑕᐅᒪᐃᑦᓯ (Google Gemma + GPT-4o ᓂᑕᔪᑦᓯᐏᑦᑕᒥᔭ!)
// @description:is           Bætir AI svar við Google Search (keyrir á Google Gemma + GPT-4o!)
// @description:it           Aggiunge risposte AI a Google Search (alimentato da Google Gemma + GPT-4o!)
// @description:iu           Google Search ᑲᑎᒪᔪᖅᑐᖅᑐᐃᓐᓇᓂᒃ AI ᑎᑎᕋᖃᕐᓯᒪᓂᖏᓐ (Google Gemma + GPT-4o ᑐᑭᒧᑦᑖᑦ!)
// @description:ja           Google Search に AI 回答を追加します (Google Gemma + GPT-4o で動作!)
// @description:jv           Nambéhi pirangga AI nganti Google Search (diduweni déning Google Gemma + GPT-4o!)
// @description:ka           ამატებს AI პასუხებს Google Search-ს (იმართება Google Gemma + GPT-4o!)
// @description:kk           Google Search-ға AI жауаптарын қосады (Google Gemma + GPT-4o арқылы жұмыс істейді!)
// @description:kl           Google Search-mi AI-t Kalaallit Nunaanni iluani (Google Gemma + GPT-4o! -nip ilaanni!)
// @description:km           បន្ថែមចម្លើយ AI ទៅ Google Search (ដំណើរការដោយ Google Gemma + GPT-4o!)
// @description:kn           Google Search ಗೆ AI ಉತ್ತರಗಳನ್ನು ಸೇರಿಸುತ್ತದೆ (Google Gemma + GPT-4o ನಿಂದ ನಡೆಸಲ್ಪಡುತ್ತಿದೆ!)
// @description:ko           Google Search에 AI 답변을 추가합니다(Google Gemma + GPT-4o 제공!)
// @description:ku           Bersivên AI-ê li Google Search zêde dike (ji hêla Google Gemma + GPT-4o ve hatî hêzdar kirin!)
// @description:ky           Google Search'го AI жоопторун кошот (Google Gemma + GPT-4o тарабынан иштейт!)
// @description:la           Addit AI responsa Google Search (powered per Google Gemma + GPT-4o!)
// @description:lb           Füügt AI Äntwerten op Google Search (ugedriwwen duerch Google Gemma + GPT-4o!)
// @description:lg           Yambula emisomo ey'ensobi ku Google Search (enkuuma Google Gemma + GPT-4o!)
// @description:ln           Ebakisi biyano ya AI na Google Search (ezali na nguya ya Google Gemma + GPT-4o!)
// @description:lo           ເພີ່ມຄໍາຕອບ AI ໃຫ້ກັບ Google Search (ຂັບເຄື່ອນໂດຍ Google Gemma + GPT-4o!)
// @description:lt           Prideda AI atsakymus į „Google Search“ (maitina Google Gemma + GPT-4o!)
// @description:lv           Pievieno AI atbildes Google Search (darbina Google Gemma + GPT-4o!)
// @description:mg           Manampy valiny AI amin'ny Google Search (nampiasain'ny Google Gemma + GPT-4o!)
// @description:mi           Ka taapirihia nga whakautu AI ki a Google Search (whakamahia e Google Gemma + GPT-4o!)
// @description:mk           Додава одговори со вештачка интелигенција на Google Search (напојувано од Google Gemma + GPT-4o!)
// @description:ml           Google Search-യിലേക്ക് AI ഉത്തരങ്ങൾ ചേർക്കുന്നു (Google Gemma + GPT-4o നൽകുന്നതാണ്!)
// @description:mn           Google Search-д AI хариултуудыг нэмдэг (Google Gemma + GPT-4o-оор ажилладаг!)
// @description:mr           Google Search ला AI उत्तरे जोडते (Google Gemma + GPT-4o द्वारे समर्थित!)
// @description:ms           Menambahkan jawapan AI pada Google Search (dikuasakan oleh Google Gemma + GPT-4o!)
// @description:mt           Iżżid it-tweġibiet AI għal Google Search (mħaddma minn Google Gemma + GPT-4o!)
// @description:my           Google Search (Google Gemma + GPT-4o ဖြင့် စွမ်းဆောင်ထားသည့်) တွင် AI အဖြေများကို ပေါင်းထည့်သည်
// @description:na           Aeta AI teroma i Google Search (ira Google Gemma + GPT-4o reke akea!)
// @description:nb           Legger til AI-svar på Google Search (drevet av Google Gemma + GPT-4o!)
// @description:nd           Iyatholakala amaswelelo e-AI kuGoogle Search (kuyatholakala ngokulawula uGoogle Gemma + GPT-4o!)
// @description:ne           Google Search मा AI जवाफहरू थप्छ (Google Gemma + GPT-4o द्वारा संचालित!)
// @description:ng           Ondjova mbelelo dha AI moGoogle Search (uumbuli nguGoogle Gemma + GPT-4o!)
// @description:nl           Voegt AI-antwoorden toe aan Google Search (mogelijk gemaakt door Google Gemma + GPT-4o!)
// @description:nn           Legg til AI-svar på Google Search (drevet av Google Gemma + GPT-4o!)
// @description:no           Legger til AI-svar til Google Search (drevet av Google Gemma + GPT-4o!)
// @description:nso          Ya go etela ditshenyegi tsa AI mo Google Search (e dirwang ke Google Gemma + GPT-4o!)
// @description:ny           Imawonjezera mayankho a AI ku Google Search (yoyendetsedwa ndi Google Gemma + GPT-4o!)
// @description:oc           Ajusta de respòstas d'IA a Google Search (amb Google Gemma + GPT-4o!)
// @description:om           Deebii AI Google Search (Google Gemma + GPT-4o'n kan hojjetu!) irratti dabalata.
// @description:or           Google Search କୁ AI ଉତ୍ତର ଯୋଗ କରେ (Google Gemma + GPT-4o ଦ୍ୱାରା ଚାଳିତ!)
// @description:pa           Google Search (Google Gemma + GPT-4o ਦੁਆਰਾ ਸੰਚਾਲਿਤ!) ਵਿੱਚ AI ਜਵਾਬ ਸ਼ਾਮਲ ਕਰਦਾ ਹੈ
// @description:pl           Dodaje odpowiedzi AI do Google Search (obsługiwane przez Google Gemma + GPT-4o!)
// @description:ps           Google Search ته د AI ځوابونه اضافه کوي (د Google Gemma + GPT-4o لخوا پرمخ وړل کیږي!)
// @description:pt           Adiciona respostas de IA ao Google Search (desenvolvido por Google Gemma + GPT-4o!)
// @description:pt-BR        Adiciona respostas de IA ao Google Search (desenvolvido por Google Gemma + GPT-4o!)
// @description:qu           Google Search (Google Gemma + GPT-4o nisqawan kallpachasqa!) nisqaman AI kutichiykunata yapan.
// @description:rm           Agiuntescha respostas d'IA a Google Search (propulsà da Google Gemma + GPT-4o!)
// @description:rn           Abafasha inyandiko z'IA ku Google Search (yashyizweho na Google Gemma + GPT-4o!)
// @description:ro           Adaugă răspunsuri AI la Google Search (alimentat de Google Gemma + GPT-4o!)
// @description:ru           Добавляет ответы ИИ в Google Search (на базе Google Gemma + GPT-4o!)
// @description:rw           Ongeraho ibisubizo bya AI kuri Google Search (ikoreshwa na Google Gemma + GPT-4o!)
// @description:sa           Google Search (Google Gemma + GPT-4o द्वारा संचालितम्!) इत्यत्र AI उत्तराणि योजयति ।
// @description:sat          Google Search ar AI jawab khon ojantok (Google Gemma + GPT-4o! sebadha manju)
// @description:sc           Agiungit rispostas de IA a Google Search (motorizadu da Google Gemma + GPT-4o!)
// @description:sd           شامل ڪري ٿو AI جوابن کي Google Search (Google Gemma + GPT-4o پاران طاقتور!)
// @description:se           Lávdegáhtii AI vástid Google Search (Google Gemma + GPT-4o! vuosttas!)
// @description:sg           Nâ tî-kûzâ mái vêdáara AI mbi Google Search (ngâ Google Gemma + GPT-4o!)
// @description:si           Google Search වෙත AI පිළිතුරු එක් කරයි (Google Gemma + GPT-4o මගින් බලගන්වයි!)
// @description:sk           Pridáva odpovede AI do Google Search (poháňané Google Gemma + GPT-4o!)
// @description:sl           Dodaja odgovore AI v Google Search (poganja Google Gemma + GPT-4o!)
// @description:sm           Faʻaopoopo tali AI ile Google Search (faʻamalosia e Google Gemma + GPT-4o!)
// @description:sn           Inowedzera mhinduro dzeAI kuGoogle Search (inofambiswa neGoogle Gemma + GPT-4o!)
// @description:so           Waxay ku dartay jawaabaha AI Google Search (waxaa ku shaqeeya Google Gemma + GPT-4o!)
// @description:sq           Shton përgjigjet e AI në Google Search (mundësuar nga Google Gemma + GPT-4o!)
// @description:sr           Додаје АИ одговоре у Google Search (покреће ГПТ-4о!)
// @description:ss           Iphendvulela izindlela zezilungiselelo ku-Google Search (izenzakalo nge-Google Gemma + GPT-4o!)
// @description:st           E kopanetse diqoqo tsa AI ka Google Search (ka sebelisoa ke Google Gemma + GPT-4o!)
// @description:su           Nambahkeun jawaban AI kana Google Search (dikuatkeun ku Google Gemma + GPT-4o!)
// @description:sv           Lägger till AI-svar till Google Search (driven av Google Gemma + GPT-4o!)
// @description:sw           Inaongeza majibu ya AI kwa Google Search (inaendeshwa na Google Gemma + GPT-4o!)
// @description:ta           Google Search க்கு AI பதில்களைச் சேர்க்கிறது (Google Gemma + GPT-4o மூலம் இயக்கப்படுகிறது!)
// @description:te           Google Searchకి AI సమాధానాలను జోడిస్తుంది (Google Gemma + GPT-4o ద్వారా ఆధారితం!)
// @description:tg           Ба Google Search ҷавобҳои AI илова мекунад (аз ҷониби Google Gemma + GPT-4o!)
// @description:th           เพิ่มคำตอบ AI ให้กับ Google Search (ขับเคลื่อนโดย Google Gemma + GPT-4o!)
// @description:ti           ናብ Google Search (ብGoogle Gemma + GPT-4o ዝሰርሕ!) ናይ AI መልስታት ይውስኸሉ።
// @description:tk           Google Search-a AI jogaplaryny goşýar (Google Gemma + GPT-4o bilen işleýär!)
// @description:tl           Nagdadagdag ng mga sagot ng AI sa Google Search (pinapatakbo ng Google Gemma + GPT-4o!)
// @description:tn           O amogela dipotso tsa AI mo Google Search (e a nang le Google Gemma + GPT-4o!)
// @description:to           Tambisa mabizo a AI ku Google Search (mukutenga na Google Gemma + GPT-4o!)
// @description:tr           Google Search'ya yapay zeka yanıtları ekler (Google Gemma + GPT-4o tarafından desteklenmektedir!)
// @description:ts           Ku engetela tinhlamulo ta AI eka Google Search (leyi fambiwaka hi Google Gemma + GPT-4o!)
// @description:tt           Google Search'ка AI җаваплары өсти (Google Gemma + GPT-4o белән эшләнгән!)
// @description:tw           Ɔde AI mmuae ka Google Search (a Google Gemma + GPT-4o na ɛma ahoɔden!) ho.
// @description:ug           Google Search ۋەبسېتكە AI جاۋابلار قوشۇدۇ (Google Gemma + GPT-4o تەكشۈرگۈچى بىلەن!)
// @description:uk           Додає відповіді штучного інтелекту в Google Search (на базі Google Gemma + GPT-4o!)
// @description:ur           Google Search میں AI جوابات شامل کرتا ہے (Google Gemma + GPT-4o کے ذریعے تقویت یافتہ!)
// @description:uz           Google Search-ga AI javoblarini qo'shadi (Google Gemma + GPT-4o tomonidan quvvatlanadi!)
// @description:vi           Thêm câu trả lời AI vào Google Search (được cung cấp bởi Google Gemma + GPT-4o!)
// @description:xh           Yongeza iimpendulo ze-AI kwi-Google Search (ixhaswe yi-Google Gemma + GPT-4o!)
// @description:yi           לייגט אַי ענטפֿערס צו Google Search (Powered דורך Google Gemma + GPT-4o!)
// @description:yo           Ṣe afikun awọn idahun AI si Google Search (agbara nipasẹ Google Gemma + GPT-4o!)
// @description:zh           为 Google Search 添加 AI 答案(由 Google Gemma + GPT-4o 提供支持!)
// @description:zh-CN        为 Google Search 添加 AI 答案(由 Google Gemma + GPT-4o 提供支持!)
// @description:zh-HK        為 Google Search 添加 AI 答案(由 Google Gemma + GPT-4o 提供支援!)
// @description:zh-SG        为 Google Search 添加 AI 答案(由 Google Gemma + GPT-4o 提供支持!)
// @description:zh-TW        為 Google Search 添加 AI 答案(由 Google Gemma + GPT-4o 提供支援!)
// @description:zu           Yengeza izimpendulo ze-AI ku-Google Search (inikwa amandla yi-Google Gemma + GPT-4o!)
// @author                   KudoAI
// @namespace                https://kudoai.com
// @version                  2026.1.27.3
// @license                  MIT
// @icon                     data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22170.667%22%20height=%22170.667%22%3E%3Cstyle%3E:root%7B--fill:%23000%7D@media%20(prefers-color-scheme:dark)%7B:root%7B--fill:%23fff%7D%7D%3C/style%3E%3Cpath%20fill=%22var(--fill)%22%20d=%22M82.346%20159.79c-18.113-1.815-31.78-9.013-45.921-24.184C23.197%20121.416%2017.333%20106.18%2017.333%2086c0-21.982%205.984-36.245%2021.87-52.131C55.33%2017.74%2069.27%2011.867%2091.416%2011.867c17.574%200%2029.679%203.924%2044.309%2014.363l8.57%206.116-8.705%208.705-8.704%208.704-4.288-3.608c-13.91-11.704-35.932-14.167-53.085-5.939-3.4%201.631-9.833%206.601-14.297%2011.045C44.669%2061.753%2040.95%2070.811%2040.95%2086c0%2014.342%203.594%2023.555%2013.26%2033.995%2019.088%2020.618%2048.46%2022.539%2070.457%204.608l5.333-4.348%2011.333%203.844c6.234%202.114%2011.54%203.857%2011.791%203.873.252.015-2.037%203.008-5.087%206.65-6.343%207.577-20.148%2017.217-30.493%2021.295-8.764%203.454-23.358%205.06-35.198%203.873zM92%2086.333V74.667h60.648l-11.41%2011.41-11.411%2011.41-18.914.257L92%2098z%22/%3E%3C/svg%3E
// @icon64                   data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%22170.667%22%20height=%22170.667%22%3E%3Cstyle%3E:root%7B--fill:%23000%7D@media%20(prefers-color-scheme:dark)%7B:root%7B--fill:%23fff%7D%7D%3C/style%3E%3Cpath%20fill=%22var(--fill)%22%20d=%22M82.346%20159.79c-18.113-1.815-31.78-9.013-45.921-24.184C23.197%20121.416%2017.333%20106.18%2017.333%2086c0-21.982%205.984-36.245%2021.87-52.131C55.33%2017.74%2069.27%2011.867%2091.416%2011.867c17.574%200%2029.679%203.924%2044.309%2014.363l8.57%206.116-8.705%208.705-8.704%208.704-4.288-3.608c-13.91-11.704-35.932-14.167-53.085-5.939-3.4%201.631-9.833%206.601-14.297%2011.045C44.669%2061.753%2040.95%2070.811%2040.95%2086c0%2014.342%203.594%2023.555%2013.26%2033.995%2019.088%2020.618%2048.46%2022.539%2070.457%204.608l5.333-4.348%2011.333%203.844c6.234%202.114%2011.54%203.857%2011.791%203.873.252.015-2.037%203.008-5.087%206.65-6.343%207.577-20.148%2017.217-30.493%2021.295-8.764%203.454-23.358%205.06-35.198%203.873zM92%2086.333V74.667h60.648l-11.41%2011.41-11.411%2011.41-18.914.257L92%2098z%22/%3E%3C/svg%3E
// @compatible               brave
// @compatible               chrome
// @compatible               chromebeta
// @compatible               chromecanary
// @compatible               chromedev
// @compatible               edge
// @compatible               edgebeta
// @compatible               edgecanary
// @compatible               edgedev
// @compatible               fennec
// @compatible               firefox
// @compatible               firefoxbeta
// @compatible               firefoxnightly
// @compatible               ghost
// @compatible               iceraven
// @compatible               ironfox
// @compatible               lemur
// @compatible               librewolf
// @compatible               mises
// @compatible               opera after allowing userscript manager access to search page results in opera://extensions
// @compatible               operaair after allowing userscript manager access to search page results in opera://extensions
// @compatible               operagx after allowing userscript manager access to search page results in opera://extensions
// @compatible               qq
// @compatible               quetta
// @compatible               safari
// @compatible               orion
// @compatible               vivaldi
// @compatible               waterfox
// @compatible               whale
// @match                    *://*.google.com/search*
// @match                    *://*.google.ad/search*
// @match                    *://*.google.ae/search*
// @match                    *://*.google.com.af/search*
// @match                    *://*.google.com.ag/search*
// @match                    *://*.google.com.ai/search*
// @match                    *://*.google.al/search*
// @match                    *://*.google.am/search*
// @match                    *://*.google.co.ao/search*
// @match                    *://*.google.com.ar/search*
// @match                    *://*.google.as/search*
// @match                    *://*.google.at/search*
// @match                    *://*.google.com.au/search*
// @match                    *://*.google.az/search*
// @match                    *://*.google.ba/search*
// @match                    *://*.google.com.bd/search*
// @match                    *://*.google.be/search*
// @match                    *://*.google.bf/search*
// @match                    *://*.google.bg/search*
// @match                    *://*.google.com.bh/search*
// @match                    *://*.google.bi/search*
// @match                    *://*.google.bj/search*
// @match                    *://*.google.com.bn/search*
// @match                    *://*.google.com.bo/search*
// @match                    *://*.google.com.br/search*
// @match                    *://*.google.bs/search*
// @match                    *://*.google.bt/search*
// @match                    *://*.google.co.bw/search*
// @match                    *://*.google.by/search*
// @match                    *://*.google.com.bz/search*
// @match                    *://*.google.ca/search*
// @match                    *://*.google.cd/search*
// @match                    *://*.google.cf/search*
// @match                    *://*.google.cg/search*
// @match                    *://*.google.ch/search*
// @match                    *://*.google.ci/search*
// @match                    *://*.google.co.ck/search*
// @match                    *://*.google.cl/search*
// @match                    *://*.google.cm/search*
// @match                    *://*.google.cn/search*
// @match                    *://*.google.com.co/search*
// @match                    *://*.google.co.cr/search*
// @match                    *://*.google.com.cu/search*
// @match                    *://*.google.cv/search*
// @match                    *://*.google.com.cy/search*
// @match                    *://*.google.cz/search*
// @match                    *://*.google.de/search*
// @match                    *://*.google.dj/search*
// @match                    *://*.google.dk/search*
// @match                    *://*.google.dm/search*
// @match                    *://*.google.com.do/search*
// @match                    *://*.google.dz/search*
// @match                    *://*.google.com.ec/search*
// @match                    *://*.google.ee/search*
// @match                    *://*.google.com.eg/search*
// @match                    *://*.google.es/search*
// @match                    *://*.google.com.et/search*
// @match                    *://*.google.fi/search*
// @match                    *://*.google.com.fj/search*
// @match                    *://*.google.fm/search*
// @match                    *://*.google.fr/search*
// @match                    *://*.google.ga/search*
// @match                    *://*.google.ge/search*
// @match                    *://*.google.gg/search*
// @match                    *://*.google.com.gh/search*
// @match                    *://*.google.com.gi/search*
// @match                    *://*.google.gl/search*
// @match                    *://*.google.gm/search*
// @match                    *://*.google.gr/search*
// @match                    *://*.google.com.gt/search*
// @match                    *://*.google.gy/search*
// @match                    *://*.google.com.hk/search*
// @match                    *://*.google.hn/search*
// @match                    *://*.google.hr/search*
// @match                    *://*.google.ht/search*
// @match                    *://*.google.hu/search*
// @match                    *://*.google.co.id/search*
// @match                    *://*.google.ie/search*
// @match                    *://*.google.co.il/search*
// @match                    *://*.google.im/search*
// @match                    *://*.google.co.in/search*
// @match                    *://*.google.iq/search*
// @match                    *://*.google.is/search*
// @match                    *://*.google.it/search*
// @match                    *://*.google.je/search*
// @match                    *://*.google.com.jm/search*
// @match                    *://*.google.jo/search*
// @match                    *://*.google.co.jp/search*
// @match                    *://*.google.co.ke/search*
// @match                    *://*.google.com.kh/search*
// @match                    *://*.google.ki/search*
// @match                    *://*.google.kg/search*
// @match                    *://*.google.co.kr/search*
// @match                    *://*.google.com.kw/search*
// @match                    *://*.google.kz/search*
// @match                    *://*.google.la/search*
// @match                    *://*.google.com.lb/search*
// @match                    *://*.google.li/search*
// @match                    *://*.google.lk/search*
// @match                    *://*.google.co.ls/search*
// @match                    *://*.google.lt/search*
// @match                    *://*.google.lu/search*
// @match                    *://*.google.lv/search*
// @match                    *://*.google.com.ly/search*
// @match                    *://*.google.co.ma/search*
// @match                    *://*.google.md/search*
// @match                    *://*.google.me/search*
// @match                    *://*.google.mg/search*
// @match                    *://*.google.mk/search*
// @match                    *://*.google.ml/search*
// @match                    *://*.google.com.mm/search*
// @match                    *://*.google.mn/search*
// @match                    *://*.google.ms/search*
// @match                    *://*.google.com.mt/search*
// @match                    *://*.google.mu/search*
// @match                    *://*.google.mv/search*
// @match                    *://*.google.mw/search*
// @match                    *://*.google.com.mx/search*
// @match                    *://*.google.com.my/search*
// @match                    *://*.google.co.mz/search*
// @match                    *://*.google.com.na/search*
// @match                    *://*.google.com.ng/search*
// @match                    *://*.google.com.ni/search*
// @match                    *://*.google.ne/search*
// @match                    *://*.google.nl/search*
// @match                    *://*.google.no/search*
// @match                    *://*.google.com.np/search*
// @match                    *://*.google.nr/search*
// @match                    *://*.google.nu/search*
// @match                    *://*.google.co.nz/search*
// @match                    *://*.google.com.om/search*
// @match                    *://*.google.com.pa/search*
// @match                    *://*.google.com.pe/search*
// @match                    *://*.google.com.pg/search*
// @match                    *://*.google.com.ph/search*
// @match                    *://*.google.com.pk/search*
// @match                    *://*.google.pl/search*
// @match                    *://*.google.pn/search*
// @match                    *://*.google.com.pr/search*
// @match                    *://*.google.ps/search*
// @match                    *://*.google.pt/search*
// @match                    *://*.google.com.py/search*
// @match                    *://*.google.com.qa/search*
// @match                    *://*.google.ro/search*
// @match                    *://*.google.ru/search*
// @match                    *://*.google.rw/search*
// @match                    *://*.google.com.sa/search*
// @match                    *://*.google.com.sb/search*
// @match                    *://*.google.sc/search*
// @match                    *://*.google.se/search*
// @match                    *://*.google.com.sg/search*
// @match                    *://*.google.sh/search*
// @match                    *://*.google.si/search*
// @match                    *://*.google.sk/search*
// @match                    *://*.google.com.sl/search*
// @match                    *://*.google.sn/search*
// @match                    *://*.google.so/search*
// @match                    *://*.google.sm/search*
// @match                    *://*.google.sr/search*
// @match                    *://*.google.st/search*
// @match                    *://*.google.com.sv/search*
// @match                    *://*.google.td/search*
// @match                    *://*.google.tg/search*
// @match                    *://*.google.co.th/search*
// @match                    *://*.google.com.tj/search*
// @match                    *://*.google.tl/search*
// @match                    *://*.google.tm/search*
// @match                    *://*.google.tn/search*
// @match                    *://*.google.to/search*
// @match                    *://*.google.com.tr/search*
// @match                    *://*.google.tt/search*
// @match                    *://*.google.com.tw/search*
// @match                    *://*.google.co.tz/search*
// @match                    *://*.google.com.ua/search*
// @match                    *://*.google.co.ug/search*
// @match                    *://*.google.co.uk/search*
// @match                    *://*.google.com.uy/search*
// @match                    *://*.google.co.uz/search*
// @match                    *://*.google.com.vc/search*
// @match                    *://*.google.co.ve/search*
// @match                    *://*.google.vg/search*
// @match                    *://*.google.co.vi/search*
// @match                    *://*.google.com.vn/search*
// @match                    *://*.google.vu/search*
// @match                    *://*.google.ws/search*
// @match                    *://*.google.rs/search*
// @match                    *://*.google.co.za/search*
// @match                    *://*.google.co.zm/search*
// @match                    *://*.google.co.zw/search*
// @match                    *://*.google.cat/search*
// @include                  https://auth0.openai.com
// @connect                  api.binjie.fun
// @connect                  api.openai.com
// @connect                  api11.gptforlove.com
// @connect                  cdn.jsdelivr.net
// @connect                  chat-share.kudoai.workers.dev
// @connect                  chatai.mixerbox.com
// @connect                  chatgpt.com
// @connect                  fanyi.sogou.com
// @connect                  googlegpt.io
// @connect                  raw.githubusercontent.com
// @connect                  localhost
// @connect                  127.0.0.1
// @require                  https://cdn.jsdelivr.net/npm/@kudoai/[email protected]/dist/chatgpt.min.js#sha256-XyrLEk81vg4/zgOeYDWtugRQKJvrWEefACp0EfwMVHE=
// @require                  https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha256-dppVXeVTurw1ozOPNE3XqhYmDJPOosfbKQcHyQSE58w=
// @require                  https://cdn.jsdelivr.net/npm/[email protected]/dist/index.min.js#sha256-S7ltnVPzgKyAGBlBG4wQhorJqYTehj5WQCrADCKJufE=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/components/buttons.js#sha256-bam0MF6WM+Lyt5Wgop3rfHPYZHmOPXq4ZxbUXort2+M=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/components/icons.js#sha256-nAcuQD4FVFzUN1pS6pOjw2V3IvkDwYV+P3m5IN8nsdo=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@787f07b/assets/js/chatbot/components/menus.js#sha256-YaV3USVJvvChUwPjoF2jwRwbKVBdjoFfLS6ThEnZchE=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/components/replyBubble.js#sha256-zN/oMInc63biUtC+qNfP48vntQiEw2zyCIVszeBxLmg=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/components/tooltip.js#sha256-/xPw7DnS8F9dBH/s0ffMrErweHgFBeKpkUM4tUDy4vo=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@787f07b/assets/js/chatbot/lib/api.js#sha256-9mC3x8yqdVp3WpWMreBsTzunXu1+VSm0bXvVOQz3ODs=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/lib/feedback.js#sha256-ri8OzNa/8sQINDn7bW84F2OuVYZxubMSm/Zpli/cPnQ=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/lib/log.js#sha256-puXwoSKgog6EhgDzlJrAzMnGRM6kLMTT8NF0jYncIt8=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/lib/prompts.js#sha256-Z9QsKpAcqSoclxyPTkPfe8/K3s9eDrbSYgumr/GYyLc=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/lib/session.js#sha256-cH2e3l2bZQRekQHxaeSShdNguqD41evEOkMrrVIydHQ=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/lib/themes.js#sha256-NSiOkXoRC/fF8zdmnbIk9XL5tKWWP5MU2NOfdJ9G0NU=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/lib/ui.js#sha256-+UnPhc4zxrdWEuLU8PFrnpAW9TFS2c1hOGbcby2HlEU=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@30ce038/assets/js/chatbot/lib/userscript.js#sha256-DTD+Tj/9angBw8/Q4e8PMz2SBwueqvNzeY8PwZlMgbs=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/userscripts@ff2baba/assets/js/lib/css.js/dist/css.min.js#sha256-zf9s8C0cZ/i+gnaTIUxa0+RpDYpsJVlyuV5L2q4KUdA=
// @require                  https://cdn.jsdelivr.net/gh/adamlui/userscripts@ff2baba/assets/js/lib/dom.js/dist/dom.min.js#sha256-nTc2by3ZAz6AR7B8fOqjloJNETvjAepe15t2qlghMDo=
// @require                  https://cdn.jsdelivr.net/npm/[email protected]/dist/generate-ip.min.js#sha256-PI9snFGy1YvX4fiT8SJ01RveRSa6vZujEJxk8y/jvVs=
// @require                  https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js#sha256-g3pvpbDHNrUrveKythkPMF2j/J7UFoHbUyFQcFe1yEY=
// @require                  https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js#sha256-n0UwfFeU7SR6DQlfOmLlLvIhWmeyMnIDp/2RmVmuedE=
// @require                  https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js#sha256-e1fUJ6xicGd9r42DgN7SzHMzb5FJoWe44f4NbvZmBK4=
// @require                  https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js#sha256-Ffq85bZYmLMrA/XtJen4kacprUwNbYdxEKd0SqhHqJQ=
// @require                  https://unpkg.com/[email protected]/build/Tone.js#sha256-4pCVL6Q9mnp4AYKoPG/M9E15y3riy6EC7x8rnZgSTiI=
// @resource ggptIconBlack   https://cdn.jsdelivr.net/gh/KudoAI/googlegpt@9db3bda/assets/images/icons/googlegpt/black/icon64.png.b64#sha256-yiTqggYRNsWcJtyIUDzFrPqrL3yeTaPCrEGAW0QFuPM=
// @resource ggptIconWhite   https://cdn.jsdelivr.net/gh/KudoAI/googlegpt@9db3bda/assets/images/icons/googlegpt/white/icon64.png.b64#sha256-BYRq92cF5knykaKnmNi1U4CrwBC/jK1V+MGfH4NGui4=
// @resource ggptLSlogo      https://cdn.jsdelivr.net/gh/KudoAI/googlegpt@9db3bda/assets/images/logos/googlegpt/flat/black-green/logo480x64.png.b64#sha256-fzSZhLVQQolCLWYr/h29NWfR1Yl4glHv1TcsveYYv+U=
// @resource ggptDSlogo      https://cdn.jsdelivr.net/gh/KudoAI/googlegpt@9db3bda/assets/images/logos/googlegpt/flat/white-green/logo480x64.png.b64#sha256-3qRdGKhF3pojDqVVh/5kODIg7QvYbbLf4zFkEh5xoGc=
// @resource hljsCSS         https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/base16/railscasts.min.css#sha256-nMf0Oxaj3sYJiwGCsfqNpGnBbcofnzk+zz3xTxtdLEQ=
// @resource rpgCSS          https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@727feff/assets/styles/rising-particles/dist/gray.min.css#sha256-48sEWzNUGUOP04ur52G5VOfGZPSnZQfrF3szUr4VaRs=
// @resource rpwCSS          https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@727feff/assets/styles/rising-particles/dist/white.min.css#sha256-6xBXczm7yM1MZ/v0o1KVFfJGehHk47KJjq8oTktH4KE=
// @grant                    GM_getValue
// @grant                    GM_setValue
// @grant                    GM_deleteValue
// @grant                    GM_cookie
// @grant                    GM_registerMenuCommand
// @grant                    GM_unregisterMenuCommand
// @grant                    GM_getResourceText
// @grant                    GM_xmlhttpRequest
// @grant                    GM.xmlHttpRequest
// @noframes
// @homepageURL              https://www.googlegpt.io
// @supportURL               https://support.googlegpt.io
// @contributionURL          https://github.com/sponsors/KudoAI
// ==/UserScript==

// Dependencies:
// ✓ chatgpt.js (https://chatgpt.js.org) © 2023–2026 KudoAI & contributors under the MIT license
// ✓ dom.js © 2023–2026 Adam Lui under the MIT license
// ✓ generate-ip (https://generate-ip.org) © 2024–2026 Adam Lui & contributors under the MIT license
// ✓ highlight.js (https://highlightjs.org) © 2006 Ivan Sagalaev under the BSD 3-Clause license
// ✓ KaTeX (https://katex.org) © 2013–2020 Khan Academy & other contributors under the MIT license
// ✓ Marked (https://marked.js.org) © 2018+ MarkedJS © 2011–2018 Christopher Jeffrey under the MIT license
// ✓ Tone.js (https://tonejs.github.io) © 2014–2026 Yotam Mann under the MIT license

// Documentation: https://docs.googlegpt.io

(async () => {
    'use strict'

    // 用戶自定義本地端點
    const localHostEndpoint = 'http://localhost:5555/v1/chat/completions'

    // Init DATA
    window.env = {
        browser: { language: chatgpt.getUserLanguage() },
        scriptManager: {
            name: (() => { try { return GM_info.scriptHandler } catch (err) { return 'unknown' }})(),
            version: (() => { try { return GM_info.version } catch (err) { return 'unknown' }})()
        }
    } ; ['Chromium', 'Firefox', 'Chrome', 'Edge', 'Brave', 'Mobile'].forEach(platform =>
        env.browser[`is${ platform == 'Firefox' ? 'FF' : platform }`] = chatgpt.browser['is' + platform]())
    Object.assign(env.browser, { get isCompact() { return innerWidth <= 480 }})
    env.userLocale = location.hostname.endsWith('.com') ? 'us' : location.hostname.split('.').pop()
    env.scriptManager.supportsStreaming = /Tampermonkey|ScriptCat/.test(env.scriptManager.name)
    env.scriptManager.supportsTooltips = env.scriptManager.name == 'Tampermonkey'
                                      && parseInt(env.scriptManager.version.split('.')[0]) >= 5
    window.inputEvents = {} ; ['down', 'move', 'up'].forEach(action =>
        inputEvents[action] = ( window.PointerEvent ? 'pointer' : env.browser.isMobile ? 'touch' : 'mouse' ) + action)
    window.xhr = typeof GM != 'undefined' && GM.xmlHttpRequest || GM_xmlhttpRequest
    window.app = {
        version: GM_info.script.version, chatgptjsVer: /chatgpt\.js@([\d.]+)/.exec(GM_info.scriptMetaStr)[1],
        commitHashes: {
            app: '08a8bf1', // for cached <app|messages>.json
            aiweb: '02c1241' // for cached ai-chat-apis.json5 + <code-languages|katex-delimiters|sogou-tts-lang-codes>.json
        }
    }
    app.urls = { resourceHost: `https://cdn.jsdelivr.net/gh/KudoAI/googlegpt@${app.commitHashes.app}` }
    const remoteData = {
        app: await new Promise(resolve => xhr({
            method: 'GET', url: `${app.urls.resourceHost}/assets/data/app.json`,
            onload: ({ responseText }) => resolve(JSON.parse(responseText))
        })),
        msgs: await new Promise(resolve => {
            const msgHostDir = `${app.urls.resourceHost}/greasemonkey/_locales/`,
                  msgLocaleDir = `${ env.browser.language ? env.browser.language.replace('-', '_') : 'en' }/`
            let msgHref = `${ msgHostDir + msgLocaleDir }messages.json`, msgXHRtries = 0
            function fetchMsgs() { xhr({ method: 'GET', url: msgHref, onload: handleMsgs })}
            function handleMsgs(resp) {
                try { // to return localized messages.json
                    const msgs = JSON.parse(resp.responseText), flatMsgs = {}
                    for (const key in msgs)  // remove need to ref nested keys
                        if (typeof msgs[key] == 'object' && 'message' in msgs[key])
                            flatMsgs[key] = msgs[key].message
                    resolve(flatMsgs)
                } catch (err) { // if bad response
                    msgXHRtries++ ; if (msgXHRtries == 3) return resolve({}) // try original/region-stripped/EN only
                    msgHref = env.browser.language.includes('-') && msgXHRtries == 1 ? // if regional lang on 1st try...
                        msgHref.replace(/(_locales\/[^_]+)_[^_]+(\/)/, '$1$2') // ...strip region before retrying
                            : `${msgHostDir}en/messages.json` // else use default English messages
                    fetchMsgs()
                }
            }
            fetchMsgs()
        })
    }
    Object.assign(app, { ...remoteData.app, urls: { ...app.urls, ...remoteData.app.urls }, msgs: remoteData.msgs })
    app.urls.aiwebAssets = app.urls.aiwebAssets.replace('@latest', `@${app.commitHashes.aiweb}`)
    app.alerts = {
        waitingResponse:  `${app.msgs.alert_waitingFor} ${app.name} ${app.msgs.alert_response}...`,
        login:            `${app.msgs.alert_login} @ `,
        checkCloudflare:  `${app.msgs.alert_checkCloudflare} @ `,
        tooManyRequests:  `${app.msgs.alert_tooManyRequests}.`,
        parseFailed:      `${app.msgs.alert_parseFailed}.`,
        proxyNotWorking:  `${app.msgs.mode_proxy} ${app.msgs.alert_notWorking}.`,
        apiNotWorking:    `API ${app.msgs.alert_notWorking}.`,
        suggestProxy:     `${app.msgs.alert_try} ${app.msgs.alert_switchingOn} ${app.msgs.mode_proxy}`,
        suggestDiffAPI:   `${app.msgs.alert_try} ${app.msgs.alert_selectingDiff} API`,
        suggestOpenAI:    `${app.msgs.alert_try} ${app.msgs.alert_switchingOff} ${app.msgs.mode_proxy}`
    }
    app.katexDelimiters = await new Promise(resolve => xhr({ // used in show.reply()
        method: 'GET', onload: ({ responseText }) => resolve(JSON.parse(responseText)),
        url: `${app.urls.aiwebAssets}/data/katex-delimiters.json`
    }))
    window.apis = Object.assign(Object.create(null), await new Promise(resolve => xhr({
        method: 'GET', onload: ({ responseText }) => resolve(Object.fromEntries(
            Object.entries(JSON5.parse(responseText)).filter(([, api]) => !api.disabled))),
        url: `${app.urls.aiwebAssets}/data/ai-chat-apis.json5`
    })))
    apis.OpenAI = { method: 'POST', endpoint: localHostEndpoint, endpoints: { completions: localHostEndpoint } } // 確保 OpenAI 進入點存在
    apis.AIchatOS.userID = `#/chat/${Date.now()}`

    // Init SETTINGS
    app.config ??= {}
    window.settings = {
        load(...keys) {
            keys.flat().forEach(key =>
                app.config[key] = processKey(key, GM_getValue(`${app.configKeyPrefix}_${key}`, undefined)))
            function processKey(key, val) {
                const ctrl = settings.controls?.[key]
                if (val != undefined && ( // validate stored val
                        (ctrl?.type == 'toggle' && typeof val != 'boolean')
                     || (ctrl?.type == 'slider' && isNaN(parseFloat(val)))
                )) val = undefined
                return val ?? (ctrl?.defaultVal ?? (ctrl?.type == 'slider' ? 100 : false))
            }
        },
        save(key, val) { GM_setValue(`${app.configKeyPrefix}_${key}`, val) ; app.config[key] = val },
        typeIsEnabled(key) {
            const reInvertFlags = /disabled|hidden/i
            return reInvertFlags.test(key) // flag in control key name
                && !reInvertFlags.test(this.controls[key]?.label || '') // but not in label msg key name
                    ? !app.config[key] : app.config[key] // so invert since flag reps opposite type state, else don't
        }
    }
    settings.load('debugMode') ; log.debug('Initializing settings...')
    Object.assign(settings, { controls: { // displays top-to-bottom, left-to-right in Settings modal
        proxyAPIenabled: { type: 'toggle', icon: 'sunglasses', defaultVal: false,
            label: app.msgs.menuLabel_proxyAPImode,
            helptip: app.msgs.helptip_proxyAPImode },
        preferredAPI: { type: 'modal', icon: 'lightning', defaultVal: false,
            label: `${app.msgs.menuLabel_preferred} API`,
            helptip: app.msgs.helptip_preferredAPI },
        streamingDisabled: { type: 'toggle', icon: 'signalStream', defaultVal: false,
            label: app.msgs.mode_streaming,
            helptip: app.msgs.helptip_streamingMode },
        autoGet: { type: 'toggle', icon: 'speechBalloonLasso', defaultVal: true,
            label: app.msgs.menuLabel_autoAnswer,
            helptip: app.msgs.helptip_autoGetAnswers },
        autoSummarize: { type: 'toggle', icon: 'summarize', defaultVal: false,
            label: app.msgs.menuLabel_autoSummarizeResults,
            helptip: app.msgs.helptip_autoSummarizeResults },
        autoFocusChatbarDisabled: { type: 'toggle', mobile: false, icon: 'caretsInward', defaultVal: true,
            label: app.msgs.menuLabel_autoFocusChatbar,
            helptip: app.msgs.helptip_autoFocusChatbar },
        autoScroll: { type: 'toggle', mobile: false, icon: 'arrowsDown', defaultVal: false,
            label: `${app.msgs.mode_autoScroll} (${app.msgs.menuLabel_whenStreaming})`,
            helptip: app.msgs.helptip_autoScroll },
        rqDisabled: { type: 'toggle', icon: 'speechBalloons', defaultVal: false,
            label: `${app.msgs.menuLabel_show} ${app.msgs.menuLabel_relatedQueries}`,
            helptip: app.msgs.helptip_showRelatedQueries },
        prefixEnabled: { type: 'toggle', icon: 'slash', defaultVal: false,
            label: `${app.msgs.menuLabel_require} "/" ${app.msgs.menuLabel_beforeQuery}`,
            helptip: app.msgs.helptip_prefixMode },
        suffixEnabled: { type: 'toggle', icon: 'questionMark', defaultVal: false,
            label: `${app.msgs.menuLabel_require} "?" ${app.msgs.menuLabel_afterQuery}`,
            helptip: app.msgs.helptip_suffixMode },
        widerSidebar: { type: 'toggle', mobile: false, icon: 'widescreenTall', defaultVal: false,
            label: app.msgs.menuLabel_widerSidebar,
            helptip: app.msgs.helptip_widerSidebar },
        stickySidebar: { type: 'toggle', mobile: false, icon: 'sidebar', defaultVal: false,
            label: app.msgs.menuLabel_stickySidebar,
            helptip: app.msgs.helptip_stickySidebar },
        anchored: { type: 'toggle', mobile: false, icon: 'anchor', defaultVal: false,
            label: app.msgs.mode_anchor,
            helptip: app.msgs.helptip_anchorMode },
        bgAnimationsDisabled: { type: 'toggle', icon: 'sparkles', defaultVal: false,
            label: `${app.msgs.menuLabel_background} ${app.msgs.menuLabel_animations}`,
            helptip: app.msgs.helptip_bgAnimations },
        fgAnimationsDisabled: { type: 'toggle', icon: 'sparkles', defaultVal: false,
            label: `${app.msgs.menuLabel_foreground} ${app.msgs.menuLabel_animations}`,
            helptip: app.msgs.helptip_fgAnimations },
        replyLang: { type: 'prompt', icon: 'languageChars',
            label: app.msgs.menuLabel_replyLanguage,
            helptip: app.msgs.helptip_replyLanguage },
        scheme: { type: 'modal', icon: 'scheme',
            label: app.msgs.menuLabel_colorScheme,
            helptip: app.msgs.helptip_colorScheme },
        debugMode: { type: 'toggle', icon: 'bug', defaultVal: false,
            label: app.msgs.mode_debug,
            helptip: app.msgs.helptip_debugMode },
        about: { type: 'modal', icon: 'questionMarkCircle',
            label: `${app.msgs.menuLabel_about} ${app.name}...` }
    }})
    Object.assign(app.config, {
        lineHeightRatio: env.browser.isMobile ? 1.357 : 1.375, maxFontSize: 24, minFontSize: 11 })
    settings.load(Object.keys(settings.controls), 'expanded', 'fontSize', 'minimized', 'aiSafetyWarned')
    if (!app.config.replyLang) settings.save('replyLang', env.browser.language) // init reply language if unset
    if (!app.config.fontSize) settings.save('fontSize', env.browser.isMobile ? 14 : 14.0423) // init reply font size if unset
    if (!env.scriptManager.supportsStreaming) settings.save('streamingDisabled', true) // disable Streaming in unspported env
    log.debug(`Success! app.config = ${log.prettifyObj(app.config)}`)

    // Define UI functions

    window.update = {

        appBottomPos() { app.div.style.bottom = `${ app.config.minimized ? 22 - app.div.offsetHeight : -33 }px` },

        appStyle() { // used in toggle.animations() + update.scheme() + main's app init
            const { scheme: appScheme } = env.ui.app,
                  isParticlizedDS = appScheme == 'dark' && !app.config.bgAnimationsDisabled
            modals.stylize() // update modal styles
            if (!app.styles?.isConnected) document.head.append(app.styles ||= dom.create.style())
            app.styles.textContent = (

                // Init vars
               `:root {
                  --app-bg-color-light-scheme: white ; --app-bg-color-dark-scheme: #1f1f1f ;
                  --pre-bg-color-light-scheme: #b7b7b736 ; --pre-bg-color-dark-scheme: #3a3a3a ;
                  --reply-header-bg-color-light-scheme: #d7d4d4 ;
                  --reply-header-bg-color-dark-scheme: ${ !isParticlizedDS ? '#545454' : '#0e0e0e24' };
                  --reply-header-fg-color-light-scheme: white ; --reply-header-fg-color-dark-scheme: white ;
                  --chatbar-btn-color-light-scheme: lightgrey ; --chatbar-btn-color-dark-scheme: #fff ;
                  --chatbar-btn-hover-color-light-scheme: #638ed4 ; --chatbar-btn-hover-color-dark-scheme: #fff ;
                  --font-color-light-scheme: #4e4e4e ; --font-color-dark-scheme: #e3e3e3 ;
                  --app-border: ${ isParticlizedDS ? 'none'
                        : `1px solid #${ appScheme == 'light' ? 'dadce0' : '3b3b3b' }`};
                  --app-gradient-bg: linear-gradient(180deg, ${
                        appScheme == 'dark' ? '#99a8a6 -245px, black 185px' : '#b6ebff -163px, white 65px' }) ;
                  --app-anchored-shadow: 0 15px 52px rgb(0,0,${ appScheme == 'light' ? '7,0.06' : '11,0.22' }) ;
                  --app-transition: opacity 0.5s ease, transform 0.5s ease, /* for 1st fade-in */
                                    bottom 0.1s cubic-bezier(0,0,0.2,1), /* smoothen Anchor Y min/restore */
                                    width 0.167s cubic-bezier(0,0,0.2,1) ; /* smoothen Anchor X expand/shrink */
                  --standby-btn-zoom: scale(1.015) ; --standby-btn-transition: all 0.18s ease ;
                  --btn-transition: transform 0.15s ease, /* for hover-zoom */
                                    opacity 0.25s ease-in-out ; /* + btn-zoom-fade-out + .app-hover-only shows */
                  --font-size-slider-thumb-transition: transform 0.05s ease ; /* for hover-zoom */
                  --reply-pre-transition: max-height 0.167s cubic-bezier(0, 0, 0.2, 1) ; /* for Anchor changes */
                  --rq-transition: opacity 0.55s ease, transform 0.1s ease !important ; /* for fade-in + hover-zoom */
                  --fade-in-transition: opacity 0.4s ease ;
                  --fade-in-less-transition: opacity 0.2s ease } /* used by Font Size slider + Pin menu */`

                // Animations
             + `.fade-in {
                    opacity: 0 ; transform: translateY(10px) ;
                    transition: var(--fade-in-less-transition) ;
                       -webkit-transition: var(--fade-in-transition) ;
                       -moz-transition: var(--fade-in-transition) ;
                       -o-transition: var(--fade-in-transition) ;
                       -ms-transition: var(--fade-in-transition)
                }
                .fade-in-less {
                    opacity: 0 ;
                    transition: var(--fade-in-less-transition) ;
                       -webkit-transition: var(--fade-in-less-transition) ;
                       -moz-transition: var(--fade-in-less-transition) ;
                       -o-transition: var(--fade-in-less-transition) ;
                       -ms-transition: var(--fade-in-less-transition)
                }
                .fade-in.active, .fade-in-less.active { opacity: 1 ; transform: translateY(0) }
                @keyframes btn-zoom-fade-out {
                    0% { opacity: 1 } 55% { opacity: 0.25 ; transform: scale(1.85) }
                    75% { opacity: 0.05 ; transform: scale(2.15) } 100% { opacity: 0 ; transform: scale(6.85) }}
                @keyframes icon-scroll { 0% { transform: translateX(0) } 100% { transform: translateX(-14px) }}
                @keyframes pulse { 0%, to { opacity: 1 } 50% { opacity: .5 }}
                @keyframes rotate { from { transform: rotate(0deg) } to { transform: rotate(360deg) }}
                @keyframes spinY { 0% { transform: rotateY(0deg) } 100% { transform: rotateY(360deg) }}`

                // Main styles
             + `.no-user-select {
                   -webkit-user-select: none ; -moz-user-select: none ;
                   -ms-user-select: none ; user-select: none }
                .no-mobile-tap-outline { outline: none ; -webkit-tap-highlight-color: transparent }
                /* stylize scrollbars in Chromium/Safari */
                    #${app.slug} *::-webkit-scrollbar { width: 7px }
                    #${app.slug} *::-webkit-scrollbar-thumb { background: #cdcdcd }
                    #${app.slug} *::-webkit-scrollbar-thumb:hover { background: #a6a6a6 }
                    #${app.slug} *::-webkit-scrollbar-track { background: none }
                #${app.slug} * { scrollbar-width: thin } /* make scrollbars thin in Firefox */
                .cursor-overlay { /* for fontSizeSlider.createAppend() drag listeners to show resize cursor everywhere */
                    position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ;
                    z-index: 9999 ; cursor: ew-resize }
                #${app.slug} { /* main app div */
                    color: var(--font-color-${appScheme}-scheme) ;
                    background: var(--app-bg-color-${appScheme}-scheme) ;
                    position: sticky ; z-index: 101 ; padding: ${ env.browser.isFF ? 20 : 22 }px 26px 6px 26px ;
                    ${ !env.browser.isMobile ? 'margin-top: 55px ;' : '' } /* add top margin on desktop */
                    border-radius: 8px ; height: fit-content ;
                    width: ${ // hard-width to prevent Google's flex-wrap moving app to bottom
                        env.browser.isMobile ? 'auto' : '319px' };
                    ${ env.browser.isMobile ? 'margin: 8px 0 8px' : 'margin-bottom: 30px' }; /* add vertical margins */
                    word-wrap: break-word ; white-space: pre-wrap ;
                    transition: var(--app-transition) ;
                       -webkit-transition: var(--app-transition) ; -moz-transition: var(--app-transition) ;
                       -o-transition: var(--app-transition) ; -ms-transition: var(--app-transition) }
                #${app.slug}:has(.${app.slug}-alert) { /* app alerts */
                    border: var(--app-border) ; background-image: var(--app-gradient-bg) }
                #${app.slug} .app-hover-only { /* hide app-hover-only elems */
                    position: absolute ; left: -9999px ; opacity: 0 ; /* using position to support transitions */
                    width: 0 } /* to support width calcs */
                /* show app-hover-only elems on hover + Font Size button when slider visible */
                #${app.slug}:hover .app-hover-only, #${app.slug}:active .app-hover-only,
                    #${app.slug}:has([id$=font-size-slider-track].active) [id$=font-size-btn] {
                        position: relative ; left: auto ; width: auto ; opacity: 1 }
                #${app.slug} p { margin: 0 }
                #${app.slug} .alert-link {
                    color: ${ appScheme == 'light' ? '#190cb0' : 'white ; text-decoration: underline' }}
                .${app.slug}-name {
                    font-size: 1.35rem ; font-weight: 700 ; text-decoration: none ;
                    color: ${ appScheme == 'dark' ? 'white' : 'black' } !important }
                .byline { /* header byline */
                    font-size: 12px ; margin-left: 7px ; color: #aaa ;
                  --byline-transition: 0.15s ease-in-out ; transition: var(--byline-transition) ;
                       -webkit-transition: var(--byline-transition) ; -moz-transition: var(--byline-transition) ;
                       -o-transition: var(--byline-transition) ; -ms-transition: var(--byline-transition) }
                .byline a, .byline a:visited { color: #aaa ; text-decoration: none !important }
                .byline a:hover {
                    color: ${ appScheme == 'dark' ? 'white' : 'black' };
                    transition: var(--byline-transition) ;
                       -webkit-transition: var(--byline-transition) ; -moz-transition: var(--byline-transition) ;
                       -o-transition: var(--byline-transition) ; -ms-transition: var(--byline-transition) }
                #${app.slug}-header-btns { display: flex ; direction: rtl ; gap: 2px ; float: right }
                .${app.slug}-header-btn {
                    float: right ; cursor: pointer ; position: relative ; top: 6px ;
                    ${ appScheme == 'dark' ? 'fill: white ; stroke: white' : 'fill: #adadad ; stroke: #adadad' }}
                .${app.slug}-header-btn:hover svg { /* highlight/zoom header button on hover */
                    ${ appScheme == 'dark' ? 'fill: #d9d9d9 ; stroke: #d9d9d9' : 'fill: black ; stroke: black' };
                    ${ env.browser.isMobile ? '' : 'transform: scale(1.285)' }}
                ${ app.config.fgAnimationsDisabled ? '' :
                   `.${app.slug}-header-btn, .${app.slug}-header-btn svg { /* smooth header button fade-in + hover-zoom */
                    transition: var(--btn-transition) ;
                       -webkit-transition: var(--btn-transition) ; -moz-transition: var(--btn-transition) ;
                       -o-transition: var(--btn-transition) ; -ms-transition: var(--btn-transition) }`}
                .${app.slug}-header-btn:active {
                    ${ appScheme == 'dark' ? 'fill: #999999 ; stroke: #999999'
                                           : 'fill: #638ed4 ; stroke: #638ed4' }}
                #${app.slug}-logo, .${app.slug}-header-btn svg {
                    filter: drop-shadow(${ appScheme == 'dark' ? '#7171714d 10px' : '#aaaaaa21 7px' } 7px 3px) }
                #${app.slug} .loading {
                    padding-bottom: 15px ; color: #b6b8ba ; fill: #b6b8ba ;
                    animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite }
                #${app.slug} section.loading { padding: 15px 0 14px 5px } /* pad loading status when sending replies */
                #${app.slug}-font-size-slider-track {
                    width: 98% ; height: 7px ; margin: 3px auto ${ env.browser.isCompact ? -6 : -11 }px ;
                    padding: 15px 0 ; background-color: #ccc ; box-sizing: content-box; background-clip: content-box ;
                   -webkit-background-clip: content-box }
                #${app.slug}-font-size-slider-track::before { /* to add finger cursor to unpadded core only */
                    content: "" ; position: absolute ; top: 10px ; left: 0 ; right: 0 ;
                    height: calc(100% - 20px) ; cursor: pointer }
                #${app.slug}-font-size-slider-tip {
                    z-index: 1 ; position: absolute ; bottom: 20px ;
                    border-left: 4.5px solid transparent ; border-right: 4.5px solid transparent ;
                    border-bottom: 16px solid #ccc }
                #${app.slug}-font-size-slider-thumb {
                    z-index: 2 ; width: 7px ; height: 25px ; border-radius: 30% ; position: relative ;
                    top: -7.5px ; cursor: ew-resize ;
                    background-color: ${ appScheme == 'dark' ? 'white' : '#4a4a4a' };
                  --shadow: rgba(0,0,0,0.21) 1px 1px 9px 0 ;
                        box-shadow: var(--shadow) ; -webkit-box-shadow: var(--shadow) ; -moz-box-shadow: var(--shadow) ;
                    ${ app.config.fgAnimationsDisabled ? '' : `transition: var(--font-size-slider-thumb-transition)
                       -webkit-transition: var(--font-size-slider-thumb-transition) ;
                       -moz-transition: var(--font-size-slider-thumb-transition) ;
                       -o-transition: var(--font-size-slider-thumb-transition) ;
                       -ms-transition: var(--font-size-slider-thumb-transition)` }}
                ${ env.browser.isMobile ? '' : `#${app.slug}-font-size-slider-thumb:hover { transform: scale(1.125) }`}
                .${app.slug}-standby-btns { margin: 20px 0 -14px }
                .${app.slug}-standby-btn {
                  --skew: skew(-13deg) ; --counter-skew: skew(13deg) ; border-radius: 8px ;
                  --content-color: ${ appScheme == 'dark' ? 'white' : 'black' };
                    display: flex ; align-items: center ; justify-content: center ; gap: 8px ;
                    width: 90% ; height: 51px ; margin-bottom: 9px ; padding: 12px 0 ;
                    cursor: pointer ; transform: var(--skew) ; border: 1px solid var(--content-color) ;
                    background: none ; box-shadow: #aaaaaa12 7px 7px 3px 0px ; color: var(--content-color) ;
                    ${ app.config.fgAnimationsDisabled ? ''
                        : `will-change: transform ;
                           transition: var(--standby-btn-transition) ;
                               -webkit-transition: var(--standby-btn-transition) ;
                               -moz-transition: var(--standby-btn-transition) ;
                               -o-transition: var(--standby-btn-transition) ;
                               -ms-transition: var(--standby-btn-transition)` }}
                .${app.slug}-standby-btn:hover {
                    color: var(--content-color) ; border-radius: 2px ; transform: var(--skew) var(--standby-btn-zoom) }
                .${app.slug}-standby-btn > span { transform: var(--counter-skew) }
                .${app.slug}-standby-btn > svg {
                    position: relative ; stroke: var(--content-color) ; fill: stroke: var(--content-color) ;
                    transform: var(--counter-skew) }
                .${app.slug}-standby-btn:nth-child(odd) { margin-right: 10% }
                .${app.slug}-standby-btn:nth-child(even) { margin-left: 10% ; margin-bottom: 19px }
                .${app.slug}-standby-btn:first-of-type svg { /* Query button icon */
                    width: 11px ; height: 11px ; top: -1.5px ; right: -1.5px }
                .${app.slug}-standby-btn:nth-of-type(2) svg { /* Summarize button icon */
                    width: 17.5px ; height: 17.5px }`

              // AI reply elem styles
             + `#${app.slug} .reply-tip {
                    content: "" ; position: relative ; border: 7px solid transparent ;
                    float: left ; margin: 3px -15px 0 0 ;
                    left: ${ env.browser.isMobile ? 12 : 6 }px ; /* positioning */
                    border-bottom-style: solid ; border-bottom-width: 20px ; border-top: 0 ; border-bottom-color:
                        ${ // hide reply tip for terminal aesthetic
                            isParticlizedDS ? '#0000' : `var(--reply-header-bg-color-${appScheme}-scheme)` }}
                #${app.slug} .reply-header {
                    display: flex ; align-items: center ; position: relative ; box-sizing: border-box ; width: 100% ;
                    top: 16px ; padding: 16px 14px ; height: 18px ; border-radius: 12px 12px 0 0 ;
                    ${ appScheme == 'light' ? 'border-bottom: 1px solid white'
                          : isParticlizedDS ? 'border: 1px solid ; border-bottom-color: transparent' : '' };
                    background: var(--reply-header-bg-color-${appScheme}-scheme) ;
                    color:      var(--reply-header-fg-color-${appScheme}-scheme) ;
                    fill:       var(--reply-header-fg-color-${appScheme}-scheme) ;
                    stroke:     var(--reply-header-fg-color-${appScheme}-scheme) }
                #${app.slug} .reply-header-txt { flex-grow: 1 ; font-size: 12px ; font-family: monospace }
                #${app.slug} .reply-header-btns { margin: 3.5px -5px 0 }
                #${app.slug} .reply-pre {
                    font-size: ${app.config.fontSize}px ; white-space: pre-wrap ; min-width: 0 ;
                    line-height: ${ app.config.fontSize * app.config.lineHeightRatio }px ; overscroll-behavior: contain ;
                    position: relative ; z-index: 1 ; /* allow top-margin to overlap header in light scheme */
                    margin: ${ appScheme == 'light' ? 13 : 15 }px 0 0 0 ; padding: 1em 1em 0 1em ;
                    border-radius: 0 0 12px 12px ; overflow: auto ;
                    ${ app.config.bgAnimationsDisabled ? // classic opaque bg
                        `background: var(--pre-bg-color-${appScheme}-scheme) ;
                         color: var(--font-color-${appScheme}-scheme)`
                    : appScheme == 'dark' ? // slightly tranluscent bg
                        'background: #2b3a40cf ; color: var(--font-color-dark-scheme) ; border: 1px solid white'
                    : /* light scheme */ `background: var(--pre-bg-color-light-scheme) ;
                            color: var(--font-color-light-scheme) ; border: none` };
                    ${ app.config.fgAnimationsDisabled ? '' : // smoothen Anchor mode expand/shrink
                        `transition: var(--reply-pre-transition) ;
                            -webkit-transition: var(--reply-pre-transition) ;
                            -moz-transition: var(--reply-pre-transition) ;
                            -o-transition: var(--reply-pre-transition) ;
                            -ms-transition: var(--reply-pre-transition)` }}
                #${app.slug} .reply-pre a, #${app.slug} .reply-pre a:visited { color: #4495d4 }
                #${app.slug} .reply-pre a:hover { color: ${ appScheme == 'dark' ? 'white' : '#28a017' }}
                #${app.slug} .code-header {
                    display: flex ; direction: rtl ; gap: 9px ; align-items: center ;
                    height: 11px ; margin: 3px -2px 0 }
                #${app.slug} .code-header btn { cursor: pointer }
                #${app.slug} .code-header svg { height: 13px ; width: 13px ; fill: white }`

              // Rendered markdown styles
             + `#${app.slug} .reply-pre h1 { font-size: 1.25em }
                #${app.slug} .reply-pre h2 { font-size: 1.1em } /* size headings */
                #${app.slug} .reply-pre ol { margin: -5px 0 -8px 7px ; padding-left: 1.58em }
                #${app.slug} .reply-pre ul { /* reduce v-spacing, indent */
                    margin: -10px 0 -6px ; padding-left: 1.5em }
                #${app.slug} .reply-pre li { /* reduce v-spacing, show hollow bullets */
                    margin: -8px 0 ; list-style: circle }
                #${app.slug} .reply-pre ul ul { margin-top: 0 } /* push sub-lists down */
                #${app.slug} .reply-pre ul ul > li { list-style: disc } /* fill sub-bullets */`

              // Rendered code styles
             + `#${app.slug} ${GM_getResourceText('hljsCSS') // color code
                    .replace(/\/\*[^*]+\*\//g, '') // strip comments
                    .trim().replace(/([,}])(.)(?![^{]*\})/g, `$1#${app.slug} $2`)} /* scope selectors to app */
                #${app.slug} pre:has(> code) { padding: 0 } /* remove padded border around code blocks */
                #${app.slug} code { font-size: 0.85em } /* shrink code vs. regular text */`

              // Rendered math styles
             + '.katex-html { display: none } /* hide unrendered math */'

              // Chatbar styles
             + `#${app.slug}-chatbar {
                    border: solid 1px ${ isParticlizedDS ? '#aaa' : appScheme == 'dark' ? '#777' : '#555' };
                    border-radius: 12px 13px 12px 0 ; margin: 12px 0 15px 0 ; padding: 13px 55px 13px 10px ;
                    position: relative ; z-index: 555 ; color: ${ appScheme == 'dark' ? '#eee' : '#222' };
                    height: 16px ; max-height: 200px ; resize: none ;
                    background: ${ appScheme == 'light' ? '#eeeeee9e'
                        : `#515151${ app.config.bgAnimationsDisabled ? '' : '9e' }`};
                    ${ appScheme == 'dark' ? '' :
                        `--chatbar-inset-shadow: 0 1px 2px rgba(15,17,17,0.1) inset ;
                        box-shadow: var(--chatbar-inset-shadow) ; -webkit-box-shadow: var(--chatbar-inset-shadow) ;
                       -moz-box-shadow: var(--chatbar-inset-shadow) ;` }
                        transition: box-shadow 0.15s ease ;
                           -webkit-transition: box-shadow 0.15s ease ; -moz-transition: box-shadow 0.15s ease ;
                           -o-transition: box-shadow 0.15s ease ; -ms-transition: box-shadow 0.15s ease }
                ${ isParticlizedDS ? '' : ` /* chatbar hover styles */
                    #${app.slug}-chatbar:hover:not(:focus),
                    div:has(.${app.slug}-chatbar-btn:hover) #${app.slug}-chatbar:not(:focus) {
                        outline: ${ appScheme == 'light' ? 'black' : 'white' } auto 5px ;
                      --chatbar-hover-inset-shadow: 0 ${
                            appScheme == 'dark' ? '3px 2px' : '1px 7px' } rgba(15,17,17,0.15) inset ;
                        box-shadow: var(--chatbar-hover-inset-shadow) ;
                       -webkit-box-shadow: var(--chatbar-hover-inset-shadow) ;
                       -moz-box-shadow: var(--chatbar-hover-inset-shadow) ;
                        transition: box-shadow 0.25s ease ;
                           -webkit-transition: box-shadow 0.25s ease ; -moz-transition: box-shadow 0.25s ease ;
                           -o-transition: box-shadow 0.25s ease ; -ms-transition: box-shadow 0.25s ease }`}
                #${app.slug}-chatbar:focus-visible { /* fallback outline chatbar + reduce inset shadow on focus */
                    outline: -webkit-focus-ring-color auto 1px ;
                    ${ isParticlizedDS ? '' :
                        `--inset-shadow: 0 ${ appScheme == 'dark' ? '3px -1px' : '1px 2px' } rgba(0,0,0,0.3) inset ;
                        box-shadow: var(--inset-shadow) ;
                       -webkit-box-shadow: var(--inset-shadow) ; -moz-box-shadow: var(--inset-shadow)` }
                }
                .${app.slug}-chatbar-btn {
                    z-index: 560 ; border: none ; float: right ; position: relative ; background: none ;
                    cursor: pointer ; bottom: ${ env.browser.isFF ? 47 : 52 }px ;
                    transform: scale(1.05) ; margin-right: 3px ; /* zoom 'em a bit */
                    color:  var(--chatbar-btn-color-${appScheme}-scheme) ;
                    fill:   var(--chatbar-btn-color-${appScheme}-scheme) ;
                    stroke: var(--chatbar-btn-color-${appScheme}-scheme)
                }
                .${app.slug}-chatbar-btn:hover {
                    color:  var(--chatbar-btn-hover-color-${appScheme}-scheme) ;
                    fill:   var(--chatbar-btn-hover-color-${appScheme}-scheme) ;
                    stroke: var(--chatbar-btn-hover-color-${appScheme}-scheme)
                }`

              // Related Queries styles
             + `.${app.slug}-related-queries {
                    display: flex ; flex-wrap: wrap ; width: 100% ; margin-bottom: 19px ; padding: 0 5px }
                .${app.slug}-related-query {
                    font-size: ${ env.browser.isMobile ? 1 : 0.81 }em ; cursor: pointer ; will-change: transform ;
                    box-sizing: border-box ; width: fit-content ; max-width: 100% ; /* confine to outer div */
                    margin: 5px 12px 7px 0 ; padding: 8px 12px 8px 13px ;
                    color: ${ appScheme == 'dark' ? ( app.config.bgAnimationsDisabled ? '#ccc' : '#f2f2f2' )
                                                  : '#767676' };
                    background: ${ appScheme == 'dark' ? '#7e7e7e4f' : '#fdfdfdb0' };
                    border: 1px solid ${ appScheme == 'dark' ? (
                        app.config.bgAnimationsDisabled ? '#5f5f5f' : '#777' ) : '#d2d2d2' };
                    border-radius: 0 13px 12px 13px ; flex: 0 0 auto ;
                  --rq-shadow: 1px 4px 8px -6px rgba(169,169,169,0.75) ; box-shadow: var(--rq-shadow) ;
                       -webkit-box-shadow: var(--rq-shadow) ; -moz-box-shadow: var(--rq-shadow) ;
                    ${ app.config.fgAnimationsDisabled ? '' : `transition: var(--rq-transition) ;
                       -webkit-transition: var(--rq-transition) ; -moz-transition: var(--rq-transition) ;
                       -o-transition: var(--rq-transition) ; -ms-transition: var(--rq-transition)` }}
                .${app.slug}-related-query:hover, .${app.slug}-related-query:focus {
                    ${ app.config.fgAnimationsDisabled ? '' : 'transform: scale(1.055) !important ;' }
                    background: ${ appScheme == 'dark' ? '#a2a2a270'
                        : '#dae5ffa3 ; color: #000000a8 ; border-color: #a3c9ff' }}
                .${app.slug}-related-query svg { /* related query icon */
                    float: left ; margin: -0.09em 6px 0 0 ;
                    color: ${ appScheme == 'dark' ? '#aaa' : '#c1c1c1' }}`

              // Footer styles
             + `#${app.slug} footer {
                    position: relative ; text-align: right ; font-size: 0.75rem ; line-height: 1.43em ;
                    right: -54px ; margin: ${ env.browser.isFF ? 1 : -2 }px -32px 12px }
                #${app.slug} footer * { color: #aaa ; text-decoration: none }
                #${app.slug} footer a:hover { color: ${ appScheme == 'dark' ? 'white' : 'black' }}`

              // Notif styles
             + `.chatgpt-notif {
                    fill: white ; stroke: white ; font-size: 25px !important ; padding: 13px 14px 13px 13px !important }
                .notif-close-btn { display: none !important } /* hide notif close btn */
                .${app.slug}-menu {
                    position: absolute ; z-index: 12250 ;
                    padding: 3.5px 5px !important ; font-family: "Source Sans Pro", sans-serif ; font-size: 12px }`

              // Menu styles
             + `.${app.slug}-menu ul { margin: 0 ; padding: 0 ; list-style: none }
                .${app.slug}-menu-item { padding: 0 5px ; line-height: 20.5px }
                .${app.slug}-menu-item:not(.${app.slug}-menu-header):hover {
                    cursor: pointer ; background: white ; color: black ; fill: black }`

              // Wider Sidebar styles
             + `#${app.slug}.wider { min-width: 455px }
                #${app.slug}.wider ~ div { min-width: 508px } /* expand side snippets */
                #center_col:has(~ div #${app.slug}.wider),
                    #center_col:has(~ div #${app.slug}.wider) div {
                        max-width: 516px } /* shrink center column/children */
                div:has(> #${app.slug}.wider) { /* shift sidebar left to align w/ skinnier center column */
                    position: relative ; left: -136px }`

              // Sticky Sidebar styles
             + `#${app.slug}.sticky { position: sticky ; top: 87px }
                #${app.slug}.sticky ~ * { display: none }` // hide sidebar contents

              // Anchor Mode styles
             + `#${app.slug}.anchored {
                    position: fixed ; bottom: -7px ; right: 35px ; width: 388px ; z-index: 8888 ;
                    border: var(--app-border) ; box-shadow: var(--app-anchored-shadow) ;
                    ${ app.config.bgAnimationsDisabled ? `background: var(--app-bg-color-${appScheme}-scheme)`
                                                   : 'background-image: var(--app-gradient-bg)' }}
                #${app.slug}.expanded { width: 528px !important }
                #${app.slug}.anchored .anchored-hidden { display: none } /* hide non-Anchor elems in mode */
                #${app.slug}:not(.anchored) .anchored-only { display: none } /* hide Anchor elems outside mode */`

              // Touch device styles
             + `@media (hover: none) {
                    #${app.slug} .app-hover-only { /* show app-hover-only elems */
                        position: relative ; left: auto ; width: auto ; opacity: 1 }
                    #${app.slug} *:hover { transform: none !important } /* disable hover fx */
                }`

              // Phone styles
             + `@media screen and (max-width: 480px) {
                    #${app.slug} #${app.slug}-logo { /* header logo... */
                        top: 0 ; width: calc(100% - 154px) } /* remove y-pos, widen till btns */
                    #${app.slug} .byline { display: none !important } /* hide byline */
                    #${app.slug} [class*=reply-tip] { display: none } /* hide reply tip */
                    .${app.slug}-related-queries { padding: 0 } /* remove RQ parent padding */
                }`
            )
            themes.apply(app.config.theme)
        },

        bylineVisibility() {
            if (env.browser.isCompact) return // since byline hidden by app.styles

            // Init header elems
            const headerElems = { byline: app.div.querySelector('.byline') }
            if (!headerElems.byline) return // since in loading state
            Object.assign(headerElems, {
                appPrefix: app.div.querySelector('#app-prefix'),
                btns: app.div.querySelectorAll(`#${app.slug}-header-btns > btn`),
                logo: app.div.querySelector(`#${app.slug}-logo`)
            })

            // Calc/store widths of app/x-padding + header elems
            const appDivStyle = getComputedStyle(app.div)
            const widths = {
                appDiv: app.div.getBoundingClientRect().width,
                appDivXpadding: parseFloat(appDivStyle.paddingLeft) + parseFloat(appDivStyle.paddingRight)
            }
            Object.entries(headerElems).forEach(([key, elem]) => widths[key] = dom.get.computedWidth(elem))

            // Hide/show byline based on space available
            const availSpace = widths.appDiv - widths.appDivXpadding - widths.appPrefix - widths.logo - widths.btns -16
            Object.assign(headerElems.byline.style, widths.byline > availSpace ?
                { position: 'absolute', left: '-9999px', opacity: 0 } // hide using position to support transition
              : { position: '', left: '', opacity: 1 } // show
            )
        },

        chatbarWidth() {
            const chatbar = app.div.querySelector(`#${app.slug}-chatbar`)
            if (chatbar) chatbar.style.width = `${
                env.browser.isMobile ? 81.4
              : app.config.anchored ? ( app.config.expanded ? 87.4 : 83.3 )
              : app.config.widerSidebar ? ( env.ui.site.hasSidebar ? 85.4 : 85.9 )
                                    : ( env.ui.site.hasSidebar ? 79.3 : 80.1 )}%`
        },

        async footerContent() {

            // Init advertisers data
            const advertisersData = await get.json(
                'https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/index.json'
            ).catch(err => log.error(err.message)) ; if (!advertisersData) return

            // Pick random advertiser
            let chosenAdvertiser
            for (const [advertiser, details] of shuffle(applyBoosts(Object.entries(advertisersData))))
                if (details.campaigns.text) { chosenAdvertiser = advertiser ; break }
            if (!chosenAdvertiser) return

            // Init chosen advertiser's campaigns data
            const campaignsData = await get.json(
                `https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/${chosenAdvertiser}/text/campaigns.json`
            ).catch(err => log.error(err.message)) ; if (!campaignsData) return

            // Init vars for ad selection
            const reAppName = new RegExp(app.name.toLowerCase(), 'i')
            const currentDate = (() => { // in YYYYMMDD format
                const today = new Date(), year = today.getFullYear(),
                      month = String(today.getMonth() + 1).padStart(2, '0'),
                      day = String(today.getDate()).padStart(2, '0')
                return year + month + day
            })() ; let adSelected = false

            // Select random, active campaign
            for (const [campaignName, campaign] of shuffle(applyBoosts(Object.entries(campaignsData)))) {
                const campaignIsActive = campaign.active && (!campaign.endDate || currentDate <= campaign.endDate)
                if (!campaignIsActive) continue // to next campaign since campaign inactive

                // Select random active group
                for (const [groupName, adGroup] of shuffle(applyBoosts(Object.entries(campaign.adGroups)))) {

                    // Skip disqualified groups
                    if ( // self-group for other apps
                        /^self$/i.test(groupName) && !reAppName.test(campaignName)
                        || ( // non-self group for this app
                            reAppName.test(campaignName) && !/^self$/i.test(groupName))
                        || adGroup.active == false // group explicitly disabled
                        || adGroup.targetBrowsers && // target browser(s) exist...
                            !adGroup.targetBrowsers.some( // ...but doesn't match user's
                                browser => new RegExp(browser, 'i').test(navigator.userAgent))
                        || adGroup.targetLocations && ( // target locale(s) exist...
                            // ...but user locale is missing or excluded
                            !env.userLocale || !adGroup.targetLocations.some(
                                loc => loc.includes(env.userLocale) || env.userLocale.includes(loc)))
                    ) continue // to next group

                    // Filter out inactive ads, pick random active one
                    const activeAds = adGroup.ads.filter(ad => ad.active != false)
                    if (!activeAds.length) continue // to next group since no ads active
                    const chosenAd = activeAds[Math.floor(chatgpt.randomFloat() * activeAds.length)]

                    // Build destination URL
                    let destinationURL = chosenAd.destinationURL || adGroup.destinationURL || campaign.destinationURL || ''
                    if (destinationURL.includes('http')) { // insert UTM tags
                        const [baseURL, queryString] = destinationURL.split('?'),
                              queryParams = new URLSearchParams(queryString || '')
                        queryParams.set('utm_source', app.name.toLowerCase())
                        queryParams.set('utm_content', 'app_footer_link')
                        destinationURL = `${baseURL}?${queryParams.toString()}`
                    }

                    // Update footer content
                    const newFooterContent = destinationURL ? dom.create.anchor(destinationURL)
                                                            : dom.create.elem('span')
                    app.footerContent.replaceWith(newFooterContent) ; app.footerContent = newFooterContent
                    app.footerContent.textContent = chosenAd.text
                    app.footerContent.setAttribute('title', chosenAd.tooltip || '')
                    adSelected = true ; break // out of group loop
                }
                if (adSelected) break // out of campaign loop
            }

            function shuffle(list) {
                let currentIdx = list.length, tempValue, randomIdx
                while (currentIdx != 0) { // elements remain to be shuffled
                    randomIdx = Math.floor(chatgpt.randomFloat() * currentIdx) ; currentIdx -=1
                    tempValue = list[currentIdx] ; list[currentIdx] = list[randomIdx] ; list[randomIdx] = tempValue
                } return list
            }

            function applyBoosts(list) {
                let boostedList = [...list],
                    boostedListLength = boostedList.length -1 // for applying multiple boosts
                list.forEach(([name, data]) => { // check for boosts
                    if (data.boost) { // boost flagged entry's selection probability
                        const boostPercent = parseInt(data.boost) / 100
                        const entriesNeeded = Math.ceil(boostedListLength / (1 - boostPercent)) // total entries needed
                                            * boostPercent -1 // reduced to boosted entries needed
                        for (let i = 0 ; i < entriesNeeded ; i++) boostedList.push([name, data]) // saturate list
                        boostedListLength += entriesNeeded // update for subsequent calculations
                }})
                return boostedList
            }
        },

        replyPrefix() {
            const firstP = app.div.querySelector('pre p') ; if (!firstP) return
            const prefixNeeded = env.ui.app.scheme == 'dark'
                && !app.config.bgAnimationsDisabled && !/shuffle|summarize/.test(get.reply.src)
            const prefixExists = firstP.textContent.startsWith('>> ')
            if (prefixNeeded && !prefixExists) firstP.prepend('>> ')
            else if (!prefixNeeded && prefixExists) firstP.textContent = firstP.textContent.replace(/^>> /, '')
        },

        risingParticles() {
            ['sm', 'med', 'lg'].forEach(size =>
                document.querySelectorAll(`[id*=particles-${size}]`).forEach(particlesDiv =>
                    particlesDiv.id = app.config.bgAnimationsDisabled ? `particles-${size}-off`
                    : `${ env.ui.app.scheme == 'dark' ? 'white' : 'gray' }-particles-${size}`))
        },

        rqVisibility() {
            const rqsDiv = app.div.querySelector(`.${app.slug}-related-queries`)
            if (rqsDiv) // update visibility based on latest setting
                rqsDiv.style.display = app.config.rqDisabled || app.config.anchored ? 'none' : 'flex'
        },

        scheme(newScheme) {
            env.ui.app.scheme = newScheme ; logos.googlegpt.update() ; icons.googlegpt.update() ; update.appStyle()
            update.risingParticles() ; update.replyPrefix() ; modals.settings.updateSchemeStatus()
        }
    }

    // Define TOGGLE functions

    window.toggle = {

        anchorMode(state = '') {
            const prevState = app.config.anchored // for restraining notif if no change from Pin menu 'Sidebar' click
            let sidebarModeToggled = false // to extend this notif duration

            // Save new state + disable incompatible Sidebar modes
            if (state == 'on' || !state && !app.config.anchored) {
                settings.save('anchored', true)
                ;['sticky', 'wider'].forEach(mode => {
                    if (app.config[`${mode}Sidebar`]) { toggle.sidebar(mode) ; sidebarModeToggled = true }})
            } else {
                settings.save('anchored', false)
                if (app.config.expanded) { toggle.expandedMode('off') ; sidebarModeToggled = true }
            }
            if (prevState == app.config.anchored) return

            // Apply changed state to UI
            app.div.classList.toggle('anchored', app.config.anchored)
            update.rqVisibility() ; replyBubble.updateMaxHeight() ; update.chatbarWidth()
            if (getComputedStyle(app.div).transitionProperty.includes('width')) // update byline visibility
                app.div.addEventListener('transitionend', function onTransitionEnd(event) { // ...after width transition
                    if (event.propertyName == 'width') {
                        update.bylineVisibility() ; app.div.removeEventListener('transitionend', onTransitionEnd)
            }})
            if (modals.settings.get()) { // update visual state of Settings toggle
                const anchorToggle = document.querySelector('[id*=anchor] input')
                if (anchorToggle.checked != app.config.anchored) modals.settings.toggle.switch(anchorToggle)
            }
            feedback.notify(`${app.msgs.mode_anchor} ${menus.toolbar.state.words[+app.config.anchored]}`,
                undefined, sidebarModeToggled ? 2.75 : undefined) // +1s duration if conflicting mode notif shown
        },

        animations(layer) {
            const configKey = `${layer}AnimationsDisabled`
            settings.save(configKey, !app.config[configKey])
            update.appStyle() ; if (layer == 'bg') { update.risingParticles() ; update.replyPrefix() }
            if (layer == 'fg' && modals.settings.get()) { // toggle ticker-scroll of About status label
                const aboutStatusLabel = document.querySelector('#about-settings-entry > span > div')
                aboutStatusLabel.innerHTML = modals.settings.aboutContent[
                    app.config.fgAnimationsDisabled ? 'short' : 'long']
                aboutStatusLabel.style.float = app.config.fgAnimationsDisabled ? 'right' : ''
            }
            feedback.notify(`${settings.controls[configKey].label} ${menus.toolbar.state.words[+!app.config[configKey]]}`)
        },

        autoGen(mode) {
            const validModes = ['get', 'summarize'], modeKey = `auto${log.toTitleCase(mode)}`
            let conflictingModeToggled = false // to extend this notif duration
            settings.save(modeKey, !app.config[modeKey])
            if (app.config[modeKey]) { // this Auto-Gen mode toggled on, disable other one + Manual-Gen + do it
                const otherMode = validModes[+(mode == validModes[0])]
                if (app.config[`auto${log.toTitleCase(otherMode)}`]) {
                    toggle.autoGen(otherMode) ; conflictingModeToggled = true }
                ['prefix', 'suffix'].forEach(mode => {
                    if (app.config[`${mode}Enabled`]) { toggle.manualGen(mode) ; conflictingModeToggled = true }})
                app.div.querySelector(
                    `button[class*=standby]:has(svg.${ mode == 'get' ? 'send' : 'summarize' })`)?.click()
            }
            feedback.notify(`${settings.controls[modeKey].label} ${menus.toolbar.state.words[+app.config[modeKey]]}`,
                undefined, conflictingModeToggled ? 2.75 : undefined) // +1s duration if conflicting mode notif shown
            if (modals.settings.get()) { // update visual state of Settings toggle
                const modeToggle = document.querySelector(`[id*=${modeKey}] input`)
                if (modeToggle.checked != app.config[modeKey]) modals.settings.toggle.switch(modeToggle)
            }
        },

        expandedMode(state = '') {
            const toExpand = state == 'on' || !state && !app.config.expanded
            settings.save('expanded', toExpand) ; app.div.classList.toggle('expanded', toExpand)
            if (app.config.minimized) toggle.minimized('off') // since user wants to see stuff
            update.chatbarWidth()
            if (getComputedStyle(app.div).transitionProperty.includes('width')) // update byline visibility
                app.div.addEventListener('transitionend', function onTransitionEnd(event) { // ...after width transition
                    if (event.propertyName == 'width') {
                        update.bylineVisibility() ; app.div.removeEventListener('transitionend', onTransitionEnd)
            }})
            const expandBtn = app.div.querySelector(`#${app.slug}-arrows-btn`)
            if (expandBtn) expandBtn.firstChild.replaceWith(
                icons.create({ key: `arrowsDiagonal${ app.config.expanded ? 'In' : 'Out' }`, size: 17 }))
        },

        manualGen(mode) { // Prefix/Suffix modes
            const modeKey = `${mode}Enabled`
            let autoGenToggled = false // to extend this notif duration
            settings.save(modeKey, !app.config[modeKey])
            if (app.config[modeKey]) // Manual-Gen toggled on, disable all Auto-Gen
                ['get', 'summarize'].forEach(mode => {
                    if (app.config[`auto${log.toTitleCase(mode)}`]) { toggle.autoGen(mode) ; autoGenToggled = true }})
            feedback.notify(`${settings.controls[modeKey].label} ${menus.toolbar.state.words[+app.config[modeKey]]}`,
                undefined, autoGenToggled ? 2.75 : undefined) // +1s duration if conflicting mode notif shown)
            if (modals.settings.get()) { // update visual state of Settings toggle
                const modeToggle = document.querySelector(`[id*=${modeKey}] input`)
                if (modeToggle.checked != app.config[modeKey]) modals.settings.toggle.switch(modeToggle)
            }
        },

        minimized(state = '') {
            const toMinimize = state == 'on' || !state && !app.config.minimized
            settings.save('minimized', toMinimize)
            const chevronBtn = app.div.querySelector('[id$=chevron-btn]')
            if (chevronBtn) { // update icon
                chevronBtn.textContent = ''
                chevronBtn.append(icons.create({ key: `chevron${ app.config.minimized ? 'Up' : 'Down' }`,
                    size: 22, style: 'position: relative ; top: -1px' }))
                chevronBtn.onclick = () => {
                    if (app.div.querySelector('[id$=font-size-slider-track]')?.classList.contains('active'))
                        fontSizeSlider.toggle('off')
                    toggle.minimized()
                }
            }
            update.appBottomPos() // toggle visual minimization
            tooltip.toggle('off') // hide lingering tooltip
        },

        proxyMode() {
            settings.save('proxyAPIenabled', !app.config.proxyAPIenabled)
            feedback.notify(`${app.msgs.menuLabel_proxyAPImode} ${menus.toolbar.state.words[+app.config.proxyAPIenabled]}`)
            menus.toolbar.refresh()
            if (modals.settings.get()) { // update visual states of Settings toggles
                const proxyToggle = document.querySelector('[id*=proxy] input'),
                      preferredAPIentry = document.querySelector('[id*=preferredAPI]'),
                      streamingToggle = document.querySelector('[id*=streaming] input')
                if (proxyToggle.checked != app.config.proxyAPIenabled) // Proxy state out-of-sync (from using toolbar menu)
                    modals.settings.toggle.switch(proxyToggle)
                preferredAPIentry.classList.toggle('active', app.config.proxyAPIenabled)
                preferredAPIentry.style.pointerEvents = app.config.proxyAPIenabled ? '' : 'none'
                if (streamingToggle.checked && !app.config.proxyAPIenabled // Streaming checked but OpenAI mode
                    || // ...or Streaming unchecked but enabled in Proxy mode
                        !streamingToggle.checked && app.config.proxyAPIenabled && !app.config.streamingDisabled)
                            modals.settings.toggle.switch(streamingToggle)
            }
            const apiBeacon = app.div.querySelector(`#${app.slug} .api-btn`)
            if (apiBeacon) apiBeacon.style.pointerEvents = app.config.proxyAPIenabled ? '' : 'none'
            if (app.div.querySelector(`.${app.slug}-alert`)) // re-send query if user alerted
                get.reply({ msgs: app.msgChain, src: get.reply.src })
        },

        relatedQueries() {
            settings.save('rqDisabled', !app.config.rqDisabled)
            update.rqVisibility()
            if (!app.config.rqDisabled && !app.div.querySelector(`.${app.slug}-related-queries`)) // get related queries for 1st time
                get.related(app.msgChain[app.msgChain.length - 1]?.content || searchQuery)
                    .then(queries => show.related(queries))
                    .catch(err => { log.error(err.message) ; api.tryNew(get.related) })
            replyBubble.updateMaxHeight()
            feedback.notify(`${app.msgs.menuLabel_relatedQueries} ${menus.toolbar.state.words[+!app.config.rqDisabled]}`)
        },

        sidebar(mode, state = '') {
            const configKeyName = mode + 'Sidebar',
                  prevStickyState = app.config.stickySidebar // for hiding notif if no change from Pin menu 'Sidebar' click
            let anchorModeDisabled = false // to extend this notif duration

            // Save new state + disable incompatible Anchor mode
            if (state == 'on' || !state && !app.config[configKeyName]) { // toggle on
                if (mode == 'sticky' && app.config.anchored) { toggle.anchorMode() ; anchorModeDisabled = true }
                settings.save(configKeyName, true)
            } else settings.save(configKeyName, false)

            // Apply new state to UI
            app.div.classList.toggle(mode, app.config[configKeyName])
            replyBubble.updateMaxHeight() ; update.bylineVisibility() ; update.chatbarWidth()
            if (mode == 'wider') // toggle icons everywhere
            document.querySelectorAll(`#${app.slug} svg.widescreenTall, #${app.slug} svg.widescreenWide`)
                .forEach(icon => icon.replaceWith(
                    icons.create({ key: `widescreen${ app.config.widerSidebar ? 'Wide' : 'Tall' }`})))
            if (modals.settings.get()) { // update visual state of Settings toggles
                const sidebarToggle = document.querySelector(`[id*=${mode}] input`)
                if (sidebarToggle.checked != app.config[`${mode}Sidebar`]) modals.settings.toggle.switch(sidebarToggle)
            }

            // Notify of mode change
            if (mode == 'sticky' && prevStickyState == app.config.stickySidebar) return
            feedback.notify(
                `${ app.msgs[`menuLabel_${mode}Sidebar`] || log.toTitleCase(mode) + ' Sidebar' } ${
                    menus.toolbar.state.words[+app.config[configKeyName]]}`,
                undefined, anchorModeDisabled  ? 2.75 : undefined // +1s duration if conflicting mode notif shown
            )
        },

        streaming() {
            if (!env.scriptManager.supportsStreaming) { // alert userscript manager unsupported, suggest TM/SC
                const scLink = (
                    env.browser.isFF ?
                        'https://addons.mozilla.org/firefox/addon/scriptcat/'
                  : env.browser.isEdge ?
                        'https://microsoftedge.microsoft.com/addons/detail/scriptcat/liilgpjgabokdklappibcjfablkpcekh'
                      : 'https://chromewebstore.google.com/detail/scriptcat/ndcooeababalnlpkfedmmbbbgkljhpjf' )
                modals.alert(
                    `${settings.controls.streamingDisabled.label} ${app.msgs.alert_unavailable}`,
                    `${settings.controls.streamingDisabled.label} ${app.msgs.alert_isOnlyAvailFor}`
                        + ` <a target="_blank" rel="noopener" href="https://tampermonkey.net">Tampermonkey</a> ${
                                app.msgs.about_and}`
                        + ` <a target="_blank" rel="noopener" href="${scLink}">ScriptCat</a>.`
                        + ` (${app.msgs.alert_userscriptMgrNoStream}.)`
                )
            } else if (!app.config.proxyAPIenabled) { // alert OpenAI API unsupported, suggest Proxy Mode
                let msg = `${settings.controls.streamingDisabled.label} `
                        + `${app.msgs.alert_isCurrentlyOnlyAvailBy} `
                        + `${app.msgs.alert_switchingOn} ${app.msgs.mode_proxy}. `
                        + `(${app.msgs.alert_openAIsupportSoon}!)`
                const switchPhrase = app.msgs.alert_switchingOn
                msg = msg.replace(switchPhrase, `<a class="alert-link" href="#">${switchPhrase}</a>`)
                const alert = modals.alert(`${app.msgs.mode_streaming} ${app.msgs.alert_unavailable}`, msg)
                alert.querySelector('[href="#"]').onclick = () => {
                    alert.querySelector('.modal-close-btn')?.click() ; toggle.proxyMode() }
            } else { // functional toggle
                settings.save('streamingDisabled', !app.config.streamingDisabled)
                feedback.notify(`${settings.controls.streamingDisabled.label} ${
                                   menus.toolbar.state.words[+!app.config.streamingDisabled]}`)
            }
        }
    }

    // Define GET functions

    window.get = {

        json(url) { // requires lib/json5.js
            return new Promise((resolve, reject) => {
                let retryCnt = 0 ; getData(url)

                function getData(currentURL) {
                    xhr({
                        method: 'GET', url: currentURL,
                        onload: ({ responseText, status }) => {
                            if (status >= 300 && status != 404) {
                                const errType = status < 400 ? 'REDIRECT' : status < 500 ? 'CLIENT' : 'SERVER'
                                return reject(new Error(`${errType} ERROR: ${status}`))
                            }
                            try {
                                resolve(currentURL.endsWith('.json') ? JSON.parse(responseText)
                                                                     : JSON5.parse(responseText))
                            } catch (err) {
                                retryCnt < 1 ? tryAltDataFormat() : reject(new Error(`PARSE ERROR: ${err.message}`)) }
                        },
                        onerror: err => {
                            retryCnt < 1 ? tryAltDataFormat() : reject(new Error(`LOAD ERROR: ${err.message}`)) }
                    })
                }

                function tryAltDataFormat() {
                    retryCnt++ ; getData(url.endsWith('.json') ? url + '5' : url.slice(0, -1)) }
            })
        },
        async related(query) {

            // Init API attempt props
            get.related.status = 'waiting'
            get.related.triedAPIs = get.related.triedAPIs || []
            get.related.attemptCnt = get.related.attemptCnt || 1

            // Pick API
            get.related.api = 'OpenAI'

            // Init OpenAI key
            app.config.openAIkey = 'local-dummy-key'

            // Augment query
            const reqAPI = get.related.api
            let rqPrompt = prompts.create('relatedQueries', { prevQuery: query, mods: 'all' })
            rqPrompt = prompts.augment(rqPrompt, { api: reqAPI })

            // Get related queries
            return new Promise(resolve => {
                const reqMethod = 'POST'
                const reqData = api.createReqData(reqAPI, [{ role: 'user', content: rqPrompt }])
                const xhrConfig = {
                    headers: api.createHeaders(reqAPI), method: reqMethod, responseType: 'text',
                    onerror: err => { log.error(err) ; api.tryNew(get.related) },
                    onload: resp => api.process.text(resp, { caller: get.related, callerAPI: reqAPI }).then(resolve),
                    url: localHostEndpoint
                }
                if (reqMethod == 'POST') xhrConfig.data = JSON.stringify(reqData)
                else if (reqMethod == 'GET') xhrConfig.url += `?q=${reqData}`
                xhr(xhrConfig)
            })
        },

        async reply({ msgs, src = null }) {
            get.reply.src = src ; show.reply.updatedAPIinHeader = false

            // Show loading status
            const rqDiv = app.div.querySelector(`.${app.slug}-related-queries`),
                  loadingSpinner = icons.create({ key: 'arrowsCyclic', size: 15 })
            let loadingElem
            loadingSpinner.style.cssText = 'position: relative ; top: 2px ; margin-right: 6px'
            if (app.div.querySelector('.reply-pre')) { // reply exists, show where chatbar was
                if (!/regen|summarize/i.test(src)) rqDiv?.remove() // clear RQs to re-get later
                app.div.querySelector('footer').textContent = '' // clear footer
                loadingElem = app.div.querySelector('section')
                loadingElem.style.margin = '3px 0 -10px'
                loadingElem.textContent = app.alerts.waitingResponse
                loadingSpinner.style.animation = 'rotate 1s infinite cubic-bezier(0, 1.05, 0.79, 0.44)' // faster ver
            } else { // replace app div w/ alert
                feedback.appAlert('waitingResponse')
                loadingElem = app.div.querySelector(`.${app.slug}-alert`)
                loadingSpinner.style.animation = 'rotate 2s infinite linear' // slower ver
            }
            loadingElem.classList.add('loading', 'no-user-select')
            loadingElem.prepend(loadingSpinner)

            // Init msgs
            msgs = structuredClone(msgs) // deep copy to not affect app.msgChain
            if (msgs.length > 3) msgs = msgs.slice(-3) // keep last 3 only
            msgs.forEach(msg => { // trim agent msgs
                if (msg.role == 'assistant' && msg.content.length > 250)
                    msg.content = msg.content.substring(0, 250) + '...' })

            // Init API attempt props
            get.reply.status = 'waiting'
            get.reply.triedAPIs = get.reply.triedAPIs || []
            get.reply.attemptCnt = get.reply.attemptCnt || 1

            // Pick API
            get.reply.api = 'OpenAI'

            // Init OpenAI key
            app.config.openAIkey = 'local-dummy-key'

            // Augment query
            const reqAPI = get.reply.api, lastUserMsg = msgs[msgs.length - 1]
            lastUserMsg.content = prompts.augment(lastUserMsg.content, { api: reqAPI, caller: get.reply })

            // Get/show answer from AI
            const reqMethod = 'POST'
            const reqData = api.createReqData(reqAPI, msgs)
            const xhrConfig = {
                headers: api.createHeaders(reqAPI), method: reqMethod,
                responseType: app.config.streamingDisabled || !app.config.proxyAPIenabled ? 'text' : 'stream',
                onerror: err => { log.error(err)
                    if (!app.config.proxyAPIenabled)
                        feedback.appAlert(!app.config.openAIkey ? 'login' : ['OpenAI', 'apiNotWorking', 'suggestProxy'])
                    else api.tryNew(get.reply)
                },
                onload: resp => api.process.text(resp, { caller: get.reply, callerAPI: reqAPI }),
                onloadstart: resp => api.process.stream(resp, { caller: get.reply, callerAPI: reqAPI }),
                url: localHostEndpoint
            }
            if (reqMethod == 'POST') xhrConfig.data = JSON.stringify(reqData)
            else if (reqMethod == 'GET') xhrConfig.url += `?q=${reqData}`
            xhr(xhrConfig)

            // Get/show Related Queries if enabled/missing/on 1st get.reply() attempt only
            if (!app.config.rqDisabled && !rqDiv && get.reply.attemptCnt == 1)
                get.related(app.msgChain[app.msgChain.length - 1].content)
                    .then(queries => show.related(queries))
                    .catch(err => { log.error(err.message) ; api.tryNew(get.related) })

            update.footerContent()
        }
    }

    // Define SHOW functions

    window.show = {

        async codeCornerBtns() {
            if (!app.div.querySelector('code')) return

            // Init general language data
            window.codeLangData ||= await get.json(`${app.urls.aiwebAssets}/data/code-languages.json`)
                .catch(err => log.error(err.message))

            // Add buttons to every block
            app.div.querySelectorAll('code').forEach(block => {
                if (block.querySelector('[id$=copy-btn]')) return
                const codeBtnsDiv = dom.create.elem('div', { class: `code-header` })

                // Create Copy button
                const copyBtn = buttons.reply.bubble.copy.cloneNode(true)
                copyBtn.style.cssText = '' // clear app header btn styles
                Object.entries(buttons.reply.bubble.copy.listeners).forEach(
                    ([eventType, handler]) => copyBtn[eventType] = handler)

                // Create Download button
                const downloadBtn = dom.create.elem('btn', { id: `${app.slug}-download-btn` })
                const downloadSVGs = {
                    download: icons.create({ key: 'download' }), downloaded: icons.create({ key: 'checkmarkDouble' })}
                Object.entries(downloadSVGs).forEach(([svgType, svg]) => {
                    svg.id = `${app.slug}-${svgType}-icon`
                    ;['width', 'height'].forEach(attr => svg.setAttribute(attr, 15))
                })
                downloadBtn.append(downloadSVGs.download)
                downloadBtn.onclick = ({ currentTarget }) => { // download code, update icon + tooltip status
                    if (!downloadBtn.firstChild.matches('[id$=download-icon]')) return // since clicking on DL'd icon

                    // Update cursor/icon/tooltip
                    downloadBtn.style.cursor = 'default' // remove finger
                    downloadBtn.firstChild.replaceWith(downloadSVGs.downloaded.cloneNode(true)) // change to DL'd icon
                    tooltip.update(currentTarget) // to 'Code downloaded!'
                    setTimeout(() => { // restore icon/cursor/tooltip after a bit
                        downloadBtn.firstChild.replaceWith(downloadSVGs.download.cloneNode(true))
                        downloadBtn.style.cursor = 'pointer'
                        if (downloadBtn.matches(':hover')) // restore tooltip
                            downloadBtn.dispatchEvent(new Event('mouseenter'))
                    }, 10000)

                    // Init block's language data
                    const codeBlock = downloadBtn.closest('code'),
                          blockLang = { hljsSlug: /language-(\w+)/.exec(codeBlock.className)?.[1] }
                    if (blockLang.hljsSlug && window.codeLangData)
                        for (const [langName, langEntry] of Object.entries(window.codeLangData))
                            if (langEntry.hljsSlug == blockLang.hljsSlug) {
                                [blockLang.name, blockLang.fileExtension] = [langName, langEntry.fileExtension]
                                break
                            }

                    // Download code
                    const code = codeBlock.textContent.replace(/^>> /, '').trim() + '\n'
                    const date = new Date().toISOString().split('T')[0] // YYYY-MM-DD
                    const download = `${app.slug}_${blockLang.name.toLowerCase() || 'code'}_${
                        date}_${Date.now().toString(36)}${blockLang.fileExtension ? '.' + blockLang.fileExtension : ''}`
                    const href = URL.createObjectURL(new Blob([code], { type: 'text/plain' }))
                    const a = dom.create.anchor(href, '', { download, style: 'display: none' })
                    document.body.append(a) ; a.click() ; a.remove() ; URL.revokeObjectURL(href)
                }
                downloadBtn.onmouseenter = downloadBtn.onmouseleave = tooltip.toggle

                // Assemble elems
                codeBtnsDiv.append(copyBtn, downloadBtn) ; block.prepend(codeBtnsDiv)
            })
        },

        related(queries) {
            log.caller = 'show.related()'
            if (get.reply.status == 'waiting') // recurse until get.reply() finishes showing answer
                return setTimeout(() => show.related(queries), 500, queries)

            // Re-get.related() if current reply is question to suggest answers
            const currentReply = app.div.querySelector(`#${app.slug} .reply-pre`)?.textContent.trim()
            if (!/shuffle|summarize/i.test(get.reply.src)
                    && !get.related.replyIsQuestion && /[??]/.test(currentReply)) {
                log.debug('Re-getting related queries to answer reply question...')
                get.related.replyIsQuestion = true
                get.related(currentReply).then(queries => show.related(queries))
                    .catch(err => { log.error(err.message) ; api.tryNew(get.related) })
            }

            // Show the queries
            else if (queries && !app.div.querySelector(`.${app.slug}-related-queries`)) {

                // Create/classify/append parent div
                const rqsDiv = dom.create.elem('div', { class: `${app.slug}-related-queries anchored-hidden` })
                app.div.append(rqsDiv)

                // Fill each child div, add attributes + icon + listener
                queries.forEach((query, idx) => {
                    const rqDiv = dom.create.elem('div', {
                        title: app.msgs.tooltip_sendRelatedQuery, tabindex: 0,
                        class: `${app.slug}-related-query fade-in no-user-select no-mobile-tap-outline` })
                    rqDiv.textContent = query ; rqDiv.prepend(icons.create({ key: 'arrowDownRight' }))
                    rqsDiv.append(rqDiv)
                    setTimeout(() => { // add fade + listeners
                        rqDiv.classList.add('active')
                        rqDiv.onclick = rqDiv.onkeydown = event => {
                            const keys = [' ', 'Spacebar', 'Enter', 'Return'], keyCodes = [32, 13]
                            if (keys.includes(event.key) || keyCodes.includes(event.keyCode) || event.type == 'click') {
                                event.preventDefault() // prevent scroll on space taps
                                const chatbar = app.div.querySelector('textarea') ; if (!chatbar) return
                                const relatedQuery = event.target.textContent ; chatbar.value = relatedQuery
                                if (/\[[^[\]]+\]/.test(relatedQuery)) { // highlight 1st bracleted placeholder
                                    chatbar.focus()
                                    ui.addListeners.replySection.chatbarAutoSizer() // since query not auto-sent
                                    chatbar.setSelectionRange(relatedQuery.indexOf('['), relatedQuery.indexOf(']') +1)
                                } else // send placeholder-free related query
                                    chatbar.dispatchEvent(new KeyboardEvent('keydown',
                                        { key: 'Enter', bubbles: true, cancelable: true }))
                            }
                        }
                    }, (idx+1) *50)
                })

                replyBubble.updateMaxHeight() ; get.related.replyIsQuestion = null
            }
        },

        reply({ content, standby = false, apiUsed = null }) {
            show.reply.shareURL = null // reset to regen using longer app.msgChain
            tooltip.toggle('off') // hide lingering tooltip if cursor was on corner button
            const regenSVGwrapper = app.div.querySelector('[id$=regen-btn]')?.firstChild
            if (regenSVGwrapper?.style?.animation) { // remove animation, restore cursor/tooltip
                regenSVGwrapper.style.animation = regenSVGwrapper.style.cursor = ''
                const regenBtn = regenSVGwrapper.closest('btn')
                if (regenBtn.matches(':hover')) // restore tooltip
                    regenBtn.dispatchEvent(new Event('mouseenter'))
            }

            // Build answer interface up to reply section if missing
            if (!app.div.querySelector('.reply-pre')) {
                app.div.textContent = '' ; css.addRisingParticles(app.div)

                // Create/append header div
                const appHeaderDiv = dom.create.elem('div', { class: 'app-header', style: 'margin: -8px 0 0 0' })
                app.div.append(appHeaderDiv)

                // Create/append title
                const appPrefixSpan = dom.create.elem('span', {
                    id: 'app-prefix', class: 'no-user-select',
                    style: `margin-right: -2px ; font-size: ${ env.browser.isMobile ? '1.7rem' : '1.1rem' }`})
                appPrefixSpan.textContent = '🤖 ' ; appHeaderDiv.append(appPrefixSpan)
                const appHeaderLogo = logos.googlegpt.create()
                appHeaderLogo.width = env.browser.isMobile ? 177 : env.browser.isFF ? 124 : 122
                appHeaderLogo.style.cssText = (
                    `position: relative ; top: ${ env.browser.isMobile ? 4 : env.browser.isFF ? 3 : 2 }px`
                  + ( env.browser.isMobile ? '; margin-left: 1px' : '' ))
                const appTitleAnchor = dom.create.anchor(app.urls.app, appHeaderLogo)
                appTitleAnchor.classList.add(`${app.slug}-name`, 'no-user-select')
                appHeaderDiv.append(appTitleAnchor)

                // Create/append header buttons div
                const headerBtnsDiv = dom.create.elem('div', {
                    id: `${app.slug}-header-btns`, class: 'no-mobile-tap-outline', style: 'margin-top: 1px' })
                appHeaderDiv.append(headerBtnsDiv)

                // Create/append Chevron button
                if (!env.browser.isMobile) {
                    var chevronBtn = dom.create.elem('btn', {
                        id: `${app.slug}-chevron-btn`, class: `${app.slug}-header-btn anchored-only`,
                        style: 'margin: -3.5px 1px 0 11px' })
                    chevronBtn.append(icons.create({ key: `chevron${ app.config.minimized ? 'Up' : 'Down' }`,
                        size: 22, style: 'position: relative ; top: -1px' }))
                    headerBtnsDiv.append(chevronBtn)
                }

                // Create/append About button
                const aboutBtn = dom.create.elem('btn', {
                    id: `${app.slug}-about-btn`, class: `${app.slug}-header-btn`,
                    style: `margin-top: ${ env.browser.isMobile ? 0.25 : -0.15 }rem`})
                aboutBtn.append(icons.create({ key: 'questionMarkCircle' })) ; headerBtnsDiv.append(aboutBtn)

                // Create/append Settings button
                const settingsBtn = dom.create.elem('btn',{
                    id: `${app.slug}-settings-btn`, class: `${app.slug}-header-btn`,
                    style: `margin: ${ env.browser.isMobile ? 6 : 0 }px 10px 0 4.5px` })
                settingsBtn.append(icons.create({ key: 'sliders', size: 17 })) ; headerBtnsDiv.append(settingsBtn)

                // Create/append Font Size button
                if (!standby) {
                    var fontSizeBtn = dom.create.elem('btn', {
                        id: `${app.slug}-font-size-btn`, class: `${app.slug}-header-btn app-hover-only`,
                        style: `margin: ${ env.browser.isMobile ? 5 : -2 }px 9px 0 0` })
                    fontSizeBtn.append(icons.create({ key: 'fontSize' })) ; headerBtnsDiv.append(fontSizeBtn)
                }

                // Create/append Pin button
                if (!env.browser.isMobile) {
                    var pinBtn = dom.create.elem('btn', {
                        id: `${app.slug}-pin-btn`, class: `${app.slug}-header-btn app-hover-only`,
                        style: 'margin: -1.55px 9.5px 0 0' })
                    pinBtn.append(icons.create({ key: 'pin', size: 16.5, style: 'position: relative ; top: 2px' }))
                    headerBtnsDiv.append(pinBtn)

                // Create/append Wider Sidebar button
                    var wsbBtn = dom.create.elem('btn', {
                        id: `${app.slug}-wsb-btn`, class: `${app.slug}-header-btn app-hover-only anchored-hidden`,
                        style: 'margin: -0.5px 12px 0 0' })
                    wsbBtn.append(icons.create({ key: `widescreen${ app.config.widerSidebar ? 'Wide' : 'Tall' }`}))
                    headerBtnsDiv.append(wsbBtn)

                // Create/append Expand/Shrink button
                    var arrowsBtn = dom.create.elem('btn', {
                        id: `${app.slug}-arrows-btn`, class: `${app.slug}-header-btn app-hover-only anchored-only`,
                        style: 'margin: 0 11.5px 0 0' })
                    arrowsBtn.append(icons.create({
                        key: `arrowsDiagonal${ app.config.expanded ? 'In' : 'Out' }`, size: 17 }))
                    headerBtnsDiv.append(arrowsBtn)
                }

                // Add app header button listeners
                ui.addListeners.btns.appHeader()

                // Create/append 'by KudoAI' if it fits
                if (!env.browser.isMobile) {
                    const bylineSpan = dom.create.elem('span', { class: 'byline no-user-select', textContent: 'by ' })
                    bylineSpan.append(dom.create.anchor(app.urls.publisher, 'KudoAI'))
                    appHeaderDiv.querySelector(`.${app.slug}-name`).insertAdjacentElement('afterend', bylineSpan)
                    update.bylineVisibility()
                }

                // Show standby state if prefix/suffix mode on
                if (standby) {
                    const standbyBtnsDiv = dom.create.elem('div', {
                        class: `${app.slug}-standby-btns`, style: 'will-change: transform' })
                    ;['query', 'summarize'].forEach(btnType => {
                        const btn = {
                            node: dom.create.elem('button', {
                                class: `${app.slug}-standby-btn no-mobile-tap-outline` }),
                            icon: icons.create({ key: btnType == 'query' ? 'send' : 'summarize' }),
                            textSpan: dom.create.elem('span')
                        }
                        btn.textSpan.textContent = btnType == 'query' ?
                            `${app.msgs.btnLabel_sendSearchQueryTo} ${app.name}` : app.msgs.tooltip_summarizeResults
                        btn.node.onclick = () => {
                            show.reply.userInteracted = true ; show.reply.chatbarFocused = false
                            app.msgChain.push({ role: 'user', content:
                                btnType == 'summarize' ? prompts.create('summarizeResults')
                                                       : new URL(location.href).searchParams.get('q') })
                            get.reply({ msgs: app.msgChain, src: btnType })
                        }
                        btn.node.append(btn.icon, btn.textSpan) ; standbyBtnsDiv.append(btn.node)
                    })
                    app.div.append(standbyBtnsDiv)

                // Otherwise create/append answer bubble section
                } else replyBubble.insert()
            }

            // Build reply section if missing
            if (!app.div.querySelector(`#${app.slug}-chatbar`)) {

                // Init/clear user reply section content/classes/style
                const replySection = app.div.querySelector('section') || dom.create.elem('section')
                if (replySection.className.includes('loading'))
                    replySection.textContent = replySection.className = replySection.style = ''

                // Create/append section elems
                const replyForm = dom.create.elem('form')
                const continueChatDiv = dom.create.elem('div')
                const chatTextarea = dom.create.elem('textarea', {
                    id: `${app.slug}-chatbar`, rows: 1,
                    placeholder: `${app.msgs[standby ? 'placeholder_askSomethingElse' : 'tooltip_sendReply']}...`
                })
                continueChatDiv.append(chatTextarea)
                replyForm.append(continueChatDiv) ; replySection.append(replyForm)
                app.div.querySelector('.reply-bubble, [class*=standby-btns]').after(replySection)

                // Create/append chatbar buttons
                ;['send', 'shuffle', 'summarize'].forEach((btnType, idx) => {
                    if (btnType == 'summarize' && app.div.querySelector('[class*=standby-btn]'))
                        return // since big Summarize button exists
                    const btn = dom.create.elem('button', {
                        id: `${app.slug}-${btnType}-btn`, class: `${app.slug}-chatbar-btn no-mobile-tap-outline` })
                    btn.style.right = `${ idx == 0 ? 3 : idx == 1 ? -3 : -5 }px`
                    btn.append(icons.create({ key: btnType, size: btnType == 'send' ? 14 : 18 }))
                    continueChatDiv.append(btn)
                })

                // Init/fill/append footer
                app.footer = app.div.querySelector('footer') || dom.create.elem('footer')
                app.footer.append(app.footerContent)
                if (!app.div.querySelector('footer')) app.div.append(app.footer)

                // Add listeners
                ui.addListeners.replySection()

                // Scroll to top on mobile if user interacted
                if (env.browser.isMobile && show.reply.userInteracted) {
                    document.body.scrollTop = 0 // Safari
                    document.documentElement.scrollTop = 0 // Chromium/FF/IE
                }
            }

            // Render/show answer if query sent
            if (!standby) {

                // Show API used in bubble header
                if (!show.reply.updatedAPIinHeader) {
                    show.reply.updatedAPIinHeader = true
                    const preHeaderLabel = app.div.querySelector('.reply-header-txt')
                    const apiBeacon = dom.create.elem('span',
                        { class: 'api-btn', style: 'cursor: pointer', textContent: '⦿' })
                    apiBeacon.onmouseenter = apiBeacon.onmouseleave = apiBeacon.onclick = menus.hover.toggle
                    apiBeacon.style.pointerEvents = app.config.proxyAPIenabled ? '' : 'none'
                    preHeaderLabel.replaceChildren(
                        apiBeacon, ` API ${app.msgs.componentLabel_used}: `, dom.create.elem('b'))
                    setTimeout(() => type(apiUsed, preHeaderLabel.lastChild, { speed: 1.5 }), 150)
                    function type(text, targetElem, { speed = 1 } = {}) {
                        targetElem.textContent = '' ; let i = 0;
                        (function typeNextChar() {
                            if (i < text.length) {
                                targetElem.textContent += text[i] ; i++ ; setTimeout(typeNextChar, 50 / speed) }
                        })()
                    }
                }

                // Render MD, highlight code
                const replyPre = app.div.querySelector('.reply-pre')
                try { // to render markdown
                    replyPre.innerHTML = marked.parse(content) } catch (err) { log.error(err.message) }
                hljs.highlightAll() // highlight code
                replyPre.querySelectorAll('code').forEach(codeBlock => { // add linebreaks after semicolons
                    codeBlock.innerHTML = codeBlock.innerHTML.replace(/;\s*/g, ';<br>') })
                update.replyPrefix() // prepend '>> ' if dark scheme w/ bg animations to emulate terminal

                // Typeset math
                ;[replyPre, ...replyPre.querySelectorAll('*')].forEach(elem =>
                    renderMathInElement(elem, { delimiters: app.katexDelimiters, throwOnError: false }))

                if (app.config.stickySidebar) replyBubble.updateMaxHeight()

                // Auto-scroll if active
                if (app.config.autoScroll && !env.browser.isMobile && app.config.proxyAPIenabled
                    && !app.config.streamingDisabled)
                {
                    if (app.config.stickySidebar || app.config.anchored) replyPre.scrollTop = replyPre.scrollHeight
                    else scrollBy({ top: app.div.querySelector(`#${app.slug}-chatbar`)
                        .getBoundingClientRect().bottom - innerHeight +13 })
                }
            }

            // Focus chatbar conditionally
            if (!show.reply.chatbarFocused // do only once
                && !env.browser.isMobile // exclude mobile devices to not auto-popup OSD keyboard
                && ((!app.config.autoFocusChatbarDisabled && ( app.config.anchored // include Anchored mode if AF enabled
                        // ...or un-Anchored if fully above fold
                        || ( app.div.offsetHeight < innerHeight - app.div.getBoundingClientRect().top )))
                    // ...or Anchored if AF disabled & user interacted
                    || (app.config.autoFocusChatbarDisabled && app.config.anchored && show.reply.userInteracted))
            ) { app.div.querySelector(`#${app.slug}-chatbar`).focus() ; show.reply.chatbarFocused = true }

            // Update styles
            if (app.config.anchored) update.appBottomPos() // restore minimized/restored state if anchored
            update.chatbarWidth()

            show.reply.userInteracted = false
        }
    }

    // Define COMPONENTS

    window.fontSizeSlider = { // requires lib/<dom|settings>.js + <app|env|inputEvents>
        fadeInDelay: 5, // ms
        hWheelDistance: 10, // px

        createAppend() { // requires lib/<dom|settings>.js + <app|env|inputEvents>

            // Create/ID/classify slider elems
            fontSizeSlider.cursorOverlay = dom.create.elem('div', { class: 'cursor-overlay' })
            const slider = dom.create.elem('div',
                { id: `${app.slug}-font-size-slider-track`, class: 'fade-in-less', style: 'display: none' })
            const sliderThumb = dom.create.elem('div',
                { title: Math.floor(app.config.fontSize *10) /10 + 'px', id: `${app.slug}-font-size-slider-thumb` })
            const sliderTip = dom.create.elem('div', { id: `${app.slug}-font-size-slider-tip` })

            // Assemble/insert elems
            slider.append(sliderThumb, sliderTip)
            app.div.insertBefore(slider, app.div.querySelector(`.${app.slug}-tooltip,` // desktop
                                                           + '.reply-bubble')) // mobile
            // Init thumb pos
            setTimeout(() => {
                const iniLeft = (app.config.fontSize - app.config.minFontSize)
                              / (app.config.maxFontSize - app.config.minFontSize)
                              * (slider.offsetWidth - sliderThumb.offsetWidth) // slider width
                sliderThumb.style.left = iniLeft + 'px'
            }, fontSizeSlider.fadeInDelay) // to ensure visibility for accurate dimension calcs

            // Add event listeners for dragging thumb
            let isDragging = false, startX, startLeft
            sliderThumb.addEventListener(inputEvents.down, event => {
                if (event.button != 0) return // prevent non-left-click drag
                event.preventDefault() // prevent text selection
                isDragging = true ; startX = event.clientX ; startLeft = sliderThumb.offsetLeft
                document.body.append(fontSizeSlider.cursorOverlay)
            })
            document.addEventListener(inputEvents.move, ({ clientX }) => {
                if (isDragging) moveThumb(startLeft + clientX - startX) })
            document.addEventListener(inputEvents.up, () => {
                isDragging = false
                if (fontSizeSlider.cursorOverlay?.isConnected) fontSizeSlider.cursorOverlay.remove()
            })

            // Add event listener for wheel-scrolling thumb
            if (!env.browser.isMobile) slider.onwheel = event => {
                event.preventDefault()
                moveThumb(sliderThumb.offsetLeft - Math.sign(event.deltaY) * fontSizeSlider.hWheelDistance)
            }

            // Add event listener for seek/dragging by inputEvents.down on track
            slider.addEventListener(inputEvents.down, event => {
                if (event.button != 0) return // prevent non-left-click drag
                event.preventDefault() // prevent text selection
                const clientX = event.clientX || event.touches?.[0]?.clientX
                moveThumb(clientX - slider.getBoundingClientRect().left - sliderThumb.offsetWidth / 2)
                isDragging = true ; startX = clientX ; startLeft = sliderThumb.offsetLeft // manually init dragging
                document.body.append(fontSizeSlider.cursorOverlay)
            })

            function moveThumb(newLeft) {

                // Bound thumb
                const sliderWidth = slider.offsetWidth - sliderThumb.offsetWidth
                if (newLeft < 0) newLeft = 0
                if (newLeft > sliderWidth) newLeft = sliderWidth

                // Move thumb
                sliderThumb.style.left = newLeft + 'px'

                // Adjust font size based on thumb position
                const replyPre = app.div.querySelector('.reply-pre')
                const fontSizePercent = newLeft / sliderWidth
                const fontSize = app.config.minFontSize + fontSizePercent
                               *(app.config.maxFontSize - app.config.minFontSize)
                replyPre.style.fontSize = fontSize + 'px'
                replyPre.style.lineHeight = fontSize * app.config.lineHeightRatio + 'px'
                settings.save('fontSize', fontSize)
                sliderThumb.title = Math.floor(app.config.fontSize *10) /10 + 'px'
            }

            return slider
        },

        toggle(state = '') { // requires app
            const slider = document.getElementById(`${app.slug}-font-size-slider-track`)
                         || fontSizeSlider.createAppend()
            const replyTip = app.div.querySelector('.reply-tip')
            const sliderTip = document.getElementById(`${app.slug}-font-size-slider-tip`)

            // Show slider
            if (state == 'on' || (!state && slider.style.display == 'none')) {

                // Position slider tip
                const btnSpan = document.getElementById(`${app.slug}-font-size-btn`)
                const rects = { appDiv: app.div.getBoundingClientRect(), btnSpan: btnSpan.getBoundingClientRect() }
                sliderTip.style.right = `${
                    rects.appDiv.right - ( rects.btnSpan.left + rects.btnSpan.right )/2 -35.5 }px`

                // Show slider, hide reply tip
                slider.style.display = sliderTip.style.display = '' ; if (replyTip) replyTip.style.display = 'none'
                setTimeout(() => slider.classList.add('active'), fontSizeSlider.fadeInDelay)

            // Hide slider
            } else if (state == 'off' || (!state && slider.style.display != 'none')) {
                slider.classList.remove('active') ; if (replyTip) replyTip.style.display = ''
                sliderTip.style.display = slider.style.display = 'none'
            }
        }
    }

    const logos = { // requires dom.js + <app|env> + GM_getResourceText()
        googlegpt: {

            create() { // requires dom.js + app
                const googlegptlogo = dom.create.elem('img', { id: `${app.slug}-logo`, class: 'no-mobile-tap-outline' })
                logos.googlegpt.update(googlegptlogo)
                return googlegptlogo
            },

            update(...targetLogos) { // requires <app|env> + GM_getResourceText()
                targetLogos = targetLogos.flat() // flatten array args nested by spread operator
                if (!targetLogos.length) targetLogos = document.querySelectorAll(`#${app.slug}-logo`)
                targetLogos.forEach(logo =>
                    logo.src = GM_getResourceText(`ggpt${ env.ui.app.scheme == 'dark' ? 'DS' : 'LS' }logo`))
            }
        }
    }

    window.modals = {
        stack: [], // of types of undismissed modals
        class: `${app.slug}-modal`,

        about() {

            // Show modal
            const labelStyles = 'text-transform: uppercase ; font-size: 16px ; font-weight: bold ;'
                              + `color: ${ env.ui.app.scheme == 'dark' ? 'white' : '#494141' }`
            const aboutModal = modals.alert(
                `${app.symbol} ${app.msgs.appName}`, // title
                `<span style="${labelStyles}">🧠 ${app.msgs.about_author}:</span> `
                    + `<a href="${app.author[0].url}" target="_blank" rel="nopener">${app.author[0].name}</a> `
                        + `${app.msgs.about_and} <a href="${app.urls.contributors}" target="_blank" rel="nopener">`
                        + `${app.msgs.about_contributors}</a>\n`
                + `<span style="${labelStyles}">🏷️ ${app.msgs.about_version}:</span> `
                    + `<span class="about-em">${app.version}</span>\n`
                + `<span style="${labelStyles}">📜 ${app.msgs.about_openSourceCode}:</span> `
                    + `<a href="${app.urls.github}" target="_blank" rel="nopener">`
                        + app.urls.github + '</a>\n'
                + `<span style="${labelStyles}">🚀 ${app.msgs.about_latestChanges}:</span> `
                    + `<a href="${app.urls.github}/commits" target="_blank" rel="nopener">`
                        + `${app.urls.github}/commits</a>\n`
                + `<span style="${labelStyles}">⚡ ${app.msgs.about_poweredBy}:</span> `
                    + `<a href="${app.urls.chatgptjs}" target="_blank" rel="noopener">chatgpt.js</a>`
                        + ` v${app.chatgptjsVer}`,
                [ // buttons
                    function checkForUpdates() { userscript.updateCheck() },
                    function getSupport(){},
                    function rateUs(){},
                    function moreAIextensions(){}
                ], '', 629 // modal width
            )

            // Add logo
            const aboutHeaderLogo = logos.googlegpt.create() ; aboutHeaderLogo.width = 405
            aboutHeaderLogo.style.cssText = `max-width: 98% ; margin: 15px ${
                env.browser.isMobile ? 'auto' : '14.5%' } -1px`
            aboutModal.firstChild.nextElementSibling.before(aboutHeaderLogo) // after close btn

            // Center text
            aboutModal.querySelector('h2').remove() // remove empty title h2
            aboutModal.querySelector('p').style.cssText = `
                overflow-wrap: anywhere ; line-height: 1.55 ;
                margin: ${ env.browser.isCompact ? '21px 0 -20px' : '15px 0 -28px 17px' }`

            // Hack buttons
            aboutModal.querySelectorAll('button').forEach(btn => {
                btn.style.cssText = 'height: 50px ; min-width: 136px'

                // Replace link buttons w/ clones that don't dismiss modal
                if (/support|rate|extensions/i.test(btn.textContent)) {
                    btn.replaceWith(btn = btn.cloneNode(true))
                    btn.onclick = () => modals.safeWinOpen(
                        btn.textContent.includes(app.msgs.btnLabel_getSupport) ? app.urls.support
                      : app.urls.relatedExtensions)
                }

                // Prepend emoji + localize labels
                if (/updates/i.test(btn.textContent))
                    btn.textContent = `🚀 ${app.msgs.btnLabel_checkForUpdates}`
                else if (/support/i.test(btn.textContent))
                    btn.textContent = `🧠 ${app.msgs.btnLabel_getSupport}`
                else if (/rate/i.test(btn.textContent))
                    btn.textContent = `⭐ ${app.msgs.btnLabel_rateUs}`
                else if (/extensions/i.test(btn.textContent))
                    btn.textContent = `🤖 ${app.msgs.btnLabel_moreAIextensions}`

                // Hide Dismiss button
                else btn.style.display = 'none'
            })

            return aboutModal
        },

        alert(title = '', msg = '', btns = '', checkbox = '', width = '') { // generic one from chatgpt.alert()
            const alertID = chatgpt.alert(title, msg, btns, checkbox, width),
                  alert = document.getElementById(alertID).firstChild
            this.init(alert) // add classes/listeners/hack bg
            return alert
        },

        api() { // requires lib/feedback.js + <apis|app|get|settings>

            // Show modal
            const modalBtns = [app.msgs.menuLabel_random, ...Object.keys(apis).filter(api => api != 'OpenAI')]
                .map(api => { // to btn callback/label
                    function onclick() {
                        settings.save('preferredAPI', api == app.msgs.menuLabel_random ? false : api)
                        if (modals.settings.get()) { // update status of Preferred API entry
                            const preferredAPIstatus = document.querySelector('[id*=preferredAPI] > span')
                            if (preferredAPIstatus.textContent != api) preferredAPIstatus.textContent = api
                        }
                        feedback.notify(`${app.msgs.menuLabel_preferred} API ${app.msgs.menuLabel_saved.toLowerCase()}`,
                            `${ app.config.anchored ? 'top' : 'bottom' }-right`)
                        if (app.div.querySelector(`.${app.slug}-alert`) && app.config.proxyAPIenabled)
                            get.reply({ msgs: app.msgChain, src: get.reply.src }) // re-send query if user alerted
                    }
                    Object.defineProperty(onclick, 'name', { value: api.toLowerCase() })
                    return onclick
                })
            const apiModal = modals.alert(`${app.msgs.menuLabel_preferred} API:`, '', modalBtns, '', 503)

            // Re-style elems
            apiModal.querySelector('h2').style.justifySelf = 'center' // center title
            const btnsDiv = apiModal.querySelector('.modal-buttons')
            btnsDiv.style.cssText = `margin: 18px 0px 4px !important ; ${ env.browser.isCompact ? ''
                : 'flex-wrap: wrap ; justify-content: center ; gap: 9px' }` // gridify wide view btns
            btnsDiv.querySelectorAll('button').forEach((btn, idx) => {
                if (idx == 0) btn.style.display = 'none' // hide Dismiss button
                else btn.classList.toggle('primary-modal-btn', // emphasize preferred API
                    app.config.preferredAPI && app.config.preferredAPI.toLowerCase() == btn.textContent.toLowerCase()
                        || btn.textContent == app.msgs.menuLabel_random && !app.config.preferredAPI)
            })

            return apiModal
        },

        handlers: {

            dismiss: { // to dismiss native modals
                click(event) {
                    const clickedElem = event.target,
                          settingsCrown = document.querySelector(`#${app.slug}-settings > img`)
                    if (settingsCrown) { // return if cursor within bounds
                        const crownRect = settingsCrown.getBoundingClientRect()
                        if (event.clientX >= crownRect.left && event.clientX <= crownRect.right
                            && event.clientY >= crownRect.top && event.clientY <= crownRect.bottom
                        ) return
                    }
                    if (clickedElem == event.currentTarget || clickedElem.closest('[class*=-close-btn]'))
                        modals.hide((clickedElem.closest('[class*=-modal-bg]') || clickedElem).firstChild)
                },

                key(event) {
                    if (event.key.startsWith('Esc') || event.keyCode == 27)
                        modals.hide(document.querySelector('[class$=-modal]'))
                }
            },

            drag: {

                mousedown(event) { // find modal, update styles, attach listeners, init XY offsets
                    if ( // prevent drag when...
                        event.button != 0 // non-left-click
                        || !/auto|default/.test(getComputedStyle(event.target).cursor) // cursor changed
                        || event.target.closest('ul') // entry elem
                    ) return
                    modals.draggingModal = event.currentTarget
                    event.preventDefault() // prevent sub-elems like icons being draggable
                    Object.assign(modals.draggingModal.style, { // update styles
                        transform: 'scale(1.05)',
                        transition: '0.1s', '-webkit-transition': '0.1s', '-moz-transition': '0.1s',
                            '-o-transition': '0.1s', '-ms-transition': '0.1s'
                    })
                    document.body.style.cursor = 'grabbing' // update cursor
                    ;[...modals.draggingModal.children] // prevent hover FX if drag lags behind cursor
                        .forEach(child => child.style.pointerEvents = 'none')
                    ;['mousemove', 'mouseup'].forEach(eventType => // add listeners
                        document.addEventListener(eventType, modals.handlers.drag[eventType]))
                    const draggingModalRect = modals.draggingModal.getBoundingClientRect()
                    modals.handlers.drag.offsetX = event.clientX - draggingModalRect.left +21
                    modals.handlers.drag.offsetY = event.clientY - draggingModalRect.top +12
                },

                mousemove(event) { // drag modal
                    if (modals.draggingModal) {
                        const newX = event.clientX - modals.handlers.drag.offsetX,
                              newY = event.clientY - modals.handlers.drag.offsetY
                        Object.assign(modals.draggingModal.style, { left: `${newX}px`, top: `${newY}px` })
                    }
                },

                mouseup() { // restore styles/pointer events, remove listeners, reset modals.draggingModal
                    Object.assign(modals.draggingModal.style, { // restore styles
                        cursor: 'inherit', transform: 'scale(1)',
                        transition: 'inherit', '-webkit-transition': 'inherit', '-moz-transition': 'inherit',
                            '-o-transition': 'inherit', '-ms-transition': 'inherit'
                    })
                    document.body.style.cursor = '' // restore cursor
                    ;[...modals.draggingModal.children] // restore pointer events
                        .forEach(child => child.style.pointerEvents = '')
                    ;['mousemove', 'mouseup'].forEach(eventType => // remove listeners
                        document.removeEventListener(eventType, modals.handlers.drag[eventType]))
                    modals.draggingModal = null
                }
            }
        },

        hide(modal) {
            const modalContainer = modal?.parentNode ; if (!modalContainer) return
            modalContainer.style.animation = 'modal-zoom-fade-out 0.165s ease-out'
            modalContainer.onanimationend = () => modalContainer.remove()
        },

        init(modal) { // requires lib/dom.js
            this.stylize()
            modal.classList.add('no-user-select', this.class) ; modal.parentNode.classList.add(`${this.class}-bg`)

            // Add listeners
            modal.onwheel = modal.ontouchmove = event => event.preventDefault() // disable wheel/swipe scrolling
            modal.onmousedown = this.handlers.drag.mousedown // enable click-dragging
            if (!modal.parentNode.className.includes('chatgpt-modal')) { // enable click-dismissing native modals
                const dismissElems = [modal.parentNode, modal.querySelector('[class*=-close-btn]')]
                dismissElems.forEach(elem => elem.onclick = this.handlers.dismiss.click)
            }

            // Hack BG
            css.addRisingParticles(modal)
            setTimeout(() => { // dim bg
                modal.parentNode.style.backgroundColor = `rgba(67,70,72,${
                    env.ui.app.scheme == 'dark' ? 0.62 : 0.33 })`
                modal.parentNode.classList.add('animated')
            }, 100) // delay for transition fx
        },

        observeRemoval(modal, modalType, modalSubType) { // to maintain stack for proper nav
            const modalBG = modal.parentNode
            new MutationObserver(([mutation], obs) => {
                mutation.removedNodes.forEach(removedNode => { if (removedNode == modalBG) {
                    if (modals.stack[0].includes(modalSubType || modalType)) { // new modal not launched so nav back
                        modals.stack.shift() // remove this modal type from stack 1st
                        const prevModalType = modals.stack[0]
                        if (prevModalType) { // open it
                            modals.stack.shift() // remove type from stack since re-added on open
                            modals.open(prevModalType)
                        }
                    }
                    obs.disconnect()
                }})
            }).observe(modalBG.parentNode, { childList: true, subtree: true })
        },

        open(modalType, modalSubType) { // custom ones
            const modal = modalSubType ? modals[modalType][modalSubType]()
                        : (modals[modalType].show || modals[modalType])()
            if (!modal) return // since no div returned
            if (settings.controls[modalType]?.type != 'prompt') { // add to stack
                this.stack.unshift(modalSubType ? `${modalType}_${modalSubType}` : modalType)
                log.debug(`Modal stack: ${JSON.stringify(modals.stack)}`)
            }
            this.init(modal) // add classes/listeners/hack bg
            this.observeRemoval(modal, modalType, modalSubType) // to maintain stack for proper nav
            if (!modals.handlers.dismiss.key.added) { // add key listener to dismiss modals
                document.addEventListener('keydown', modals.handlers.dismiss.key)
                modals.handlers.dismiss.key.added = true
            }
        },

        replyLang() { // requires <app|env|log|modals|settings>
            let replyLang = prompt(`${app.msgs.prompt_updateReplyLang}:`, app.config.replyLang)
            if (replyLang == null) return // user cancelled so do nothing
            else if (!/\d/.test(replyLang)) {
                replyLang = ( // auto-case for menu/alert aesthetics
                    replyLang.length < 4 || replyLang.includes('-') ? replyLang.toUpperCase()
                        : log.toTitleCase(replyLang) )
                settings.save('replyLang', replyLang || env.browser.language)
                modals.alert(`${app.msgs.alert_langUpdated}!`, // title
                    `${app.name} ${ // msg
                        app.msgs.alert_willReplyIn} ${ replyLang || app.msgs.alert_yourSysLang }.`,
                    '', '', 330) // modal width
                if (modals.settings.get()) // update settings menu status label
                    document.querySelector('#replyLang-settings-entry span').textContent = replyLang
            }
        },

        safeWinOpen(url) { open(url, '_blank', 'noopener') }, // to prevent backdoor vulnerabilities

        scheme() {

            // Show modal
            const schemeModal = modals.alert(`${
                app.name } ${( app.msgs.menuLabel_colorScheme ).toLowerCase() }:`, '', // title
                [ function auto(){}, function light(){}, function dark(){} ] // buttons
            )

            // Center title/button cluster
            schemeModal.querySelector('h2').style.justifySelf = 'center'
            schemeModal.querySelector('.modal-buttons').style.cssText = `
                justify-content: center ; margin: 20px 0 9px !important`

            // Hack buttons
            const schemeEmojis = { 'light': '☀️', 'dark': '🌘', 'auto': '🌗'}
            schemeModal.querySelectorAll('button').forEach(btn => {
                const btnScheme = btn.textContent.toLowerCase()

                // Emphasize active scheme
                btn.classList.toggle('primary-modal-btn',
                    app.config.scheme == btn.textContent.toLowerCase() || (btn.textContent == 'Auto' && !app.config.scheme))

                // Prepend emoji + localize labels
                if (Object.prototype.hasOwnProperty.call(schemeEmojis, btnScheme))
                    btn.textContent = `${schemeEmojis[btnScheme]} ${ // emoji
                        app.msgs['scheme_' + btnScheme] || app.msgs['menuLabel_' + btnScheme]
                            || btnScheme.toUpperCase() }`
                else btn.style.display = 'none' // hide Dismiss button

                // Clone button to replace listener to not dismiss modal on click
                btn.replaceWith(btn = btn.cloneNode(true))
                btn.onclick = () => {
                    const newScheme = btnScheme == 'auto' ? ui.getScheme() : btnScheme
                    settings.save('scheme', btnScheme == 'auto' ? false : newScheme)
                    schemeModal.querySelectorAll('button').forEach(btn =>
                        btn.classList.remove('primary-modal-btn')) // clear prev emphasized active scheme
                    btn.classList.add('primary-modal-btn') // emphasize newly active scheme
                    btn.style.cssText = 'pointer-events: none' // disable hover fx to show emphasis
                    setTimeout(() => { btn.style.pointerEvents = 'auto' }, // re-enable hover fx
                        100) // ...after 100ms to flicker emphasis
                    update.scheme(newScheme) ; schemeNotify(btnScheme)
                }
            })

            function schemeNotify(scheme) {

                // Show notification
                feedback.notify(`${app.msgs.menuLabel_colorScheme}: ${(
                    scheme == 'light' ? app.msgs.scheme_light || 'Light'
                  : scheme == 'dark'  ? app.msgs.scheme_dark  || 'Dark'
                                      : app.msgs.menuLabel_auto
                ).toUpperCase()}`)

                // Append scheme icon
                const notifs = document.querySelectorAll('.chatgpt-notif'), notif = notifs[notifs.length -1]
                notif.append(icons.create({
                    key: scheme == 'light' ? 'sun' : scheme == 'dark' ? 'moon' : 'arrowsCyclic',
                    style: 'width: 23px ; height: 23px ; position: relative ; top: 1px ; margin-left: 6px'
                }))
            }
            return schemeModal
        },

        settings: {

            createAppend() {

                // Init master elems
                const settingsContainer = dom.create.elem('div'),
                      settingsModal = dom.create.elem('div', { id: `${app.slug}-settings` })
                      settingsContainer.append(settingsModal)

                // Init settings keys
                const settingsKeys = Object.keys(settings.controls).filter(key =>
                    !(env.browser.isMobile && settings.controls[key].mobile == false))

                // Init logo
                const settingsIcon = icons.googlegpt.create()
                settingsIcon.style.cssText += `width: ${ env.browser.isCompact ? 64 : 65 }px ;
                                               margin: 13px 0 ${ env.browser.isCompact ? '-35' : '-27' }px ;
                                               position: relative ; top: -42px ; ${
                                                   env.browser.isCompact ? 'left: 6px' : '' }`
                // Init title
                const settingsTitleDiv = dom.create.elem('div', { id: `${app.slug}-settings-title` }),
                      settingsTitleIcon = icons.create({ key: 'sliders' }),
                      settingsTitleH4 = dom.create.elem('h4', { textContent: app.msgs.menuLabel_settings })
                settingsTitleIcon.style.cssText += `
                    width: 21px ; height: 21px ; position: relative ; top: 2.5px ; right: 12px`
                settingsTitleH4.prepend(settingsTitleIcon) ; settingsTitleDiv.append(settingsTitleH4)

                // Init settings lists
                const settingsLists = [], middleGap = 30 // px
                const settingsListContainer = dom.create.elem('div')
                const settingsListCnt = (
                    env.browser.isMobile && ( env.browser.isCompact || settingsKeys.length < 8 )) ? 1 : 2
                const settingEntryCap = Math.floor(settingsKeys.length /2)
                for (let i = 0 ; i < settingsListCnt ; i++) settingsLists.push(dom.create.elem('ul'))
                settingsListContainer.style.width = '95%' // pad vs. parent
                if (settingsListCnt > 1) { // style multi-list landscape mode
                    settingsListContainer.style.cssText += ( // make/pad flexbox, add middle gap
                        `display: flex ; padding: 11px 0 13px ; gap: ${ middleGap /2 }px` )
                    settingsLists[0].style.cssText = ( // add vertical separator
                        `padding-right: ${ middleGap /2 }px` )
                }

                // Create/append setting icons/labels/toggles
                settingsKeys.forEach((key, idx) => {
                    const setting = settings.controls[key]

                    // Create/append item/label elems
                    const settingEntry = dom.create.elem('li',
                        { id: `${key}-settings-entry`, title: setting.helptip || '' })
                    const settingLabel = dom.create.elem('label', { textContent: setting.label })
                    settingEntry.append(settingLabel);
                    (settingsLists[env.browser.isCompact ? 0 : +(idx >= settingEntryCap)]).append(settingEntry)

                    // Create/prepend icons
                    const settingIcon = icons.create({ key: setting.icon })
                    settingIcon.style.cssText = 'position: relative ;' + (
                        /proxy/i.test(key) ? 'top: 3px ; left: -0.5px ; margin-right: 9px'
                      : /preferred/i.test(key) ? 'top: 3.5px ; margin-right: 7.5px'
                      : /streaming/i.test(key) ? 'top: 3px ; left: 0.5px ; margin-right: 9px'
                      : /auto(?:get|focus)/i.test(key) ? 'top: 4.5px ; margin-right: 7px'
                      : /summarize/i.test(key) ? 'top: 3.5px ; left: -5px ; margin-right: 3px ; height: 17.5px'
                      : /autoscroll/i.test(key) ? 'top: 3.5px ; left: -1.5px ; margin-right: 6px'
                      : /^rq/.test(key) ? 'top: 2.5px ; left: 0.5px ; margin-right: 9px ; transform: scaleY(-1)'
                      : /prefix/i.test(key) ? 'top: 2.5px ; left: 0.5px ; margin-right: 9px'
                      : /suffix/i.test(key) ? 'top: 4px ; left: -1.5px ; margin-right: 7px'
                      : /sidebar/i.test(key) ? 'top: 4px ; left: -1.5px ; margin-right: 7.5px'
                      : /anchor/i.test(key) ? 'top: 3px ; left: -2.5px ; margin-right: 5.5px'
                      : /animation/i.test(key) ? 'top: 3px ; left: -1.5px ; margin-right: 6.5px'
                      : /replylang/i.test(key) ? 'top: 3px ; left: -1.5px ; margin-right: 9px'
                      : /scheme/i.test(key) ? 'top: 2.5px ; left: -1.5px ; margin-right: 8px'
                      : /debug/i.test(key) ? 'top: 3.5px ; left: -1.5px ; margin-right: 8px'
                      : /about/i.test(key) ? 'top: 3px ; left: -3px ; margin-right: 5.5px' : ''
                    )
                    settingEntry.prepend(settingIcon)
                    if (key.includes('Animation')) // customize sparkle icon elem fill
                        settingIcon[`${ key.startsWith('fg') ? 'last' : 'first' }Child`].style.fill = 'none'

                    // Create/append toggles/listeners
                    if (setting.type == 'toggle') {

                        // Init toggle input
                        const settingToggle = dom.create.elem('input', {
                            type: 'checkbox', disabled: true, style: 'display: none' })
                        settingToggle.checked = settings.typeIsEnabled(key) // init based on config/name
                            && !(key == 'streamingDisabled' && !app.config.proxyAPIenabled) // uncheck Streaming in OAI mode

                        // Create/classify switch
                        const switchSpan = dom.create.elem('span', { class: 'track' }),
                              knobSpan = dom.create.elem('span', { class: 'knob' })

                        // Append elems
                        switchSpan.append(knobSpan) ; settingEntry.append(settingToggle, switchSpan)

                        // Update visual state w/ animation
                        setTimeout(() => modals.settings.toggle.updateStyles(settingToggle), 155)

                        // Add click listener
                        settingEntry.onclick = () => {
                            if (!(key == 'streamingDisabled' // visually switch toggle if not Streaminng...
                                && ( // ...in unsupported env...
                                    !env.scriptManager.supportsStreaming || !app.config.proxyAPIenabled )
                            )) modals.settings.toggle.switch(settingToggle)

                            // Call specialized toggle funcs
                            const autoGenMatch = /get|summarize/i.exec(key),
                                  manualGenMatch = /(?:suf|pre)fix/i.exec(key)
                            if (key.includes('proxy')) toggle.proxyMode()
                            else if (key.includes('streaming')) toggle.streaming()
                            else if (key.includes('rq')) toggle.relatedQueries()
                            else if (autoGenMatch) toggle.autoGen(autoGenMatch[0].toLowerCase())
                            else if (manualGenMatch) toggle.manualGen(manualGenMatch[0].toLowerCase())
                            else if (key.includes('Sidebar')) toggle.sidebar(key.replace('Sidebar', ''))
                            else if (key.includes('anchor')) toggle.anchorMode()
                            else if (key.includes('bgAnimation')) toggle.animations('bg')
                            else if (key.includes('fgAnimation')) toggle.animations('fg')

                            // ...or generically toggle/notify
                            else {
                                settings.save(key, !app.config[key]) // update config
                                feedback.notify(`${settings.controls[key].label} ${
                                    menus.toolbar.state.words[+(key.includes('Disabled') != app.config[key])]}`)
                            }
                        }

                    // Add .active + config status + listeners to pop-up settings
                    } else {
                        settingEntry.classList.add('active')
                        const configStatusSpan = dom.create.elem('span', {
                            style: `float: right ; font-size: 11px ; margin-top: 3px ;${
                                key != 'about' ? 'text-transform: uppercase !important' : '' }`
                        })
                        ;({
                            about: () => {
                                const innerDiv = dom.create.elem('div'), xGap = '&emsp;&emsp;&emsp;&emsp;&emsp;'
                                modals.settings.aboutContent = {
                                    short: `v${GM_info.script.version}`,
                                    long: `${app.msgs.about_version}: <span class="about-em">v${
                                            GM_info.script.version + xGap }</span>${
                                            app.msgs.about_poweredBy} <span class="about-em">chatgpt.js</span>${xGap}`
                                }
                                for (let i = 0; i < 7; i++)
                                    modals.settings.aboutContent.long += modals.settings.aboutContent.long // make long af
                                innerDiv.innerHTML = modals.settings.aboutContent[
                                    app.config.fgAnimationsDisabled ? 'short' : 'long']
                                innerDiv.style.float = app.config.fgAnimationsDisabled ? 'right' : ''
                                configStatusSpan.append(innerDiv) ; settingEntry.onclick = () => modals.open('about')
                            },
                            preferredAPI: () => {
                                configStatusSpan.textContent = app.config.preferredAPI || app.msgs.menuLabel_random
                                settingEntry.onclick = () => modals.open('api')
                                settingEntry.classList.toggle('active', app.config.proxyAPIenabled)
                                settingEntry.style.pointerEvents = app.config.proxyAPIenabled ? '' : 'none'
                            },
                            replyLang: () => {
                                configStatusSpan.textContent = app.config.replyLang
                                settingEntry.onclick = () => modals.open('replyLang')
                            },
                            scheme: () => {
                                modals.settings.updateSchemeStatus(configStatusSpan)
                                settingEntry.onclick = () => modals.open('scheme')
                            }
                        })[key]?.()
                        settingEntry.append(configStatusSpan)
                    }
                })
                settingsListContainer.append(...settingsLists)

                // Create close button
                const closeBtn = dom.create.elem('div',
                    { title: app.msgs.tooltip_close, class: `${app.slug}-modal-close-btn no-mobile-tap-outline` })
                closeBtn.append(icons.create({ key: 'x', size: 11 }))

                // Assemble/append elems
                settingsModal.append(settingsIcon, settingsTitleDiv, closeBtn, settingsListContainer)
                document.body.append(settingsContainer)

                return settingsContainer
            },

            get() { return document.getElementById(`${app.slug}-settings`) },

            show() {
                modals.settings.stylize()
                const settingsContainer = modals.settings.get()?.parentNode || modals.settings.createAppend()
                settingsContainer.style.display = '' // show modal
                if (env.browser.isCompact) { // scale 93% to viewport sides
                    const settingsModal = settingsContainer.querySelector(`#${app.slug}-settings`)
                    modals.settings.scaleRatio ||= 0.93 * innerWidth / settingsModal.scrollWidth
                    settingsModal.style.transform = `scale(${modals.settings.scaleRatio})`
                }
                return settingsContainer.firstChild
            },

            stylize() {
                const { scheme: appScheme } = env.ui.app
                if (!this.styles?.isConnected) document.head.append(this.styles ||= dom.create.style())
                this.styles.textContent = `
                    #${app.slug}-settings {
                        min-width: ${ env.browser.isCompact ? 288 : 698 }px ; max-width: 75vw ; word-wrap: break-word ;
                        margin: 12px 23px ; border-radius: 15px ;
                        ${ appScheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: black ; fill: black' };
                      --shadow: 0 30px 60px rgba(0,0,0,0.12) ; box-shadow: var(--shadow) ;
                           -webkit-box-shadow: var(--shadow) ; -moz-box-shadow: var(--shadow)
                    }
                    #${app.slug}-settings-title {
                        font-weight: bold ; line-height: 19px ; text-align: center ;
                        margin: 0 -6px ${ env.browser.isCompact ? 2 : -15 }px 0
                    }
                    #${app.slug}-settings-title h4 {
                        font-size: ${ env.browser.isCompact ? 22 : 29 }px ; font-weight: bold ;
                        margin: 0 0 ${ env.browser.isCompact ? 9 : 27 }px
                    }
                    #${app.slug}-settings ul {
                        align-content: center ; /* for symmetrized gaps when odd num of entries */
                        list-style: none ; padding: 0 ; margin-bottom: 2px ; /* hide bullets, close bottom gap */
                        width: ${ env.browser.isCompact ? 100 : 50 }% /* set width based on column cnt */
                    }
                    ${ env.browser.isCompact ? '' : `#${app.slug}-settings ul:first-of-type { /* color desktop middle sep */
                        border-right: 1px dotted ${ appScheme == 'dark' ? 'white' : 'black' }}`}
                    #${app.slug}-settings li {
                        color: ${ appScheme == 'dark' ? 'rgb(255,255,255,0.65)' : 'rgba(0,0,0,0.45)' };
                        fill: ${ appScheme == 'dark' ? 'rgb(255,255,255,0.65)' : 'rgba(0,0,0,0.45)' };
                        stroke: ${ appScheme == 'dark' ? 'rgb(255,255,255,0.65)' : 'rgba(0,0,0,0.45)' };
                        height: 24px ; padding: 6px 10px ; font-size: 13.5px ;
                        border-bottom: 1px dotted ${ appScheme == 'dark' ? 'white' : 'black' }; /* add separators */
                        border-radius: 3px ; /* slightly round highlight strip */
                        ${ app.config.fgAnimationsDisabled || env.browser.isMobile ? '' :
                            `transition: var(--settings-li-transition) ;
                                -webkit-transition: var(--settings-li-transition) ;
                                -moz-transition: var(--settings-li-transition) ;
                                -o-transition: var(--settings-li-transition) ;
                                -ms-transition: var(--settings-li-transition)` }
                    }
                    #${app.slug}-settings li.active {
                        color: ${ appScheme == 'dark' ? 'rgb(255,255,255)' : 'rgba(0,0,0)' };
                        fill: ${ appScheme == 'dark' ? 'rgb(255,255,255)' : 'rgba(0,0,0)' };
                        stroke: ${ appScheme == 'dark' ? 'rgb(255,255,255)' : 'rgba(0,0,0)' }
                    }
                    #${app.slug}-settings li label { padding-right: 20px } /* right-pad labels so toggles don't hug */
                    #${app.slug}-settings li:last-of-type { border-bottom: none } /* remove last bottom-border */
                    #${app.slug}-settings :where(li, li label) { cursor: pointer } /* add finger on hover */
                    #${app.slug}-settings li:hover {
                        background: rgba(100,149,237,0.88) ; color: white ; fill: white ; stroke: white ;
                        ${ env.browser.isMobile ? '' : 'transform: scale(1.15)' }
                    }
                    #${app.slug}-settings li > input { float: right } /* pos toggles */
                    #${app.slug}-settings li > .track {
                        position: relative ; left: -1px ; bottom: -5.5px ; float: right ;
                        background-color: #ccc ; width: 26px ; height: 13px ; border-radius: 28px ;
                        ${ app.config.fgAnimationsDisabled ? '' :
                            `transition: 0.4s ; -webkit-transition: 0.4s ; -moz-transition: 0.4s ;
                                -o-transition: 0.4s ; -ms-transition: 0.4s` }
                    }
                    #${app.slug}-settings li .knob {
                        position: absolute ; left: 1px ; bottom: 1px ; content: "" ;
                        background-color: white ; width: 11px ; height: 11px ; border-radius: 28px ;
                        ${ app.config.fgAnimationsDisabled ? '' :
                            `transition: 0.2s ; -webkit-transition: 0.2s ; -moz-transition: 0.2s ;
                                -o-transition: 0.2s ; -ms-transition: 0.2s` }
                    }
                    #scheme-settings-entry > span { margin: 3px -2px 0 } /* align Scheme status */
                    #scheme-settings-entry > span > svg { /* v-align/left-pad Scheme status icon */
                        position: relative ; top: 2px ; margin-left: 4px }
                    ${ app.config.fgAnimationsDisabled ? '' // spin cycle arrows icon when scheme is Auto
                        : `#scheme-settings-entry svg[class*=arrowsCyclic],
                                .chatgpt-notif svg[class*=arrowsCyclic] { animation: rotate 5s linear infinite }`
                    }
                    #about-settings-entry span { color: ${ appScheme == 'dark' ? '#28ee28' : 'green' }}
                    #about-settings-entry > span { /* outer About status span */
                        width: ${ env.browser.isCompact ? '15vw' : '95px' }; height: 20px ; overflow: hidden ;
                        ${ env.browser.isCompact ? 'position: relative ; bottom: 3px ;' : '' } /* v-align */
                        ${ app.config.fgAnimationsDisabled ? '' : // fade edges
                                `mask-image: linear-gradient(
                                    to right, transparent, black 20%, black 89%, transparent) ;
                        -webkit-mask-image: linear-gradient(
                                    to right, transparent, black 20%, black 89%, transparent) ;`}
                    }
                    #about-settings-entry > span > div {
                        text-wrap: nowrap ;
                        ${ app.config.fgAnimationsDisabled ? '' : 'animation: ticker linear 75s infinite' }
                    }
                    @keyframes ticker { 0% { transform: translateX(100%) } 100% { transform: translateX(-2000%) }}
                    .about-em { color: ${ appScheme == 'dark' ? 'white' : 'green' } !important }`
            },

            toggle: {
                switch(settingToggle) {
                    settingToggle.checked = !settingToggle.checked
                    modals.settings.toggle.updateStyles(settingToggle)
                },

                updateStyles(settingToggle) { // for .toggle.show() + staggered switch animations in .createAppend()
                    const settingLi = settingToggle.parentNode,
                          switchSpan = settingLi.querySelector('span'),
                          knobSpan = switchSpan.querySelector('span')
                    requestAnimationFrame(() => {
                        switchSpan.style.backgroundColor = settingToggle.checked ? '#ad68ff' : '#ccc'
                        switchSpan.style.boxShadow = settingToggle.checked ? '2px 1px 9px #d8a9ff' : 'none'
                        knobSpan.style.transform = settingToggle.checked ?
                            'translateX(14px) translateY(0)' : 'translateX(0)'
                        settingLi.classList.toggle('active', settingToggle.checked) // dim/brighten entry
                    }) // to trigger 1st transition fx
                }
            },

            updateSchemeStatus(schemeStatusSpan = null) {
                schemeStatusSpan ||= document.querySelector('#scheme-settings-entry span')
                if (schemeStatusSpan) {
                    schemeStatusSpan.textContent = ''
                    schemeStatusSpan.append( // status txt + icon
                        document.createTextNode(app.msgs[/dark|light/.test(app.config.scheme) ? `scheme_${app.config.scheme}`
                                                                                          : 'menuLabel_auto']),
                        icons.create({ size: 12,
                            key: app.config.scheme == 'dark' ? 'moon' : app.config.scheme == 'light' ? 'sun' : 'arrowsCyclic' })
                    )
                }
            }
        },

        shareChat(shareURL) {

            // Show modal
            const shareChatModal = modals.alert(
                `${log.toTitleCase(app.msgs.btnLabel_convo)} ${app.msgs.tooltip_page} ${ // title
                    app.msgs.alert_generated.toLowerCase()}!`,
                `<a target="_blank" rel="noopener" href="${shareURL}">${shareURL}</a>`, // link msg
                [ // buttons
                    function copyUrl() {
                        navigator.clipboard.writeText(shareURL)
                            .then(() => feedback.notify(app.msgs.notif_copiedToClipboard))
                    },
                    function visitPage() { modals.safeWinOpen(shareURL) },
                    function downloadChat() {
                        xhr({
                            method: 'GET', url: shareURL,
                            onload: ({ responseText }) => {
                                const download = responseText.match(/<title>([^<]+)<\/title>/i)[1]
                                    .replace(/\s*[—|/]+\s*/g, ' ') // convert symbols to space for hyphen-casing
                                    .replace(/\.{2,}/g, '') // strip ellipsis
                                    .toLowerCase().trim().replace(/\s+/g, '-') // hyphen-case
                                    + '.html'
                                const href = URL.createObjectURL(new Blob([responseText], { type: 'text/html' }))
                                const a = dom.create.anchor(href, '', { download, style: 'display: none' })
                                document.body.append(a) ; a.click() ; a.remove() ; URL.revokeObjectURL(href)
                            },
                            onerror: err => log.error('Failed to download chat:', err)
                        })
                    }
                ]
            )

            // Prefix icon to title
            const modalTitle = shareChatModal.querySelector('h2'), titleIcon = icons.create({ key: 'speechBalloons' })
            titleIcon.style.cssText = `height: 23px ; width: 23px ; position: relative ; top: 5px ; right: 8px ;
                                       fill: ${ env.ui.app.scheme == 'dark' ? 'white' : 'black' }`
            modalTitle.prepend(titleIcon)

            // Hide Dismiss button, localize other labels
            const modalBtns = shareChatModal.querySelectorAll('button')
            modalBtns[0].style.display = 'none' // hide Dismiss button
            if (!env.browser.language.startsWith('en')) // localize button labels
                modalBtns.forEach(btn => {
                    if (/copy/i.test(btn.textContent)) btn.textContent = `${app.msgs.tooltip_copy} URL`
                    else if (/visit/i.test(btn.textContent)) btn.textContent = app.msgs.btnLabel_visitPage
                    else if (/download/i.test(btn.textContent))
                         btn.textContent = `${app.msgs.btnLabel_download} ${log.toTitleCase(app.msgs.btnLabel_convo)}`
                })

            // Style elements
            shareChatModal.style.wordBreak = 'break-all' // since URL really long
            shareChatModal.querySelector('h2').style.justifySelf = 'center'
            shareChatModal.querySelector('p').style.cssText = 'text-align: center ; margin: 10px 0 -22px'
            shareChatModal.querySelector('.modal-buttons').style.cssText = 'justify-content: center'

            return shareChatModal
        },

        stylize() {
            const { scheme: appScheme } = env.ui.app
            if (!this.styles?.isConnected) document.head.append(this.styles ||= dom.create.style())
            this.styles.textContent = (

                // Vars
                `:root {
                  --modal-btn-zoom: scale(1.055) ; --modal-btn-transition: transform 0.15s ease ;
                  --settings-li-transition: transform 0.1s ease ; /* for Settings entry hover-zoom */
                  --fg-transition: opacity 0.65s cubic-bezier(0.165,0.84,0.44,1), /* fade-in */
                                   transform 0.55s cubic-bezier(0.165,0.84,0.44,1) !important ; /* move-in */
                  --bg-transition: background-color 0.25s ease !important } /* dim */`

                // Main modal styles
              + `@keyframes modal-zoom-fade-out {
                    0% { opacity: 1 } 50% { opacity: 0.25 ; transform: scale(1.05) }
                    100% { opacity: 0 ; transform: scale(1.35) }
                }
                .chatgpt-modal > div {
                    background-color: white ; color: #202124 ;
                    padding: ${ env.browser.isCompact ? '31px' : '25px 31px' }!important
                }
                .chatgpt-modal p { margin: 14px 0 -29px 4px ; font-size: 1.28em ; line-height: 1.57 }
                .modal-buttons {
                    margin: 42px 4px ${ env.browser.isMobile ? '2px 4px' : '-3px -4px' } !important ; width: 100% }
                .chatgpt-modal button { /* this.alert() buttons */
                    min-width: 113px ; padding: ${ env.browser.isMobile ? '5px' : '4px 15px' } !important ;
                    cursor: pointer ; border-radius: 0 !important ; height: 39px ;
                    border: 1px solid ${ appScheme == 'dark' ? 'white' : 'black' }!important }
                .primary-modal-btn { background: black !important ; color: white !important }
                .chatgpt-modal button:hover {
                  --btn-shadow: ${ appScheme == 'light' ? '2px 1px 43px #00cfff70' : '2px 1px 54px #00cfff' };
                    color: inherit !important ; /* remove color hack */
                    background-color: rgb(${ appScheme == 'light' ? '192 223 227 / 5%' : '43 156 171 / 43%' }) !important
                }
                ${ appScheme == 'dark' ? // darkmode chatgpt.alert() styles
                    `.chatgpt-modal > div, .chatgpt-modal button:not(.primary-modal-btn) {
                        color: white !important }
                    .primary-modal-btn { background: #00cfff !important ; color: black !important }
                    .chatgpt-modal a { color: #00cfff !important }` : ''
                }
                .${modals.class} { display: grid ; place-items: center } /* for centered icon/logo */
                [class*=modal-close-btn] {
                    position: absolute !important ; float: right ; top: 14px !important ; right: 16px !important ;
                    cursor: pointer ; width: 33px ; height: 33px ; border-radius: 20px
                }
                [class*=modal-close-btn] path {${ appScheme == 'dark' ? 'stroke: white ; fill: white'
                                                                      : 'stroke: #9f9f9f ; fill: #9f9f9f' }}
                ${ appScheme == 'dark' ? // invert dark mode hover paths
                    '[class*=modal-close-btn]:hover path { stroke: black ; fill: black }' : '' }
                [class*=modal-close-btn]:hover { background-color: #f2f2f2 } /* hover underlay */
                [class*=modal-close-btn] svg { margin: 11.5px } /* center SVG for hover underlay */
                [class*=-modal] h2 {
                    font-size: 1.65rem ; line-height: 32px ; padding: 0 ; margin: 9px 0 -3px !important ;
                    ${ env.browser.isMobile ? 'text-align: center' // center on mobile
                                            : 'justify-self: start' }} /* left-align on desktop */
                [class*=-modal] p { justify-self: start ; font-size: 20px }
                [class*=-modal] button {
                    color: ${ appScheme == 'dark' ? 'white' : 'black' }; font-size: 12px !important ; background: none }
                [class*=-modal-bg] {
                    pointer-events: auto ; /* override any disabling from site modals */
                    position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ; /* expand to full view-port */
                    display: flex ; justify-content: center ; align-items: center ; z-index: 9999 ; /* align */
                    transition: var(--bg-transition) ; /* dim */
                       -webkit-transition: var(--bg-transition) ; -moz-transition: var(--bg-transition) ;
                       -o-transition: var(--bg-transition) ; -ms-transition: var(--bg-transition) }
                [class*=-modal-bg].animated > div {
                    z-index: 13456 ; opacity: 0.98 ; transform: translateX(0) translateY(0) }
                [class$=-modal] { /* native modals + chatgpt.alert()s */
                    position: absolute ; /* to be click-draggable */
                    opacity: 0 ; /* to fade-in */
                    background-image: linear-gradient(180deg, ${
                       appScheme == 'dark' ? '#99a8a6 -200px, black 200px' : '#b6ebff -296px, white 171px' }) ;
                    border: 1px solid ${ appScheme == 'dark' ? 'white' : '#b5b5b5' } !important ;
                    color: ${ appScheme == 'dark' ? 'white' : 'black' };
                    transform: translateX(-3px) translateY(7px) ; /* offset to move-in from */
                    transition: var(--fg-transition) ; /* fade-in + move-in */
                       -webkit-transition: var(--fg-transition) ; -moz-transition: var(--fg-transition) ;
                       -o-transition: var(--fg-transition) ; -ms-transition:  var(--fg-transition) }
                    ${ env.browser.isMobile ? '' : `[class$=-modal] button:hover { transform: var(--modal-btn-zoom) }`}
                    ${ app.config.fgAnimationsDisabled ? '' : `[class$=-modal] button {
                        ${ env.browser.isMobile ? '' : 'will-change: transform ;' }
                        transition: var(--modal-btn-transition) ;
                           -webkit-transition: var(--modal-btn-transition) ;
                           -moz-transition: var(--modal-btn-transition) ;
                           -o-transition: var(--modal-btn-transition) ;
                           -ms-transition: var(--modal-btn-transition) }`}`
            )
        },

        update: {
            width: 377,

            available() {
                const updateAvailModal = modals.alert(`🚀 ${app.msgs.alert_updateAvail}!`, // title
                    `${app.msgs.alert_newerVer} ${app.name} ` // msg
                        + `(v${app.latestVer}) ${app.msgs.alert_isAvail}!  `
                        + '<a target="_blank" rel="noopener" style="font-size: 0.97rem" href="'
                            + `${app.urls.github}/commits/main/greasemonkey/${app.slug}.user.js`
                        + `">${app.msgs.link_viewChanges}</a>`,
                    function update() { // button
                        modals.safeWinOpen(`${app.urls.update.gm}?t=${Date.now()}`)
                    }, '', modals.update.width
                )
                if (!env.browser.language.startsWith('en')) { // localize button labels
                    const updateBtns = updateAvailModal.querySelectorAll('button')
                    updateBtns[1].textContent = app.msgs.btnLabel_update
                    updateBtns[0].textContent = app.msgs.btnLabel_dismiss
                }
                return updateAvailModal
            },

            unavailable() {
                return modals.alert(`${app.msgs.alert_upToDate}!`, // title
                    `${app.name} (v${app.version}) ${app.msgs.alert_isUpToDate}!`, // msg
                    '', '', modals.update.width
                )
            }
        }
    }

    // Run MAIN routine

    menus.toolbar.register()

    if (/udm=2(?:&|$)/.test(location.search)) return log.debug('Exited from Google Images')

    // Init UI props
    env.ui = {
        app: { scheme: app.config.scheme || ui.getScheme() },
        site: { hasSidebar: !!document.querySelector('[class*=kp-]'), scheme: ui.getScheme() }
    }

    if (!app.config.aiSafetyWarned) {
        modals.alert('⚠️ Important Notice:',
            `<b>${app.name}</b> is powered by AI technology. While designed to be helpful:\n\n`
                + '• <b>AI can make mistakes</b> - Always verify important information\n'
                + `• <b>Double-check critical decisions</b> - Don't rely solely on AI advice\n`
                + '• <b>Not a substitute</b> - For professional, medical, or legal matters\n\n'
                + 'Use responsibly!',
            null, null, 388
        )
        settings.save('aiSafetyWarned', true)
    }

    // Create/ID/classify/listenerize/stylize APP container
    app.div = dom.create.elem('div', { id: app.slug, class: 'fade-in' })
    themes.apply(app.config.theme) ; ui.addListeners.appDiv()
    ;['anchored', 'expanded', 'sticky', 'wider'].forEach(mode =>
        (app.config[mode] || app.config[`${mode}Sidebar`]) && app.div.classList.add(mode))
    update.appStyle()
    ;['rpg', 'rpw'].forEach(cssType => // rising particles
        document.head.append(dom.create.style(GM_getResourceText(`${cssType}CSS`))))

    // APPEND to Google
    app.centerCol = document.querySelector('#center_col') || document.querySelector('#main')
    const appDivParent = env.browser.isMobile ? app.centerCol
        : document.getElementById('rhs') // sidebar container if side snippets exist
        || (() => { // create new one if no side snippets exist
               const appDivParent = dom.create.elem('div')
               app.centerCol.insertAdjacentElement('afterend', appDivParent)
               return appDivParent
           })()
    appDivParent.prepend(app.div)
    setTimeout(() => app.div.classList.add('active'), 100) // fade in

    // ANCHOR GoogleGPT in Google AI Mode
    if (/udm=50(?:&|$)/.test(location.search)) {
        toggle.anchorMode('on')
        if (!env.browser.isMobile) { // hide Pin button + Anchor Mode setting
            dom.get.loadedElem(`#${app.slug}-pin-btn`).then(btn => btn.style.display = 'none')
            document.head.append(dom.create.style('li#anchored-settings-entry { display: none }'))
        }

    // Strip Google TRACKING
    } else document.addEventListener(inputEvents.down, event => {
        let a = event.target ; while (a && !a.href) a = a.parentElement ; if (!a) return // find closest ancestor href
        a.removeAttribute('ping') // prevent pingback on link click
        if (a.getAttribute('onmousedown')?.includes('rwt(')) {
            a.removeAttribute('onmousedown')
            if (env.browser.isChrome) event.stopImmediatePropagation() // since inline listener still runs
        }
        let realURL = getRealURL(a)
        if (realURL) {
            a.href = realURL
            realURL = getRealURL(a) ; if (realURL) a.href = realURL // do again for old mobile UA
        }

        function getRealURL(a) {
            if (!a.protocol.startsWith('http')) return
            let url
            if ((a.hostname.startsWith('www.google.') || a.hostname == location.hostname) &&
               ['/url', // mobile: /url?q=<url>
                '/local_url', // Maps/Dito: /local_url?q=<url>
                '/searchurl/rr.html', '/linkredirect'].includes(a.pathname)) {
                    url = /[?&](?:q|url|dest)=((?:https?|ftp)[%:][^&]+)/.exec(a.search) // HTTP/FTP URLs
                    if (url) return decodeURIComponent(url[1])
                    url = /[?&](?:q|url)=((?:%2[Ff]|\/)[^&]+)/.exec(a.search) // help pages, e.g. safe browsing (/url?...&q=%2Fsupport%2Fanswer...)
                    if (url) return a.origin + decodeURIComponent(url[1])
                    url = /[#&]url=(https?[:%][^&]+)/.exec(a.hash) // Android intents (/searchurl/rr.html#...&url=...)
                    if (url) return decodeURIComponent(url[1])
            }
            if (a.hostname == 'googleweblight.com' && a.pathname == '/fp') { // Google Search w/ old mobile UA (e.g. Firefox 41)
                url = /[?&]u=((?:https?|ftp)[%:][^&]+)/.exec(a.search)
                if (url) return decodeURIComponent(url[1])
            }
        }
    }, true) // invoke during capturing phase

    // Init footer CTA to share feedback
    app.footerContent = dom.create.anchor(app.urls.discuss, app.msgs.link_shareFeedback)

    // AUTO-GEN reply or show STANDBY mode
    app.msgChain = [] ; const searchQuery = new URL(location.href).searchParams.get('q')
    if (app.config.autoGet || app.config.autoSummarize // Auto-Gen on
        || (app.config.prefixEnabled || app.config.suffixEnabled) // or Manual-Gen on
            && [app.config.prefixEnabled && location.href.includes('q=%2F'), // prefix required/present
                app.config.suffixEnabled // suffix required/present
                    && /q=.*?(?:%3F|?|%EF%BC%9F)(?:&|$)/.test(location.href)
            ].filter(Boolean).length == (app.config.prefixEnabled + app.config.suffixEnabled) // validate both Manual-Gen modes
    ) { // auto-gen reply
        app.msgChain.push({
            time: Date.now(), role: 'user',
            content: app.config.autoSummarize ? prompts.create('summarizeResults') : searchQuery
        })
        get.reply({ msgs: app.msgChain, src: 'query' })
    } else { // show Standby mode
        show.reply({ standby: true })
        if (!app.config.rqDisabled)
            get.related(searchQuery)
                .then(queries => show.related(queries))
                .catch(err => { log.error(err.message) ; api.tryNew(get.related) })
    }

    // Observe DOM for new sidebar div#rhs created by other extensions to INSERT GoogleGPT to visually co-exist
    const sidebarObserver = new MutationObserver(() => {
        const newSidebar = document.getElementById('rhs')
        if (newSidebar) { newSidebar.prepend(app.div) ; sidebarObserver.disconnect() }
    })
    sidebarObserver.observe(document.body, { subtree: true, childList: true })
    setTimeout(() => sidebarObserver.disconnect(), 5000) // don't observe forever

})()