Greasy Fork is available in English.

BraveGPT 🤖

Ajoute des réponses IA à Brave Search (propulsé par GPT-4o!)

Installer ce script?
Script suggéré par l'auteur

Vous aimerez aussi DuckDuckGPT 🤖.

Installer ce script
// ==UserScript==
// @name                  BraveGPT 🤖
// @description           Adds AI answers to Brave Search (powered by GPT-4o!)
// @description:af        Voeg AI-antwoorde by Brave Search (aangedryf deur GPT-4o!)
// @description:am        የ Brave Search ውስጥ AI መልቀቅን አድርግ፣ (GPT-4o በመሣሪያዎቹ ውስጥ!)
// @description:ar        يضيف إجابات AI إلى Brave Search (مدعوم بواسطة GPT-4o!)
// @description:as        Brave Search-লৈ AI উত্তৰ যোগ দিয়ে (GPT-4o দ্বাৰা পাওৱা হৈছে!)
// @description:az        Brave Search-ya AI cavablarını əlavə edir (GPT-4o tərəfindən dəstəklənir!)
// @description:be        Дадае ІА адказы на Brave Search (падтрымліваецца GPT-4o!)
// @description:bg        Добавя ИИ отговори в Brave Search (поддържан от GPT-4o!)
// @description:bn        Brave Search-ত AI উত্তর যোগ করে (GPT-4o দ্বারা প্রচালিত!)
// @description:bs        Dodaje AI odgovore na Brave Search (pokreće GPT-4o!)
// @description:ca        Afegeix respostes d'IA a Brave Search (impulsat per GPT-4o!)
// @description:ceb       Nagdugang ug mga tubag AI ngadto sa Brave Search (gipadagan sa GPT-4o!)
// @description:co        Aggiunge risposte AI a Brave Search (supportate da GPT-4o!)
// @description:cs        Přidává AI odpovědi do Brave Search (poháněno GPT-4o!)
// @description:cy        Ychwanegu atebion AI i Brave Search (a yrrir gan GPT-4o!)
// @description:da        Tilføjer AI-svar til Brave Search (drevet af GPT-4o!)
// @description:de        Fügt AI-Antworten zu Brave Search hinzu (betrieben von GPT-4o!)
// @description:el        Προσθέτει απαντήσεις AI στο Brave Search (τροφοδοτούμενο από GPT-4o!)
// @description:en        Adds AI answers to Brave Search (powered by GPT-4o!)
// @description:eo        Aldonas AI-respondojn al Brave Search (ebligita de GPT-4o!)
// @description:es        Añade respuestas de IA a Brave Search (impulsado por GPT-4o!)
// @description:et        Lisab AI-vastused Brave Search'le (juhitud GPT-4o-ga!)
// @description:eu        Gehitu IA erantzunak Brave Search-n (GPT-4o-k bultzatuta!)
// @description:fa        پاسخهای هوشمصنوعی به Brave Search اضافه میشود (توسط GPT-4o پشتیبانی میشود!)
// @description:fi        Lisää tekoälyvastauksia Brave Search:hun (ohjattu GPT-4o:lla!)
// @description:fil       Nagdaragdag ng mga sagot ng AI sa Brave Search (pinapagana ng GPT-4o!)
// @description:fo        Bætir AI svar við Brave Search (drifin af GPT-4o!)
// @description:fr        Ajoute des réponses IA à Brave Search (propulsé par GPT-4o!)
// @description:fr-CA     Ajoute des réponses IA à Brave Search (propulsé par GPT-4o!)
// @description:fy        Foeget AI-antwurden ta oan Brave Search (dreaun troch GPT-4o!)
// @description:ga        Cuirtear freagraí AI le Brave Search (dírítear ag GPT-4o!)
// @description:gd        Cur freagairtichean AI ris an Brave Search (air a thug seachad le GPT-4o!)
// @description:gl        Engade respostas de IA a Brave Search (impulsado por GPT-4o!)
// @description:gu        Brave Search માટે AI જવાબો ઉમેરે છે (GPT-4o દ્વારા પોવરેડ!)
// @description:ha        Ƙaddara takardun AI zu Brave Search (da aka fi GPT-4o!)
// @description:haw       Hoʻohui aku i nā hoʻopiʻi AI iā Brave Search (hoʻohui ʻia e GPT-4o!)
// @description:he        מוסיף תשובות AI ל-Brave Search (מופעל על ידי GPT-4o!)
// @description:hi        Brave Search में AI उत्तर जोड़ता है (GPT-4o द्वारा संचालित!)
// @description:hmn       Ntxig AI nruab nruab rau Brave Search (pab cuam GPT-4o!)
// @description:hr        Dodaje AI odgovore na Brave Search (pokreće GPT-4o!)
// @description:ht        Ajoute repons AI nan Brave Search (pòte pa GPT-4o!)
// @description:hu        AI válaszokat ad hozzá a Brave Search-hoz (GPT-4o által hajtva!)
// @description:hy        Ավելացնում է AI պատասխաններ Brave Search-ում (աջակցված է GPT-4o-ով!)
// @description:ia        Adde responas AI a Brave Search (propulsate per GPT-4o!)
// @description:id        Menambahkan jawaban AI ke Brave Search (didukung oleh GPT-4o!)
// @description:ig        Tinye ihe ndekọ AI n'ụzọ ọgụgụ Brave Search (n'efu na GPT-4o!)
// @description:ii        Brave Search ᐸᔦᒪᔪᐃᓃᑦ AI ᓇᑕᐅᒪᐃᑦᓯ (GPT-4o ᓂᑕᔪᑦᓯᐏᑦᑕᒥᔭ!)
// @description:is        Bætir AI svar við Brave Search (keyrir á GPT-4o!)
// @description:it        Aggiunge risposte AI a Brave Search (alimentato da GPT-4o!)
// @description:iu        Brave Search ᑲᑎᒪᔪᖅᑐᖅᑐᐃᓐᓇᓂᒃ AI ᑎᑎᕋᖃᕐᓯᒪᓂᖏᓐ (GPT-4o ᑐᑭᒧᑦᑖᑦ!)
// @description:ja        Brave Search に AI 回答を追加します (GPT-4o で動作!)
// @description:jv        Nambéhi pirangga AI nganti Brave Search (diduweni déning GPT-4o!)
// @description:ka        ამატებს AI პასუხებს Brave Search-ს (იმართება GPT-4o!)
// @description:kk        Brave Search-ға AI жауаптарын қосады (GPT-4o арқылы жұмыс істейді!)
// @description:kl        Brave Search-mi AI-t Kalaallit Nunaanni iluani (GPT-4o! -nip ilaanni!)
// @description:km        បន្ថែមចម្លើយ AI ទៅ Brave Search (ដំណើរការដោយ GPT-4o!)
// @description:kn        Brave Search ಗೆ AI ಉತ್ತರಗಳನ್ನು ಸೇರಿಸುತ್ತದೆ (GPT-4o ನಿಂದ ನಡೆಸಲ್ಪಡುತ್ತಿದೆ!)
// @description:ko        Brave Search에 AI 답변을 추가합니다(GPT-4o 제공!)
// @description:ku        Bersivên AI-ê li Brave Search zêde dike (ji hêla GPT-4o ve hatî hêzdar kirin!)
// @description:ky        Brave Search'го AI жоопторун кошот (GPT-4o тарабынан иштейт!)
// @description:la        Addit AI responsa Brave Search (powered per GPT-4o!)
// @description:lb        Füügt AI Äntwerten op Brave Search (ugedriwwen duerch GPT-4o!)
// @description:lg        Yambula emisomo ey'ensobi ku Brave Search (enkuuma GPT-4o!)
// @description:ln        Ebakisi biyano ya AI na Brave Search (ezali na nguya ya GPT-4o!)
// @description:lo        ເພີ່ມຄໍາຕອບ AI ໃຫ້ກັບ Brave Search (ຂັບເຄື່ອນໂດຍ GPT-4o!)
// @description:lt        Prideda AI atsakymus į „Brave Search“ (maitina GPT-4o!)
// @description:lv        Pievieno AI atbildes Brave Search (darbina GPT-4o!)
// @description:mg        Manampy valiny AI amin'ny Brave Search (nampiasain'ny GPT-4o!)
// @description:mi        Ka taapirihia nga whakautu AI ki a Brave Search (whakamahia e GPT-4o!)
// @description:mk        Додава одговори со вештачка интелигенција на Brave Search (напојувано од GPT-4o!)
// @description:ml        Brave Search-യിലേക്ക് AI ഉത്തരങ്ങൾ ചേർക്കുന്നു (GPT-4o നൽകുന്നതാണ്!)
// @description:mn        Brave Search-д AI хариултуудыг нэмдэг (GPT-4o-оор ажилладаг!)
// @description:mr        Brave Search ला AI उत्तरे जोडते (GPT-4o द्वारे समर्थित!)
// @description:ms        Menambahkan jawapan AI pada Brave Search (dikuasakan oleh GPT-4o!)
// @description:mt        Iżżid it-tweġibiet AI għal Brave Search (mħaddma minn GPT-4o!)
// @description:my        Brave Search (GPT-4o ဖြင့် စွမ်းဆောင်ထားသည့်) တွင် AI အဖြေများကို ပေါင်းထည့်သည်
// @description:na        Aeta AI teroma i Brave Search (ira GPT-4o reke akea!)
// @description:nb        Legger til AI-svar på Brave Search (drevet av GPT-4o!)
// @description:nd        Iyatholakala amaswelelo e-AI kuBrave Search (kuyatholakala ngokulawula uGPT-4o!)
// @description:ne        Brave Search मा AI जवाफहरू थप्छ (GPT-4o द्वारा संचालित!)
// @description:ng        Ondjova mbelelo dha AI moBrave Search (uumbuli nguGPT-4o!)
// @description:nl        Voegt AI-antwoorden toe aan Brave Search (mogelijk gemaakt door GPT-4o!)
// @description:nn        Legg til AI-svar på Brave Search (drevet av GPT-4o!)
// @description:no        Legger til AI-svar til Brave Search (drevet av GPT-4o!)
// @description:nso       Ya go etela ditshenyegi tsa AI mo Brave Search (e dirwang ke GPT-4o!)
// @description:ny        Imawonjezera mayankho a AI ku Brave Search (yoyendetsedwa ndi GPT-4o!)
// @description:oc        Ajusta de respòstas d'IA a Brave Search (amb GPT-4o!)
// @description:om        Deebii AI Brave Search (GPT-4o'n kan hojjetu!) irratti dabalata.
// @description:or        Brave Search କୁ AI ଉତ୍ତର ଯୋଗ କରେ (GPT-4o ଦ୍ୱାରା ଚାଳିତ!)
// @description:pa        Brave Search (GPT-4o ਦੁਆਰਾ ਸੰਚਾਲਿਤ!) ਵਿੱਚ AI ਜਵਾਬ ਸ਼ਾਮਲ ਕਰਦਾ ਹੈ
// @description:pl        Dodaje odpowiedzi AI do Brave Search (obsługiwane przez GPT-4o!)
// @description:ps        Brave Search ته د AI ځوابونه اضافه کوي (د GPT-4o لخوا پرمخ وړل کیږي!)
// @description:pt        Adiciona respostas de IA ao Brave Search (desenvolvido por GPT-4o!)
// @description:pt-BR     Adiciona respostas de IA ao Brave Search (desenvolvido por GPT-4o!)
// @description:qu        Brave Search (GPT-4o nisqawan kallpachasqa!) nisqaman AI kutichiykunata yapan.
// @description:rm        Agiuntescha respostas d'IA a Brave Search (propulsà da GPT-4o!)
// @description:rn        Abafasha inyandiko z'IA ku Brave Search (yashyizweho na GPT-4o!)
// @description:ro        Adaugă răspunsuri AI la Brave Search (alimentat de GPT-4o!)
// @description:ru        Добавляет ответы ИИ в Brave Search (на базе GPT-4o!)
// @description:rw        Ongeraho ibisubizo bya AI kuri Brave Search (ikoreshwa na GPT-4o!)
// @description:sa        Brave Search (GPT-4o द्वारा संचालितम्!) इत्यत्र AI उत्तराणि योजयति ।
// @description:sat       Brave Search ar AI jawab khon ojantok (GPT-4o! sebadha manju)
// @description:sc        Agiungit rispostas de IA a Brave Search (motorizadu da GPT-4o!)
// @description:sd        شامل ڪري ٿو AI جوابن کي Brave Search (GPT-4o پاران طاقتور!)
// @description:se        Lávdegáhtii AI vástid Brave Search (GPT-4o! vuosttas!)
// @description:sg        Nâ tî-kûzâ mái vêdáara AI mbi Brave Search (ngâ GPT-4o!)
// @description:si        Brave Search වෙත AI පිළිතුරු එක් කරයි (GPT-4o මගින් බලගන්වයි!)
// @description:sk        Pridáva odpovede AI do Brave Search (poháňané GPT-4o!)
// @description:sl        Dodaja odgovore AI v Brave Search (poganja GPT-4o!)
// @description:sm        Faʻaopoopo tali AI ile Brave Search (faʻamalosia e GPT-4o!)
// @description:sn        Inowedzera mhinduro dzeAI kuBrave Search (inofambiswa neGPT-4o!)
// @description:so        Waxay ku dartay jawaabaha AI Brave Search (waxaa ku shaqeeya GPT-4o!)
// @description:sq        Shton përgjigjet e AI në Brave Search (mundësuar nga GPT-4o!)
// @description:sr        Додаје АИ одговоре у Brave Search (покреће ГПТ-4о!)
// @description:ss        Iphendvulela izindlela zezilungiselelo ku-Brave Search (izenzakalo nge-GPT-4o!)
// @description:st        E kopanetse diqoqo tsa AI ka Brave Search (ka sebelisoa ke GPT-4o!)
// @description:su        Nambahkeun jawaban AI kana Brave Search (dikuatkeun ku GPT-4o!)
// @description:sv        Lägger till AI-svar till Brave Search (driven av GPT-4o!)
// @description:sw        Inaongeza majibu ya AI kwa Brave Search (inaendeshwa na GPT-4o!)
// @description:ta        Brave Search க்கு AI பதில்களைச் சேர்க்கிறது (GPT-4o மூலம் இயக்கப்படுகிறது!)
// @description:te        Brave Searchకి AI సమాధానాలను జోడిస్తుంది (GPT-4o ద్వారా ఆధారితం!)
// @description:tg        Ба Brave Search ҷавобҳои AI илова мекунад (аз ҷониби GPT-4o!)
// @description:th        เพิ่มคำตอบ AI ให้กับ Brave Search (ขับเคลื่อนโดย GPT-4o!)
// @description:ti        ናብ Brave Search (ብGPT-4o ዝሰርሕ!) ናይ AI መልስታት ይውስኸሉ።
// @description:tk        Brave Search-a AI jogaplaryny goşýar (GPT-4o bilen işleýär!)
// @description:tl        Nagdadagdag ng mga sagot ng AI sa Brave Search (pinapatakbo ng GPT-4o!)
// @description:tn        O amogela dipotso tsa AI mo Brave Search (e a nang le GPT-4o!)
// @description:to        Tambisa mabizo a AI ku Brave Search (mukutenga na GPT-4o!)
// @description:tr        Brave Search'ya yapay zeka yanıtları ekler (GPT-4o tarafından desteklenmektedir!)
// @description:ts        Ku engetela tinhlamulo ta AI eka Brave Search (leyi fambiwaka hi GPT-4o!)
// @description:tt        Brave Search'ка AI җаваплары өсти (GPT-4o белән эшләнгән!)
// @description:tw        Ɔde AI mmuae ka Brave Search (a GPT-4o na ɛma ahoɔden!) ho.
// @description:ug        Brave Search ۋەبسېتكە AI جاۋابلار قوشۇدۇ (GPT-4o تەكشۈرگۈچى بىلەن!)
// @description:uk        Додає відповіді штучного інтелекту в Brave Search (на базі GPT-4o!)
// @description:ur        Brave Search میں AI جوابات شامل کرتا ہے (GPT-4o کے ذریعے تقویت یافتہ!)
// @description:uz        Brave Search-ga AI javoblarini qo'shadi (GPT-4o tomonidan quvvatlanadi!)
// @description:vi        Thêm câu trả lời AI vào Brave Search (được cung cấp bởi GPT-4o!)
// @description:xh        Yongeza iimpendulo ze-AI kwi-Brave Search (ixhaswe yi-GPT-4o!)
// @description:yi        לייגט אַי ענטפֿערס צו Brave Search (Powered דורך GPT-4o!)
// @description:yo        Ṣe afikun awọn idahun AI si Brave Search (agbara nipasẹ GPT-4o!)
// @description:zh        为 Brave Search 添加 AI 答案(由 GPT-4o 提供支持!)
// @description:zh-CN     为 Brave Search 添加 AI 答案(由 GPT-4o 提供支持!)
// @description:zh-HK     為 Brave Search 添加 AI 答案(由 GPT-4o 提供支援!)
// @description:zh-SG     为 Brave Search 添加 AI 答案(由 GPT-4o 提供支持!)
// @description:zh-TW     為 Brave Search 添加 AI 答案(由 GPT-4o 提供支援!)
// @description:zu        Yengeza izimpendulo ze-AI ku-Brave Search (inikwa amandla yi-GPT-4o!)
// @author                KudoAI
// @namespace             https://kudoai.com
// @version               2024.11.21
// @license               MIT
// @icon                  https://media.bravegpt.com/images/icons/bravegpt/icon48.png?0a9e287
// @icon64                https://media.bravegpt.com/images/icons/bravegpt/icon64.png?0a9e287
// @antifeature           ads A very tiny text ad displays below BraveGPT chatbar. This motivates me to spend otherwise unpaid time upgrading script w/ new features & APIs.
// @antifeature           referral-link
// @compatible            chrome except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible            firefox
// @compatible            edge except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible            opera after allowing userscript manager access to search page results in opera://extensions
// @compatible            brave except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible            vivaldi except for Streaming Mode w/ Tampermonkey (use ScriptCat instead)
// @compatible            waterfox
// @compatible            librewolf
// @compatible            ghost
// @compatible            qq
// @compatible            whale
// @compatible            kiwi
// @compatible            mask
// @compatible            orion
// @match                 *://search.brave.com/search*
// @include               https://auth0.openai.com
// @connect               api.binjie.fun
// @connect               api.openai.com
// @connect               api11.gptforlove.com
// @connect               cdn.jsdelivr.net
// @connect               chatai.mixerbox.com
// @connect               chatgpt.com
// @connect               update.greasyfork.org
// @connect               fanyi.sogou.com
// @require               https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js@3.3.5/dist/chatgpt.min.js#sha256-rfC4kk8q0byrafp7X0Qf9vaa3JNvkHRwNnUt6uL2hUE=
// @require               https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js#sha256-dppVXeVTurw1ozOPNE3XqhYmDJPOosfbKQcHyQSE58w=
// @require               https://cdn.jsdelivr.net/npm/generate-ip@2.4.4/dist/generate-ip.min.js#sha256-aQQKAQcMgCu8IpJp9HKs387x0uYxngO+Fb4pc5nSF4I=
// @require               https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js#sha256-g3pvpbDHNrUrveKythkPMF2j/J7UFoHbUyFQcFe1yEY=
// @require               https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js#sha256-n0UwfFeU7SR6DQlfOmLlLvIhWmeyMnIDp/2RmVmuedE=
// @require               https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js#sha256-e1fUJ6xicGd9r42DgN7SzHMzb5FJoWe44f4NbvZmBK4=
// @require               https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js#sha256-Ffq85bZYmLMrA/XtJen4kacprUwNbYdxEKd0SqhHqJQ=
// @resource bgptIcon     https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@ad5606e/media/images/icons/bravegpt/icon64.png.b64#sha256-Abqr6XIwT+g72ig2haUUkniR89b5UlxL28cAI6BVT/c=
// @resource bgptLSlogo   https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@01dd539/media/images/logos/bravegpt/lightmode/logo730x155.png.b64#sha256-gGomHdYcs/AE4Ep8dAJhPFbCX6uyHmb38vi9hWYJZLI=
// @resource bgptDSlogo   https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@01dd539/media/images/logos/bravegpt/darkmode/logo730x155.png.b64#sha256-2Qx4bTS8s7dKj4m2dsJdPnijThaYRwYQMi30+KjtopI=
// @resource hljsCSS      https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/dark.min.css#sha256-v0N76BFFkH0dCB8bUr4cHSVN8A/zCaOopMuSmJWV/5w=
// @resource bsbgCSS      https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@d7fd458/styles/css/black-rising-stars.min.css#sha256-bXbVZUD7ciKqK0wU/BLQzh08JwkoNExHHqXITugd/3o=
// @resource wsbgCSS      https://cdn.jsdelivr.net/gh/KudoAI/bravegpt@d7fd458/styles/css/white-rising-stars.min.css#sha256-ya9newifevSPO1Q4AzMf42yAF6TE+iZHrDbVj0HyuEM=
// @grant                 GM_getValue
// @grant                 GM_setValue
// @grant                 GM_deleteValue
// @grant                 GM_cookie
// @grant                 GM_registerMenuCommand
// @grant                 GM_unregisterMenuCommand
// @grant                  GM_openInTab
// @grant                 GM_getResourceText
// @grant                 GM_xmlhttpRequest
// @grant                 GM.xmlHttpRequest
// @noframes
// @homepageURL           https://www.bravegpt.com
// @supportURL            https://support.bravegpt.com
// @contributionURL       https://github.com/sponsors/KudoAI
// ==/UserScript==

// Dependencies:
// ✓ chatgpt.js (https://chatgpt.js.org) © 2023–2024 KudoAI & contributors under the MIT license
// ✓ generate-ip (https://generate-ip.org) © 2024 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

// Documentation: https://docs.bravegpt.com

(async () => {

    // Init APP info
    const app = {
        name: 'BraveGPT', symbol: '🦁', configKeyPrefix: 'braveGPT',
        urls: {
            app: 'https://www.bravegpt.com',
            chatgptJS: 'https://chatgpt.js.org',
            gitHub: 'https://github.com/KudoAI/bravegpt',
            greasyFork: 'https://greasyfork.org/scripts/462440-bravegpt',
            publisher: 'https://www.kudoai.com',
            relatedApps: 'https://github.com/adamlui/ai-web-extensions',
            review: {
                alternativeTo: 'https://alternativeto.net/software/bravegpt/about/',
                greasyFork: 'https://greasyfork.org/scripts/462440-bravegpt/feedback#post-discussion',
                productHunt: 'https://www.producthunt.com/products/bravegpt/reviews/new'
            },
            support: 'https://support.bravegpt.com'
        },
        latestAssetCommitHash: '27282d5' // for cached messages.json
    }
    app.urls.assetHost = app.urls.gitHub.replace('github.com', 'cdn.jsdelivr.net/gh') + `@${app.latestAssetCommitHash}`
    app.urls.update = app.urls.greasyFork.replace('https://', 'https://update.')
        .replace(/(\d+)-?([a-z-]*)$/i, (_, id, name) => `${id}/${ name || 'script' }.meta.js`)

    // Init ENV info
    const env = {
        browser: {},
        scriptManager: {
            name: (() => { try { return GM_info.scriptHandler } catch (err) { return 'unknown' }})(),
            version: (() => { try { return GM_info.version } catch (err) { return 'unknown' }})()
        }
    };
    ['Chrome', 'Firefox', 'Edge', 'Brave', 'Mobile'].forEach(platform =>
        env.browser[`is${ platform == 'Firefox' ? 'FF' : platform }`] = chatgpt.browser['is' + platform]())
    env.browser.isPortrait = env.browser.isMobile && (window.innerWidth < window.innerHeight)

    // Init DEBUG mode
    const config = {}, settings = {
        load(...keys) {
            if (Array.isArray(keys[0])) keys = keys[0] // use 1st array arg, else all comma-separated ones
            keys.forEach(key => config[key] = GM_getValue(app.configKeyPrefix + '_' + key, false))
        },
        save(key, value) { GM_setValue(app.configKeyPrefix + '_' + key, value) ; config[key] = value }
    } ; settings.load('debugMode')

    // Define LOG props/functions
    const log = {

        styles: {
            prefix: {
                base: `color: white ; padding: 2px 3px 2px 5px ; border-radius: 2px ; ${ env.browser.isFF ? 'font-size: 13px ;' : '' }`,
                info: 'background: linear-gradient(344deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 39%, rgba(30,29,43,0.6026611328125) 93%)',
                working: 'background: linear-gradient(342deg, rgba(255,128,0,1) 0%, rgba(255,128,0,0.9612045501794468) 57%, rgba(255,128,0,0.7539216370141807) 93%)' ,
                success: 'background: linear-gradient(344deg, rgba(0,107,41,1) 0%, rgba(3,147,58,1) 39%, rgba(24,126,42,0.7735294801514356) 93%)',
                warning: 'background: linear-gradient(344deg, rgba(255,0,0,1) 0%, rgba(232,41,41,0.9079832616640406) 57%, rgba(222,49,49,0.6530813008797269) 93%)',
                caller: 'color: blue'
            },

            msg: { working: 'color: #ff8000', warning: 'color: red' }
        },

        regEx: {
            greenVals: { caseInsensitive: /\b(?:true|\d+)\b|success\W?/i, caseSensitive: /\bON\b/ },
            redVals: { caseInsensitive: /\bfalse\b|error\W?/i, caseSensitive: /\BOFF\b/ },
            purpVals: /[ '"]\w+['"]?: / },

        prettifyObj(obj) { return JSON.stringify(obj)
            .replace(/([{,](?=")|":)/g, '$1 ') // append spaces to { and "
            .replace(/((?<!\})\})/g, ' $1') // prepend spaces to }
            .replace(/"/g, '\'') // replace " w/ '
        },

        toTitleCase(str) { return str.charAt(0).toUpperCase() + str.slice(1) }

    } ; ['info', 'error', 'debug'].forEach(logType =>
        log[logType] = function() {
            if (logType == 'debug' && !config.debugMode) return

            const args = Array.from(arguments).map(arg => typeof arg == 'object' ? JSON.stringify(arg) : arg),
                  msgType = args.some(arg => /\.{3}$/.test(arg)) ? 'working'
                          : args.some(arg => /\bsuccess\b|!$/i.test(arg)) ? 'success'
                          : args.some(arg => /\b(?:error|fail)\b/i.test(arg)) || logType == 'error' ? 'warning' : 'info',
                  prefixStyle = log.styles.prefix.base + log.styles.prefix[msgType],
                  baseMsgStyle = log.styles.msg[msgType], msgStyles = []

            // Combine regex
            const allPatterns = Object.values(log.regEx).flatMap(val =>
                val instanceof RegExp ? [val] : Object.values(val).filter(val => val instanceof RegExp))
            const combinedPattern = new RegExp(allPatterns.map(pattern => pattern.source).join('|'), 'g')

            // Combine args into finalMsg, color chars
            let finalMsg = logType == 'error' && args.length == 1 ? 'ERROR: ' : ''
            args.forEach((arg, idx) => {
                finalMsg += idx > 0 ? (idx == 1 ? ': ' : ' ') : '' // separate multi-args
                finalMsg += arg?.toString().replace(combinedPattern, match => {
                    const matched = (
                        Object.values(log.regEx.greenVals).some(val => {
                            if (val.test(match)) { msgStyles.push('color: green', baseMsgStyle) ; return true }})
                     || Object.values(log.regEx.redVals).some(val => {
                            if (val.test(match)) { msgStyles.push('color: red', baseMsgStyle) ;  return true }}))
                    if (!matched && log.regEx.purpVals.test(match)) { msgStyles.push('color: #dd29f4', baseMsgStyle) }
                    return `%c${match}%c`
                })
            })

            console[logType == 'error' ? logType : 'info'](
                `${app.symbol} %c${app.name}%c ${ log.caller ? `${log.caller} » ` : '' }%c${finalMsg}`,
                prefixStyle, log.styles.prefix.caller, baseMsgStyle, ...msgStyles
            )
        }
    )

    // Init COMPATIBILITY flags
    log.debug('Initializing compatibility flags...')
    const streamingSupported = {
        byBrowser: !(env.scriptManager.name == 'Tampermonkey' && (env.browser.isChrome || env.browser.isEdge || env.browser.isBrave)),
        byScriptManager: /Tampermonkey|ScriptCat/.test(env.scriptManager.name) }
    log.debug(`Success! streamingSupported = ${log.prettifyObj(streamingSupported)}`)

    // Init CONFIG
    log.debug('Initializing config...')
    Object.assign(config, { userLanguage: chatgpt.getUserLanguage(), minFontSize: 11, maxFontSize: 24, lineHeightRatio: 1.313 })
    config.userLocale = config.userLanguage.includes('-') ? config.userLanguage.split('-')[1].toLowerCase() : ''
    settings.load('anchored', 'autoGetDisabled', 'autoFocusChatbarDisabled', 'autoScroll', 'bgAnimationsDisabled', 'expanded',
                  'fgAnimationsDisabled', 'fontSize', 'minimized', 'prefixEnabled', 'proxyAPIenabled', 'replyLanguage',
                  'rqDisabled', 'scheme', 'stickySidebar', 'streamingDisabled', 'suffixEnabled', 'widerSidebar')
    if (!config.replyLanguage) settings.save('replyLanguage', config.userLanguage) // init reply language if unset
    if (!config.fontSize) settings.save('fontSize', 16) // init reply font size if unset
    if (!streamingSupported.byBrowser || !streamingSupported.byScriptManager) settings.save('streamingDisabled', true) // disable Streaming in unspported env
    log.debug(`Success! config = ${log.prettifyObj(config)}`)

    // Init API props
    log.debug('Initializing API properties...')
    const apis = {
        'AIchatOS': {
            endpoint: 'https://api.binjie.fun/api/generateStream',
            expectedOrigin: {
                url: 'https://chat18.aichatos68.com',
                headers: { 'Accept': 'application/json, text/plain, */*', 'Priority': 'u=0', 'Sec-Fetch-Site': 'cross-site' }},
            method: 'POST', streamable: true, accumulatesText: false, failFlags: ['很抱歉地', '系统公告'],
            userID: '#/chat/' + Date.now() },
        'GPTforLove': {
            endpoint: 'https://api11.gptforlove.com/chat-process',
            expectedOrigin: {
                url: 'https://ai27.gptforlove.com',
                headers: { 'Accept': 'application/json, text/plain, */*', 'Priority': 'u=0', 'Sec-Fetch-Site': 'same-site' }},
            method: 'POST', streamable: true, accumulatesText: true, failFlags: ['[\'"]?status[\'"]?:\\s*[\'"]Fail[\'"]'] },
        'MixerBox AI': {
            endpoint: 'https://chatai.mixerbox.com/api/chat/stream',
            expectedOrigin: {
                url: 'https://chatai.mixerbox.com',
                headers: { 'Accept': '*/*', 'Alt-Used': 'chatai.mixerbox.com', 'Sec-Fetch-Site': 'same-origin' }},
            method: 'POST', streamable: true, accumulatesText: false },
        'OpenAI': {
            endpoints: {
                auth: 'https://auth0.openai.com',
                completions: 'https://api.openai.com/v1/chat/completions',
                session: 'https://chatgpt.com/api/auth/session' },
            expectedOrigin: {
                url: 'https://chatgpt.com',
                headers: { 'Accept': '*/*', 'Priority': 'u=4', 'Sec-Fetch-Site': 'same-site' }},
            method: 'POST', streamable: true }
    }
    log.debug(`Success! apis = ${log.prettifyObj(apis)}`)

    // Init INPUT EVENTS
    log.debug('Initializing input events...')
    const inputEvents = {} ; ['down', 'move', 'up'].forEach(action =>
          inputEvents[action] = ( window.PointerEvent ? 'pointer' : env.browser.isMobile ? 'touch' : 'mouse' ) + action)
    log.debug(`Success! inputEvents = ${log.prettifyObj(inputEvents)}`)

    // Init app MESSAGES
    log.debug('Initializing app messages...')
    const xhr = env.scriptManager.name == 'OrangeMonkey' ? GM_xmlhttpRequest : GM.xmlHttpRequest
    app.msgs = {
        appDesc: 'Adds ChatGPT answers to Brave Search sidebar (powered by GPT-4o!)',
        menuLabel_proxyAPImode: 'Proxy API Mode',
        menuLabel_autoGetAnswers: 'Auto-Get Answers',
        menuLabel_autoFocusChatbar: 'Auto-Focus Chatbar',
        menuLabel_whenStreaming: 'when streaming',
        menuLabel_show: 'Show',
        menuLabel_relatedQueries: 'Related Queries',
        menuLabel_require: 'Require',
        menuLabel_beforeQuery: 'before query',
        menuLabel_afterQuery: 'after query',
        menuLabel_widerSidebar: 'Wider Sidebar',
        menuLabel_stickySidebar: 'Sticky Sidebar',
        menuLabel_pinTo: 'Pin to',
        menuLabel_top: 'Top',
        menuLabel_sidebar: 'Sidebar',
        menuLabel_bottom: 'Bottom',
        menuLabel_background: 'Background',
        menuLabel_foreground: 'Foreground',
        menuLabel_animations: 'Animations',
        menuLabel_replyLanguage: 'Reply Language',
        menuLabel_colorScheme: 'Color Scheme',
        menuLabel_auto: 'Auto',
        menuLabel_about: 'About',
        menuLabel_settings: 'Settings',
        about_version: 'Version',
        about_poweredBy: 'Powered by',
        about_sourceCode: 'Source code',
        scheme_light: 'Light',
        scheme_dark: 'Dark',
        mode_proxy: 'Proxy Mode',
        mode_streaming: 'Streaming Mode',
        mode_autoScroll: 'Auto-Scroll',
        mode_prefix: 'Prefix Mode',
        mode_suffix: 'Suffix Mode',
        mode_anchor: 'Anchor Mode',
        mode_debug: 'Debug Mode',
        tooltip_playAnswer: 'Play answer',
        tooltip_fontSize: 'Font size',
        tooltip_sendReply: 'Send reply',
        tooltip_askRandQuestion: 'Ask random question',
        tooltip_minimize: 'Minimize',
        tooltip_restore: 'Restore',
        tooltip_expand: 'Expand',
        tooltip_shrink: 'Shrink',
        tooltip_close: 'Close',
        tooltip_copy: 'Copy',
        tooltip_reply: 'Reply',
        tooltip_code: 'Code',
        tooltip_sendRelatedQuery: 'Send related query',
        helptip_proxyAPImode: 'Uses a Proxy API for no-login access to AI',
        helptip_streamingMode: 'Receive replies in a continuous text stream',
        helptip_autoGetAnswers: 'Auto-send queries to BraveGPT when using search engine',
        helptip_autoFocusChatbar: 'Auto-focus chatbar whenever it appears',
        helptip_autoScroll: 'Auto-scroll responses as they generate in Streaming Mode',
        helptip_showRelatedQueries: 'Show related queries below chatbar',
        helptip_prefixMode: 'Require "/" before queries for answers to show',
        helptip_suffixMode: 'Require "?" after queries for answers to show',
        helptip_widerSidebar: 'Horizontally expand search page sidebar',
        helptip_stickySidebar: 'Makes BraveGPT visible in sidebar even as you scroll',
        helptip_anchorMode: 'Anchor BraveGPT to bottom of window',
        helptip_bgAnimations: 'Show animated backgrounds in UI components',
        helptip_fgAnimations: 'Show foreground animations in UI components',
        helptip_replyLanguage: 'Language for BraveGPT to reply in',
        helptip_colorScheme: 'Scheme to display BraveGPT UI components in',
        helptip_debugMode: 'Show detailed logging in browser console',
        placeholder_askSomethingElse: 'Ask something else',
        placeholder_typeSomething: 'Type something',
        prompt_updateReplyLang: 'Update reply language',
        alert_langUpdated: 'Language updated',
        alert_willReplyIn: 'will reply in',
        alert_yourSysLang: 'your system language',
        alert_choosePlatform: 'Choose a platform',
        alert_updateAvail: 'Update available',
        alert_newerVer: 'An update to',
        alert_isAvail: 'is available',
        alert_upToDate: 'Up-to-date',
        alert_isUpToDate: 'is up-to-date',
        alert_isUnsupportedIn: 'is unsupported in',
        alert_whenUsing: 'when using',
        alert_pleaseUse: 'Please use',
        alert_instead: 'instead',
        alert_unavailable: 'unavailable',
        alert_isOnlyAvailFor: 'is only available for',
        alert_and: 'and',
        alert_userscriptMgrNoStream: 'Your userscript manager does not support returning stream responses',
        alert_isCurrentlyOnlyAvailBy: 'is currently only available by',
        alert_openAIsupportSoon: 'Support for OpenAI API will be added shortly',
        alert_waitingFor: 'Waiting for',
        alert_response: 'response',
        alert_login: 'Please login',
        alert_thenRefreshPage: 'then refresh this page',
        alert_tooManyRequests: 'ChatGPT is flooded with too many requests',
        alert_parseFailed: 'Failed to parse response JSON',
        alert_checkCloudflare: 'Please pass Cloudflare security check',
        alert_notWorking: 'is not working',
        alert_ifIssuePersists: 'If issue persists',
        alert_try: 'Try',
        alert_switchingOn: 'switching on',
        alert_switchingOff: 'switching off',
        notif_copiedToClipboard: 'Copied to clipboard',
        btnLabel_sendQueryToApp: 'Send search query to BraveGPT',
        btnLabel_moreAIextensions: 'More AI Extensions',
        btnLabel_rateUs: 'Rate Us',
        btnLabel_getSupport: 'Get Support',
        btnLabel_updateCheck: 'Check for Updates',
        btnLabel_update: 'Update',
        btnLabel_dismiss: 'Dismiss',
        link_viewChanges: 'View changes',
        link_shareFeedback: 'Feedback',
        prefix_exit: 'Exit',
        state_on: 'On',
        state_off: 'Off'
    }
    if (!config.userLanguage.startsWith('en')) { // localize msgs for non-English users
        const localizedMsgs = await new Promise(resolve => {
            const msgHostDir = app.urls.assetHost + '/greasemonkey/_locales/',
                  msgLocaleDir = ( config.userLanguage ? config.userLanguage.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 up to 3X (original/region-stripped/EN) only
                    msgHref = config.userLanguage.includes('-') && msgXHRtries == 1 ? // if regional lang on 1st try...
                        msgHref.replace(/([^_]+_[^_]+)_[^/]*(\/.*)/, '$1$2') // ...strip region before retrying
                            : ( msgHostDir + 'en/messages.json' ) // else use default English messages
                    fetchMsgs()
                }
            }
            fetchMsgs()
        })
        Object.assign(app.msgs, localizedMsgs)
    }
    log.debug(`Success! app.msgs = ${log.prettifyObj(app.msgs)}`)

    // Init SETTINGS controls
    log.debug('Initializing settings properties...')
    Object.assign(settings, { controls: {
        proxyAPIenabled: { type: 'toggle', icon: 'sunglasses',
            label: app.msgs.menuLabel_proxyAPImode,
            helptip: app.msgs.helptip_proxyAPImode },
        streamingDisabled: { type: 'toggle', icon: 'signalStream',
            label: app.msgs.mode_streaming,
            helptip: app.msgs.helptip_streamingMode },
        autoGetDisabled: { type: 'toggle', icon: 'speechBalloonLasso',
            label: app.msgs.menuLabel_autoGetAnswers,
            helptip: app.msgs.helptip_autoGetAnswers },
        autoFocusChatbarDisabled: { type: 'toggle', mobile: false, icon: 'caretsInward',
            label: app.msgs.menuLabel_autoFocusChatbar,
            helptip: app.msgs.helptip_autoFocusChatbar },
        autoScroll: { type: 'toggle', mobile: false, icon: 'arrowsDown',
            label: `${app.msgs.mode_autoScroll} (${app.msgs.menuLabel_whenStreaming})`,
            helptip: app.msgs.helptip_autoScroll },
        rqDisabled: { type: 'toggle', icon: 'speechBalloons',
            label: `${app.msgs.menuLabel_show} ${app.msgs.menuLabel_relatedQueries}`,
            helptip: app.msgs.helptip_showRelatedQueries },
        prefixEnabled: { type: 'toggle', icon: 'slash',
            label: `${app.msgs.menuLabel_require} "/" ${app.msgs.menuLabel_beforeQuery}`,
            helptip: app.msgs.helptip_prefixMode },
        suffixEnabled: { type: 'toggle', icon: 'questionMark',
            label: `${app.msgs.menuLabel_require} "?" ${app.msgs.menuLabel_afterQuery}`,
            helptip: app.msgs.helptip_suffixMode },
        widerSidebar: { type: 'toggle', mobile: false, icon: 'widescreen',
            label: app.msgs.menuLabel_widerSidebar,
            helptip: app.msgs.helptip_widerSidebar },
        stickySidebar: { type: 'toggle', mobile: false, icon: 'webCorner',
            label: app.msgs.menuLabel_stickySidebar,
            helptip: app.msgs.helptip_stickySidebar },
        anchored: { type: 'toggle', mobile: false, icon: 'anchor',
            label: app.msgs.mode_anchor,
            helptip: app.msgs.helptip_anchorMode },
        bgAnimationsDisabled: { type: 'toggle', icon: 'sparkles',
            label: `${app.msgs.menuLabel_background} ${app.msgs.menuLabel_animations}`,
            helptip: app.msgs.helptip_bgAnimations },
        fgAnimationsDisabled: { type: 'toggle', icon: 'sparkles',
            label: `${app.msgs.menuLabel_foreground} ${app.msgs.menuLabel_animations}`,
            helptip: app.msgs.helptip_fgAnimations },
        replyLanguage: { 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',
            label: app.msgs.mode_debug,
            helptip: app.msgs.helptip_debugMode },
        about: { type: 'modal', icon: 'questionMarkCircle',
            label: `${app.msgs.menuLabel_about} ${app.name}...` }
    }})
    log.debug(`Success! settings.controls = ${log.prettifyObj(settings.controls)}`)

    // Init ALERTS
    Object.assign(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}.`,
        openAInotWorking: `OpenAI API ${app.msgs.alert_notWorking}.`,
        suggestProxy:     `${app.msgs.alert_try} ${app.msgs.alert_switchingOn} ${app.msgs.mode_proxy}`,
        suggestOpenAI:    `${app.msgs.alert_try} ${app.msgs.alert_switchingOff} ${app.msgs.mode_proxy}`
    }})

    // Define MENU functions

    const menu = {
        ids: [], state: {
            symbols: ['❌', '✔️'], separator: env.scriptManager.name == 'Tampermonkey' ? ' — ' : ': ',
            words: [app.msgs.state_off.toUpperCase(), app.msgs.state_on.toUpperCase()]
        },

        register() {

            // Add Proxy API Mode toggle
            const pmLabel = menu.state.symbols[+config.proxyAPIenabled] + ' '
                          + settings.controls.proxyAPIenabled.label + ' '
                          + menu.state.separator + menu.state.words[+config.proxyAPIenabled]
            const pmRegisterOptions = ( // add menu tooltip in TM 5.0+
                env.scriptManager.name == 'Tampermonkey' && parseInt(env.scriptManager.version.split('.')[0]) >= 5 ?
                    { title: settings.controls.proxyAPIenabled.helptip } : undefined )
            menu.ids.push(GM_registerMenuCommand(pmLabel, toggle.proxyMode, pmRegisterOptions))

            // Add About entry
            const aboutLabel = `💡 ${settings.controls.about.label}`
            menu.ids.push(GM_registerMenuCommand(aboutLabel, modals.about.show))

            // Add Settings entry
            const settingsLabel = `⚙️ ${app.msgs.menuLabel_settings}`
            menu.ids.push(GM_registerMenuCommand(settingsLabel, modals.settings.show))
        },

        refresh() {
            log.caller = 'menu.refresh()'
            log.debug('Refreshing toolbar menu...')
            if (env.scriptManager.name == 'OrangeMonkey') { log.debug('OrangeMonkey userscript manager unsupported.') ; return }
            for (const id of menu.ids) { GM_unregisterMenuCommand(id) } menu.register()
            log.debug('Success! Menu refreshed')
        }
    }

    function updateCheck() {
        log.caller = 'updateCheck()'
        const currentVer = GM_info.script.version
        log.debug(`currentVer = ${currentVer}`)

        // Fetch latest meta
        log.debug('Fetching latest userscript meta...')
        xhr({
            method: 'GET', url: app.urls.update + '?t=' + Date.now(),
            headers: { 'Cache-Control': 'no-cache' },
            onload: resp => {
                log.debug('Success! Response received')
                const updateAlertWidth = 489

                // Compare versions
                log.debug('Comparing versions...')
                const latestVer = /@version +(.*)/.exec(resp.responseText)[1]
                for (let i = 0 ; i < 4 ; i++) { // loop thru subver's
                    const currentSubVer = parseInt(currentVer.split('.')[i], 10) || 0,
                          latestSubVer = parseInt(latestVer.split('.')[i], 10) || 0
                    if (currentSubVer > latestSubVer) break // out of comparison since not outdated
                    else if (latestSubVer > currentSubVer) { // if outdated

                        // Alert to update
                        log.debug(`Update v${latestVer} found!`)
                        const updateModalID = siteAlert(`🚀 ${app.msgs.alert_updateAvail}!`, // title
                            `${app.msgs.alert_newerVer} ${app.name} `
                                + `(v${latestVer}) ${app.msgs.alert_isAvail}!  `
                                + '<a target="_blank" rel="noopener" style="font-size: 0.93rem" '
                                    + 'href="' + app.urls.gitHub + '/commits/main/greasemonkey/'
                                    + app.urls.update.replace(/[^/]*\/([^/]*?)meta\.js/, '$1user.js') + '"'
                                    + `>${app.msgs.link_viewChanges}</a>`,
                            function update() { // button
                                GM_openInTab(app.urls.update.replace('meta.js', 'user.js') + '?t=' + Date.now(),
                                    { active: true, insert: true } // focus, make adjacent
                                ).onclose = () => location.reload()
                            }, '', updateAlertWidth
                        )
                        const updateModal = document.getElementById(updateModalID).firstChild

                        // Localize button labels if needed
                        if (!config.userLanguage.startsWith('en')) {
                            log.debug('Localizing button labels in non-English alert...')
                            const updateAlert = document.querySelector(`[id="${updateModalID}"]`),
                                  updateBtns = updateAlert.querySelectorAll('button')
                            updateBtns[1].textContent = app.msgs.btnLabel_update
                            updateBtns[0].textContent = app.msgs.btnLabel_dismiss
                        }

                        modals.init(updateModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns

                        return
                }}

                // Alert to no update found, nav back
                log.debug('No update found.')
                const noUpdateModalID = siteAlert(`${app.msgs.alert_upToDate}!`, // title
                    `${app.name} (v${currentVer}) ${app.msgs.alert_isUpToDate}!`, // msg
                    '', '', updateAlertWidth)
                const noUpdateModal = document.getElementById(noUpdateModalID).firstChild
                modals.init(noUpdateModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
                modals.about.show()
    }})}

    // Define FACTORY functions

    const create = {

        anchor(linkHref, displayContent, attrs = {}) {
            const anchor = document.createElement('a'),
                  defaultAttrs = { href: linkHref, target: '_blank', rel: 'noopener' },
                  finalAttrs = { ...defaultAttrs, ...attrs }
            Object.entries(finalAttrs).forEach(([attr, value]) => anchor.setAttribute(attr, value))
            if (displayContent) anchor.append(displayContent)
            return anchor
        },

        style(content) {
            const style = document.createElement('style')
            if (content) style.innerText = content
            return style
        },

        svgElem(type, attrs) {
            const elem = document.createElementNS('http://www.w3.org/2000/svg', type)
            for (const attr in attrs) elem.setAttributeNS(null, attr, attrs[attr])
            return elem
        }
    }

    // Define FEEDBACK functions

    function appAlert(...alerts) {
        alerts = alerts.flat() // flatten array args nested by spread operator
        appDiv.textContent = ''
        const alertP = document.createElement('p')
        alertP.id = 'bravegpt-alert' ; alertP.className = 'no-user-select'
        alertP.style.marginBottom = '-20px' // counteract appDiv padding

        alerts.forEach((alert, idx) => { // process each alert for display
            let msg = app.alerts[alert] || alert // use string verbatim if not found in app.alerts
            if (idx > 0) msg = ' ' + msg // left-pad 2nd+ alerts
            if (msg.includes(app.alerts.login)) session.deleteOpenAIcookies()
            if (msg.includes(app.alerts.waitingResponse)) alertP.classList.add('loading')

            // Add login link to login msgs
            if (msg.includes('@')) {
                msg += '<a class="alert-link" target="_blank" rel="noopener" href="https://chatgpt.com">chatgpt.com</a>,'
                     + ` ${app.msgs.alert_thenRefreshPage}.`
                     + ` (${app.msgs.alert_ifIssuePersists},`
                     + ` ${( app.msgs.alert_try ).toLowerCase() }`
                     + ` ${app.msgs.alert_switchingOn}`
                     + ` ${app.msgs.mode_proxy})`
            }

            // Hyperlink app.msgs.alert_switching<On|Off>
            const foundState = ['On', 'Off'].find(state =>
                msg.includes(app.msgs['alert_switching' + state]) || new RegExp(`\\b${state}\\b`, 'i').test(msg))
            if (foundState) { // hyperlink switch phrase for click listener to toggle.proxyMode()
                const switchPhrase = app.msgs['alert_switching' + foundState] || 'switching ' + foundState.toLowerCase()
                msg = msg.replace(switchPhrase, `<a class="alert-link" href="#">${switchPhrase}</a>`)
            }

            // Create/fill/append msg span
            const msgSpan = document.createElement('span')
            msgSpan.innerHTML = msg ; alertP.append(msgSpan)

            // Activate toggle link if necessary
            msgSpan.querySelector('[href="#"]')?.addEventListener('click', toggle.proxyMode)
        })
        appDiv.append(alertP)
    }

    function notify(msg, pos = '', notifDuration = '', shadow = 'shadow') {

        // Strip state word to append styled one later
        const foundState = menu.state.words.find(word => msg.includes(word))
        if (foundState) msg = msg.replace(foundState, '')

        // Show notification
        chatgpt.notify(msg, pos, notifDuration, shadow)
        const notif = document.querySelector('.chatgpt-notif:last-child')

        // Prepend app icon
        const notifIcon = icons.braveGPT.create()
        notifIcon.style.cssText = 'width: 32px ; position: relative ; top: 6px ; margin-right: 6px'
        notif.prepend(notifIcon)

        // Append notif type icon
        const iconStyles = 'width: 28px ; height: 28px ; position: relative ; top: 3px ; margin-left: 11px ;',
              mode = Object.keys(settings.controls).find(key => settings.controls[key].label.includes(msg.trim()))
        if (mode && !/(?:pre|suf)fix/.test(mode)) {
            const modeIcon = icons[settings.controls[mode].icon].create()
            modeIcon.style.cssText = iconStyles
                                   + ( /autoget|debug|focus|scroll/i.test(mode) ? 'top: 0.5px' : '' ) // raise some icons
                                   + ( /animation|debug/i.test(mode) ? 'width: 23px ; height: 23px' : '' ) // shrink some icon
            notif.append(modeIcon)
        }

        // Append styled state word
        if (foundState) {
            const styledStateSpan = document.createElement('span')
            styledStateSpan.style.cssText = `font-weight: bold ; color: ${
                foundState == menu.state.words[0] ? '#ef4848 ; text-shadow: rgba(255, 169, 225, 0.44) 2px 1px 5px'
                                                : '#5cef48 ; text-shadow: rgba(255, 250, 169, 0.38) 2px 1px 5px' }`
            styledStateSpan.append(foundState) ; notif.insertBefore(styledStateSpan, notif.children[2])
        }
    }

    function siteAlert(title = '', msg = '', btns = '', checkbox = '', width = 474) {
        return chatgpt.alert(title, msg, btns, checkbox, width )}

    // Define MODAL functions

    const modals = {

        init(modal) {

            // Add classes
            modal.classList.add('bravegpt-modal')
            modal.parentNode.classList.add('bravegpt-modal-bg', 'no-user-select')

            // Add listeners
            modal.onwheel = modal.ontouchmove = event => event.preventDefault() // disable wheel/swipe scrolling
            modal.onmousedown = modals.dragHandlers.mousedown
            fillStarryBG(modal) // add stars
            setTimeout(() => { // dim bg
                modal.parentNode.style.backgroundColor = `rgba(67, 70, 72, ${ ui.app.scheme == 'dark' ? 0.62 : 0.33 })`
                modal.parentNode.classList.add('animated')
            }, 100) // delay for transition fx

            // Glowup btns
            if (ui.app.scheme == 'dark' && !config.fgAnimationsDisabled) toggle.btnGlow()
        },

        hide(modal) {
            log.caller = 'modals.hide()'
            log.debug(`Dismissing div#${modal?.id}...`)
            const modalContainer = modal?.parentNode
            if (!modalContainer) return
            modalContainer.style.animation = 'modal-zoom-fade-out .135s ease-out'
            setTimeout(() => { modalContainer.remove() ; log.debug(`Success! div#${modal?.id} dismissed`)
                }, 105) // delay for fade-out
        },

        clickHandler(event) { // to dismiss modals
            log.caller = 'modals.clickHandler()'
            if (event.target == event.currentTarget || event.target instanceof SVGPathElement) {
                const targetModal = document.querySelector('[class$="-modal"]')
                log.debug(`Dismiss element of div#${targetModal?.id} clicked`)
                modals.hide(targetModal)
            }
        },

        dragHandlers: {
            mousedown(event) { // find modal, attach listeners, init XY offsets
                if (event.button != 0) return // prevent non-left-click drag
                if (getComputedStyle(event.target).cursor == 'pointer') return // prevent drag when clicking on interactive elems
                log.caller = 'modals.dragHandlers.mousedown()'
                modals.dragHandlers.draggableElem = event.currentTarget
                modals.dragHandlers.draggableElem.style.cursor = 'grabbing'
                log.debug(`Mouse down on div#${modals.dragHandlers.draggableElem?.id}`)
                event.preventDefault(); // prevent sub-elems like icons being draggable
                ['mousemove', 'mouseup'].forEach(event => document.addEventListener(event, modals.dragHandlers[event]))
                const draggableElemRect = modals.dragHandlers.draggableElem.getBoundingClientRect()
                modals.dragHandlers.offsetX = event.clientX - draggableElemRect.left +21
                modals.dragHandlers.offsetY = event.clientY - draggableElemRect.top +12
            },

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

            mouseup() { // remove listeners, reset modals.dragHandlerss.draggableElem
                log.caller = 'modals.dragHandlers.mouseup()'
                modals.dragHandlers.draggableElem.style.cursor = 'inherit'
                log.debug(`Mouse up on div#${modals.dragHandlers.draggableElem?.id}`);
                ['mousemove', 'mouseup'].forEach(event => document.removeEventListener(event, modals.dragHandlers[event]))
                modals.dragHandlers.draggableElem = null
            }
        },

        keyHandler(event) { // to dismiss modals
            log.caller = 'modals.keyHandler()'
            if (['Escape', 'Esc'].includes(event.key) || event.keyCode == 27) {
                log.debug('Escape pressed')
                const modal = document.querySelector('[class$="-modal"]')
                if (modal) modals.hide(modal)
            }
        },

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

        about: {
            show() {
                log.caller = 'modals.about.show()'

                // Hide Settings modal if visible
                const settingsModal = modals.settings.get()
                if (settingsModal) {
                    log.debug('Hiding visible Settings modal...')
                    modals.hide(settingsModal) ; log.caller = 'modals.about.show()'
                }

                log.debug('Showing About modal...')

                // Create/init modal
                const chatgptJSver = (/chatgpt-([\d.]+)\.min/.exec(GM_info.script.header) || [null, ''])[1]
                const aboutModalID = chatgpt.alert('',
                    '🏷️ ' + ( app.msgs.about_version ) + ': <span class="about-em">' + GM_info.script.version + '</span>\n'
                        + '⚡ ' + ( app.msgs.about_poweredBy ) + ': '
                            + `<a href="${app.urls.chatgptJS}" target="_blank" rel="noopener">chatgpt.js</a>`
                            + ( chatgptJSver ? ( ' v' + chatgptJSver ) : '' ) + '\n'
                        + '📜 ' + ( app.msgs.about_sourceCode )
                            + `: <a href="${app.urls.gitHub}" target="_blank" rel="nopener">`
                                + app.urls.gitHub + '</a>',
                    [ // buttons
                        function checkForUpdates() { updateCheck() },
                        function getSupport() { modals.safeWinOpen(app.urls.support) },
                        function rateUs() { modals.feedback.show({ sites: 'review' }) },
                        function moreChatGPTapps() { modals.safeWinOpen(app.urls.relatedApps) }
                    ], '', 617) // modal width
                const aboutModal = document.getElementById(aboutModalID).firstChild

                // Add logo
                const aboutHeaderLogo = logos.braveGPT.create() ; aboutHeaderLogo.width = 375
                aboutHeaderLogo.style.cssText = `max-width: 98% ; margin: 1px ${ env.browser.isMobile ? 'auto' : '16%' } 0`
                aboutModal.insertBefore(aboutHeaderLogo, aboutModal.firstChild.nextSibling) // after close btn

                // Center text
                aboutModal.querySelector('h2').remove() // remove empty title h2
                aboutModal.querySelector('p').style.cssText = 'justify-self: center ; text-align: center ; overflow-wrap: anywhere ;'
                                                            + `margin: ${ env.browser.isPortrait ? '15px 0 -21px' : '9px 0 -12px' }`

                // Resize/format buttons to include emoji + localized label + hide Dismiss button
                aboutModal.querySelectorAll('button').forEach(btn => {
                    btn.style.cssText = 'height: 53px ; min-width: 136px'

                    // Emojize/localize label
                    if (/updates/i.test(btn.textContent)) btn.textContent = (
                        '🚀 ' + ( app.msgs.btnLabel_updateCheck ))
                    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 (/apps/i.test(btn.textContent)) btn.textContent = (
                        '🤖 ' + ( app.msgs.btnLabel_moreAIextensions ))
                    else btn.style.display = 'none' // hide Dismiss button
                })

                modals.init(aboutModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
                log.debug('Success! About Modal shown')
            }
        },

        feedback: {
            show(options) {
                log.caller = `modals.feedback.show(${ options ? `'${options}'` : '' })`
                log.debug('Showing Feedback modal...')

                // Init buttons
                let btns = [
                    function greasyFork() { modals.safeWinOpen(app.urls.review.greasyFork) },
                    function productHunt() { modals.safeWinOpen(app.urls.review.productHunt) },
                    function alternativeTo() { modals.safeWinOpen(app.urls.review.alternativeTo) }
                ]
                if (options.sites == 'feedback') btns.splice(1, 0,
                    function github() { modals.safeWinOpen(app.urls.gitHub + '/discussions/new/choose') })

                // Create/init modal
                const feedbackModalID = siteAlert(`${
                    app.msgs.alert_choosePlatform }:`, '', btns, '', 456)
                const feedbackModal = document.getElementById(feedbackModalID).firstChild

                // Center CTA
                feedbackModal.querySelector('h2').style.justifySelf = 'center'

                // Re-style button cluster
                const btnsDiv = feedbackModal.querySelector('.modal-buttons')
                btnsDiv.style.cssText += 'display: flex ; flex-wrap: wrap ; justify-content: center ;'
                                       + 'margin: 18px 0 6px !important' // close gap between title/btns

                // Format button labels + add v-padding
                btns = btnsDiv.querySelectorAll('button')
                btns.forEach((btn, idx) => {
                    if (idx == 0) btn.style.display = 'none' // hide Dismiss button
                    else if (btn.textContent == 'Github') btn.textContent = 'GitHub'
                    else if (btn.textContent == 'Alternative To') btn.textContent = 'AlternativeTo'
                    if (idx == btns.length -1) btn.classList.remove('primary-modal-btn') // de-emphasize last link
                    btn.style.marginTop = btn.style.marginBottom = '5px' // v-pad btns
                })

                modals.init(feedbackModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
                log.debug('Success! Feedback modal shown')
            }
        },

        replyLang: {
            show() {
                log.caller = 'modals.replyLang.show()'
                    while (true) {
                        let replyLanguage = prompt(
                            ( app.msgs.prompt_updateReplyLang ) + ':', config.replyLanguage)
                        if (replyLanguage == null) break // user cancelled so do nothing
                        else if (!/\d/.test(replyLanguage)) {
                            replyLanguage = ( // auto-case for menu/alert aesthetics
                                [2, 3].includes(replyLanguage.length) || replyLanguage.includes('-') ? replyLanguage.toUpperCase()
                                  : replyLanguage.charAt(0).toUpperCase() + replyLanguage.slice(1).toLowerCase() )
                            log.debug('Saving reply language...')
                            settings.save('replyLanguage', replyLanguage || config.userLanguage)
                            log.debug(`Success! config.replyLanguage = ${config.replyLanguage}`)
                            const langUpdatedAlertID = siteAlert(( app.msgs.alert_langUpdated ) + '!', // title
                                `${app.name} ${app.msgs.alert_willReplyIn} `
                                    + ( replyLanguage || app.msgs.alert_yourSysLang ) + '.',
                                '', '', 447) // confirmation width
                            const langUpdatedAlert = document.getElementById(langUpdatedAlertID).firstChild
                            modals.init(langUpdatedAlert) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
                            if (modals.settings.get()) // update settings menu status label
                                document.querySelector('#replyLanguage-menu-entry span').textContent = replyLanguage
                            break
            }}}
        },

        scheme: {
            show() {
                log.caller = 'modals.scheme.show()'
                log.debug('Showing Scheme modal...')

                // Create/init modal
                const schemeModalID = siteAlert(`${
                    app.name } ${( app.msgs.menuLabel_colorScheme ).toLowerCase() }:`, '',
                    [ function auto() {}, function light() {}, function dark() {} ], // buttons
                    '', 503) // px width
                const schemeModal = document.getElementById(schemeModalID).firstChild

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

                // Re-format each button
                const buttons = schemeModal.querySelectorAll('button'),
                      schemes = { 'light': '☀️', 'dark': '🌘', 'auto': '🌗'}
                for (const btn of buttons) {
                    const btnScheme = btn.textContent.toLowerCase()

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

                    // Prepend emoji + localize labels
                    if (Object.prototype.hasOwnProperty.call(schemes, btnScheme))
                        btn.textContent = `${schemes[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
                    const newBtn = btn.cloneNode(true) ; btn.parentNode.replaceChild(newBtn, btn)
                    newBtn.onclick = event => {
                        event.stopPropagation() // disable chatgpt.js dismissAlert()
                        const newScheme = btnScheme == 'auto' ? ( chatgpt.isDarkMode() ? 'dark' : 'light' ) : btnScheme
                        settings.save('scheme', btnScheme == 'auto' ? false : newScheme)
                        schemeModal.querySelectorAll('button').forEach(btn => btn.classList = '') // clear prev emphasized active scheme
                        newBtn.classList = 'primary-modal-btn' // emphasize newly active scheme
                        newBtn.style.cssText = 'pointer-events: none' // disable hover fx to show emphasis
                        setTimeout(() => { newBtn.style.pointerEvents = 'auto'; }, 100) // re-enable hover fx after 100ms to flicker emphasis
                        update.scheme(newScheme) ; schemeNotify(btnScheme)
                    }
                }

                modals.init(schemeModal) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
                log.debug('Success! Scheme modal shown')

                function schemeNotify(scheme) {

                    // Show notification
                    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],
                          schemeIcon = icons[ui.app.scheme == 'light' ? 'sun' : scheme == 'dark' ? 'moon' : 'arrowsCycle'].create()
                    schemeIcon.style.cssText = 'width: 23px ; height: 23px ; position: relative ; top: 3px ; margin-left: 6px'
                    notif.append(schemeIcon)
                }
            }
        },

        settings: {

            createAppend() {
                log.caller = 'modals.settings.createAppend()'

                // Init master elems
                const settingsContainer = document.createElement('div'),
                      settingsModal = document.createElement('div') ; settingsModal.id = 'bravegpt-settings'
                      settingsContainer.append(settingsModal)
                modals.init(settingsModal) // add classes/stars, disable wheel-scrolling, dim bg

                // Init settings keys
                log.debug('Initializing settings keys...')
                const settingsKeys = Object.keys(settings.controls).filter(key => !(env.browser.isMobile && settings.controls[key].mobile == false))
                log.debug(`Success! settingsKeys = ${log.prettifyObj(settingsKeys)}`)

                // Init logo
                const settingsIcon = icons.braveGPT.create()
                settingsIcon.style.cssText = `width: ${ env.browser.isPortrait ? 63 : 67 }px ; margin-bottom: 10px ;`
                                           + `position: relative ; top: -29px ; right: ${ env.browser.isPortrait ? -5 : 7 }px ;`
                                           + 'filter: drop-shadow(5px 5px 15px rgba(0, 0, 0, 0.3))'
                // Init title
                const settingsTitleDiv = document.createElement('div') ; settingsTitleDiv.id = 'bravegpt-settings-title'
                const settingsTitleH4 = document.createElement('h4') ; settingsTitleH4.textContent = app.msgs.menuLabel_settings
                const settingsTitleIcon = icons.sliders.create()
                settingsTitleIcon.style.cssText = 'width: 21px ; height: 21px ; margin-right: -4px ; position: relative ; top: 2px ; right: 10px'
                settingsTitleH4.prepend(settingsTitleIcon) ; settingsTitleDiv.append(settingsTitleH4)

                // Init settings lists
                log.debug('Initializing settings lists...')
                const settingsLists = [], middleGap = 30, // px
                      settingsListContainer = document.createElement('div'),
                      settingsListCnt = ( env.browser.isMobile && ( env.browser.isPortrait || settingsKeys.length < 8 )) ? 1 : 2,
                      settingItemCap = Math.floor(settingsKeys.length /2)
                for (let i = 0 ; i < settingsListCnt ; i++) settingsLists.push(document.createElement('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 ; border-right: 1px dotted ${ ui.app.scheme == 'dark' ? 'white' : 'black '}` )
                }
                log.debug(`Success! settingsListCnt = ${settingsListCnt}`)

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

                    // Create/append item/label elems
                    const settingItem = document.createElement('li') ; settingItem.id = key + '-menu-entry'
                    settingItem.title = setting.helptip || '' // for hover assistance
                    const settingLabel = document.createElement('label') ; settingLabel.textContent = setting.label
                    settingItem.append(settingLabel) ; (settingsLists[env.browser.isPortrait ? 0 : +(idx >= settingItemCap)]).append(settingItem)

                    // Create/prepend icons
                    const settingIcon = icons[setting.icon].create(/bg|fg/.exec(key)?.[0] ?? '')
                    settingIcon.style.cssText = 'position: relative ;' + (
                        /proxy/i.test(key) ? 'top: 3px ; left: -0.5px ; margin-right: 9px'
                      : /streaming/i.test(key) ? 'top: 3px ; left: 0.5px ; margin-right: 9px'
                      : /auto(?:get|focus)/i.test(key) ? 'top: 4.5px ; margin-right: 7px'
                      : /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' : ''
                    )
                    settingItem.prepend(settingIcon)

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

                        // Init toggle input
                        const settingToggle = document.createElement('input'),
                              settingToggleAttrs = [['type', 'checkbox'], ['disabled', true]]
                        settingToggleAttrs.forEach(([attr, value]) => settingToggle.setAttribute(attr, value))
                        settingToggle.checked = config[key] ^ key.includes('Disabled') // init based on config/name
                                              && !(key == 'streamingDisabled' && !config.proxyAPIenabled) // uncheck Streaming in OpenAI mode
                        settingToggle.style.display = 'none' // hide checkbox

                        // Create/stylize switch
                        const switchSpan = document.createElement('span')
                        Object.assign(switchSpan.style, {
                            position: 'relative', left: '-1px', bottom:'-5.5px', float: 'right',
                            backgroundColor: settingToggle.checked ? '#ccc' : '#AD68FF', // init opposite  final color
                            width: '26px', height: '13px', '-webkit-transition': '.4s', transition: '0.4s',  borderRadius: '28px'
                        })

                        // Create/stylize knob
                        const knobSpan = document.createElement('span')
                        Object.assign(knobSpan.style, {
                            position: 'absolute', left: '1px', bottom: '1px',
                            width: '11px', height: '11px', content: '""', borderRadius: '28px',
                            transform: settingToggle.checked ? // init opposite final pos
                                'translateX(0)' : 'translateX(14px) translateY(0)',
                            backgroundColor: 'white',  '-webkit-transition': '0.2s', transition: '0.2s'
                        })

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

                        // Update visual state w/ animation
                        setTimeout(() => modals.settings.toggle.updateStyles(settingToggle), idx *25 -25)

                        // Add click listener
                        settingItem.onclick = () => {
                            if (!(key == 'streamingDisabled' // visually switch toggle if not Streaminng...
                                && (!streamingSupported.byBrowser || !streamingSupported.byScriptManager // ...in unsupported env...
                                || !config.proxyAPIenabled) // ...or in OpenAI mode
                            )) modals.settings.toggle.switch(settingToggle)

                            // Call specialized toggle funcs
                            const manualGetMatch = /(?:suf|pre)fix/.exec(key)
                            if (key.includes('proxy')) toggle.proxyMode()
                            else if (key.includes('streaming')) toggle.streaming()
                            else if (/autoget/i.test(key)) toggle.autoGet()
                            else if (key.includes('rq')) toggle.relatedQueries()
                            else if (manualGetMatch) toggle.manualGet(manualGetMatch[0])
                            else if (key.includes('Sidebar')) toggle.sidebar(/(.*?)Sidebar$/.exec(key)[1])
                            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 {
                                log.caller = 'settings.createAppend()'
                                log.debug(`Toggling ${settingItem.textContent} ${ key.includes('Disabled') ^ config[key] ? 'OFF' : 'ON' }...`)
                                settings.save(key, !config[key]) // update config
                                notify(`${settings.controls[key].label} ${menu.state.words[+(key.includes('Disabled') ^ config[key])]}`)
                                log[key.includes('debug') ? 'info' : 'debug'](`Success! config.${key} = ${config[key]}`)
                            }
                        }

                    // Add .active + config status + listeners to pop-up settings
                    } else {
                        settingItem.classList.add('active')
                        const configStatusSpan = document.createElement('span')
                        configStatusSpan.style.cssText = 'float: right ; font-size: 11px ; margin-top: 3px ;'
                            + ( !key.includes('about') ? 'text-transform: uppercase !important' : '' )
                        if (key.includes('replyLang')) {
                            configStatusSpan.textContent = config.replyLanguage
                            settingItem.onclick = modals.replyLang.show
                        } else if (key.includes('scheme')) {
                            modals.settings.updateSchemeStatus(configStatusSpan)
                            settingItem.onclick = modals.scheme.show
                        } else if (key.includes('about')) {
                            const innerDiv = document.createElement('div'),
                                  textGap = '&emsp;&emsp;&emsp;&emsp;&emsp;'
                            modals.settings.aboutContent = {}
                            modals.settings.aboutContent.short = `v${GM_info.script.version}`
                            modals.settings.aboutContent.long = (
                                  `${app.msgs.about_version}: <span class="about-em">v${ GM_info.script.version + textGap }</span>`
                                + `${app.msgs.about_poweredBy} <span class="about-em">chatgpt.js</span>${textGap}` )
                            for (let i = 0; i < 7; i++) modals.settings.aboutContent.long += modals.settings.aboutContent.long // make it long af
                            innerDiv.innerHTML = modals.settings.aboutContent[config.fgAnimationsDisabled ? 'short' : 'long']
                            innerDiv.style.float = config.fgAnimationsDisabled ? 'right' : ''
                            configStatusSpan.append(innerDiv) ; settingItem.onclick = modals.about.show
                        } settingItem.append(configStatusSpan)
                    }
                })
                settingsListContainer.append(...settingsLists)

                // Create close button
                log.debug('Creating Close button...')
                const closeBtn = document.createElement('div')
                closeBtn.classList.add('bravegpt-modal-close-btn', 'no-mobile-tap-outline')
                closeBtn.title = app.msgs.tooltip_close
                const closeSVG = icons.x.create() ; closeBtn.append(closeSVG)

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

                // Add listeners to dismiss modal
                const dismissElems = [settingsContainer, closeBtn, closeSVG]
                dismissElems.forEach(elem => elem.onclick = modals.clickHandler)

                return settingsContainer
            },

            get() { return document.getElementById('bravegpt-settings') },

            show() {
                log.caller = 'modals.settings.show()'
                log.debug('Showing Settings modal...')
                const settingsContainer = modals.settings.get()?.parentNode || modals.settings.createAppend()
                settingsContainer.style.display = '' // show modal
                log.caller = 'modals.settings.show()'
                if (env.browser.isMobile) { // scale 93% to viewport sides
                    log.debug('Scaling 93% to viewport sides...')
                    const settingsModal = settingsContainer.querySelector('#bravegpt-settings'),
                          scaleRatio = 0.93 * window.innerWidth / settingsModal.offsetWidth
                    settingsModal.style.transform = `scale(${scaleRatio})`
                }
                log.debug('Success! Settings modal shown')
            },

            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')
                    setTimeout(() => {
                        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[settingToggle.checked ? 'add' : 'remove']('active') // dim/brighten setting entry
                    }, 1) // min delay to trigger transition fx
                }
            },

            updateSchemeStatus(schemeStatusSpan = null) {
                schemeStatusSpan = schemeStatusSpan || document.querySelector('#scheme-menu-entry span')
                if (schemeStatusSpan) {
                    schemeStatusSpan.textContent = ''
                    schemeStatusSpan.append(...( // status txt + icon
                        config.scheme == 'dark' ? [document.createTextNode(app.msgs.scheme_dark), icons.moon.create()]
                      : config.scheme == 'light' ? [document.createTextNode(app.msgs.scheme_light), icons.sun.create()]
                      : [document.createTextNode(app.msgs.menuLabel_auto), icons.arrowsCycle.create()] ))
                    schemeStatusSpan.style.cssText += `; margin-top: ${ !config.scheme ? 3 : 0 }px !important`
                }
            }
        }
    }

    // Define MENU functions

    const menus = {
        fadeInDelay: 5, // ms

        show(menu) {
            menu.style.display = ''
            setTimeout(() => menu.classList.add('active'), menus.fadeInDelay)
        },

        pin: {
            clickHandler(event) {
                const pinMenu = event.target.closest('#pin-menu'),
                      itemLabel = event.target.textContent,
                      prevOffsetTop = appDiv.offsetTop

                // Switch mode
                if (itemLabel == app.msgs.menuLabel_top) toggle.sidebar('sticky')
                else if (itemLabel == app.msgs.menuLabel_sidebar) {
                    toggle.sidebar('sticky', 'off') ; toggle.anchorMode('off') }
                else if (itemLabel == app.msgs.menuLabel_bottom) toggle.anchorMode()

                // Close/update menu
                if (appDiv.offsetTop != prevOffsetTop) pinMenu.remove() // since app moved
                else menus.pin.update(pinMenu) // since menu stayed in place
            },

            createAppend() {
                const pinMenu = document.createElement('div') ; pinMenu.id = 'pin-menu'
                pinMenu.classList.add('bravegpt-menu', 'btn-tooltip', 'fade-in-less', 'no-user-select')
                menus.pin.update(pinMenu) ; appDiv.append(pinMenu)
                return pinMenu
            },

            update(pinMenu) {
                pinMenu.textContent = ''

                // Init core elems
                const pinMenuUL = document.querySelector('#pin-menu ul') || document.createElement('ul'),
                      pinMenuItems = []
                const pinMenulabels = [
                    `${app.msgs.menuLabel_pinTo}...`, app.msgs.menuLabel_top,
                    app.msgs.menuLabel_sidebar, app.msgs.menuLabel_bottom ]
                const pinMenuIcons = [icons.webCorner.create(), icons.sidebar.create(), icons.anchor.create(), icons.checkmark.create()]

                // Style icons
                pinMenuIcons.forEach(icon => icon.style.cssText = (
                    'width: 12px ; height: 12px ; position: relative ; top: 1px ; right: 5px ; margin-left: 5px'))
                pinMenuIcons[0].style.width = pinMenuIcons[0].style.height = '11px' // shrink corner web icon
                pinMenuIcons[3].style.cssText = 'position: relative ; float: right ; margin-right: -16px ; top: 4px' // re-style checkmarks

                // Fill menu UL
                for (let i = 0 ; i < 4 ; i++) {
                    pinMenuItems.push(document.createElement('li'))
                    pinMenuItems[i].textContent = pinMenulabels[i]
                    pinMenuItems[i].className = 'bravegpt-menu-item'
                    if (i == 0) { // format header item
                        pinMenuItems[i].innerHTML = `<b>${pinMenulabels[i]}</b>`
                        pinMenuItems[i].classList.add('bravegpt-menu-header') // to not apply hover fx from appStyle
                        pinMenuItems[i].style.cssText = 'margin-bottom: 1px ; border-bottom: 1px dotted white'
                    } else if (i == 1) pinMenuItems[i].style.marginTop = '3px' // top-pad first non-header item
                    pinMenuItems[i].style.paddingRight = '24px' // make room for checkmark
                    pinMenuItems[i].prepend(i > 0 ? pinMenuIcons[i -1] : '') // prepend left icon
                    if (i == 1 && config.stickySidebar // 'Top' item + Sticky mode on
                     || i == 2 && !config.stickySidebar && !config.anchored // 'Sidebar' item + no mode on
                     || i == 3 && config.anchored) // 'Bottom' item + Anchor mode on
                            pinMenuItems[i].append(pinMenuIcons[pinMenuIcons.length -1]) // append right checkmark
                    pinMenuItems[i].onclick = menus.pin.clickHandler
                    pinMenuUL.append(pinMenuItems[i])
                }
                pinMenu.append(pinMenuUL)

                // Add listeners
                pinMenu.onmouseover = pinMenu.onmouseout = menus.pin.toggle
            },

            toggle(event) { // visibility
                const pinMenu = document.getElementById('pin-menu') || menus.pin.createAppend()
                if (!menus.pin.topPos)
                     menus.pin.topPos = ( event.clientY || event.touches?.[0]?.clientY ) < 195 ? 58 : -73
                if (!menus.pin.rightPos)
                     menus.pin.rightPos = appDiv.getBoundingClientRect().right - event.clientX - pinMenu.offsetWidth/2
                pinMenu.style.top = `${menus.pin.topPos}px` ; pinMenu.style.right = `${menus.pin.rightPos}px`
                clearTimeout(menus.pin.hideTimeout)
                if (event.type == 'mouseover') pinMenu.style.opacity = 1
                else menus.pin.hideTimeout = setTimeout(() => pinMenu.remove(), 55) // delay to cover gap
            }
        }
    }

    // Define ICON functions

    const icons = {

        anchor: {
            create() {
                const svg = create.svgElem('svg', { width: 19, height: 19, viewBox: '0 0 24 24' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M12,2 C13.6568542,2 15,3.34314575 15,5 C15,6.30588222 14.1656226,7.41688515 13.0009007,7.82897577 L13.0008722,19.9379974 C15.8984799,19.5763478 18.3147266,17.665053 19.3940412,15.0596838 L19.417,15 L17,15 C15.9853611,15 15.6358608,13.6848035 16.4495309,13.1641077 L16.5527864,13.1055728 L20.5527864,11.1055728 C21.2176875,10.7731223 22,11.256618 22,12 C22,17.5228475 17.5228475,22 12,22 C6.4771525,22 2,17.5228475 2,12 C2,11.2957433 2.70213089,10.8247365 3.34138467,11.0597803 L3.4472136,11.1055728 L7.4472136,13.1055728 C8.35473419,13.5593331 8.07916306,14.8919819 7.11853213,14.9938221 L7,15 L4.582,15 L4.60595876,15.0596838 C5.68539551,17.6653477 8.10206662,19.5767802 11.0001109,19.9381201 L11.0000889,7.82932572 C9.8348501,7.41751442 9,6.30625206 9,5 C9,3.34314575 10.3431458,2 12,2 Z M12,4 C11.4477153,4 11,4.44771525 11,5 C11,5.55228475 11.4477153,6 12,6 C12.5522847,6 13,5.55228475 13,5 C13,4.44771525 12.5522847,4 12,4 Z' })
                svg.append(svgPath) ; return svg
            }
        },


        arrowDownRight: {
            create() {
                const svg = create.svgElem('svg', {
                    width: 18, height: 18, viewBox: '0 0 24 24', fill: 'currentColor', style: 'transform: rotate(180deg)' })
                const svgPath = create.svgElem('path', {
                    d: 'M16 10H6.83L9 7.83l1.41-1.41L9 5l-6 6 6 6 1.41-1.41L9 14.17 6.83 12H16c1.65 0 3 1.35 3 3v4h2v-4c0-2.76-2.24-5-5-5z' })
                svg.append(svgPath) ; return svg
            }
        },

        arrowsCycle: {
            create() {
                const svg = create.svgElem('svg', { id: 'arrows-cycle', width: 13, height: 13, viewBox: '197 -924 573 891' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M204-318q-22-38-33-78t-11-82q0-134 93-228t227-94h7l-64-64 56-56 160 160-160 160-56-56 64-64h-7q-100 0-170 70.5T240-478q0 26 6 51t18 49l-60 60ZM481-40 321-200l160-160 56 56-64 64h7q100 0 170-70.5T720-482q0-26-6-51t-18-49l60-60q22 38 33 78t11 82q0 134-93 228t-227 94h-7l64 64-56 56Z' })
                svg.append(svgPath) ; return svg
            }
        },

        arrowsDiagonal: {
            inwardSVGpath() { return create.svgElem('path', { stroke: 'none',
                d: 'M5 1h2v6H1V5h2.59L0 1.41 1.41 0 5 3.59zm7.41 10H15V9H9v6h2v-2.59L14.59 16 16 14.59z'
            })},

            outwardSVGpath() { return create.svgElem('path', { stroke: 'none',
                d: 'M8 6.59L6.59 8 3 4.41V7H1V1h6v2H4.41zM13 9v2.59L9.41 8 8 9.41 11.59 13H9v2h6V9z'
            })},

            create() {
                const svg = create.svgElem('svg', { id: 'arrows-diagonal-icon', width: 16, height: 16, viewBox: '0 0 16 16' })
                icons.arrowsDiagonal.update(svg) ; return svg
            },

            update(...targetIcons) {
                targetIcons = targetIcons.flat() // flatten array args nested by spread operator
                if (targetIcons.length == 0) targetIcons = document.querySelectorAll('#arrows-diagonal-icon')
                targetIcons.forEach(icon => {
                    icon.firstChild?.remove() // clear prev paths
                    icon.append(icons.arrowsDiagonal[config.expanded ? 'inwardSVGpath' : 'outwardSVGpath']())
                })
            }
        },

        arrowsDown: {
            create() {
                const svg = create.svgElem('svg', { width: 19, height: 19, viewBox: '0 0 24 24' })
                svg.append(
                    create.svgElem('path', { stroke: 'none', d: 'M18,13H6a1,1,0,0,1,0-2H18a1,1,0,0,1,0,2Z' }),
                    create.svgElem('path', { stroke: 'none',
                        d: 'M14.71,18.29a1,1,0,0,1,0,1.42l-2,2a1,1,0,0,1-1.42,0l-2-2a1,1,0,0,1,1.42-1.42l.29.3V16a1,1,0,0,1,2,0v2.59l.29-.3A1,1,0,0,1,14.71,18.29ZM11.29,8.71a1,1,0,0,0,1.42,0l2-2a1,1,0,1,0-1.42-1.42l-.29.3V3a1,1,0,0,0-2,0V5.59l-.29-.3A1,1,0,0,0,9.29,6.71Z' }))
                return svg
            }
        },

        arrowsTwistedRight: {
            create() {
                const svg = create.svgElem('svg', { width: 21, height: 21, viewBox: '-1 -1 32 32' })
                const svgPath = create.svgElem('path', { stroke: '',
                    d: 'M23.707,16.293L28.414,21l-4.707,4.707l-1.414-1.414L24.586,22H23c-2.345,0-4.496-1.702-6.702-3.753c0.498-0.458,0.984-0.92,1.46-1.374C19.624,18.6,21.393,20,23,20h1.586l-2.293-2.293L23.707,16.293zM23,11h1.586l-2.293,2.293l1.414,1.414L28.414,10l-4.707-4.707l-1.414,1.414L24.586,9H23c-2.787,0-5.299,2.397-7.957,4.936C12.434,16.425,9.736,19,7,19H4v2h3c3.537,0,6.529-2.856,9.424-5.618C18.784,13.129,21.015,11,23,11zM11.843,14.186c0.5-0.449,0.995-0.914,1.481-1.377C11.364,11.208,9.297,10,7,10H4v2h3C8.632,12,10.25,12.919,11.843,14.186z' })
                svg.append(svgPath) ; return svg
            }
        },

        arrowUp: {
            create() {
                const svg = create.svgElem('svg', {
                    width: 16, height: 16, viewBox: '4 2 16 16', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' })
                const svgPath = create.svgElem('path', {
                    stroke: '', fill: 'none', 'stroke-width': '2', linecap: 'round', 'stroke-linejoin': 'round', d: 'M7 11L12 6L17 11M12 18V7' })
                svg.append(svgPath) ; return svg
            }
        },

        bug: {
            create() {
                const svg = create.svgElem('svg', { width: 16, height: 16, viewBox: '0 0 17 17' })
                svg.append(
                    create.svgElem('path', {
                        d: 'M7 0V1.60002C7.32311 1.53443 7.65753 1.5 8 1.5C8.34247 1.5 8.67689 1.53443 9 1.60002V0H11V2.49963C11.8265 3.12041 12.4543 3.99134 12.7711 5H3.2289C3.5457 3.99134 4.17354 3.12041 5 2.49963V0H7Z' }),
                    create.svgElem('path', {
                        d: 'M0 7V9H3V10.4957L0.225279 11.2885L0.774721 13.2115L3.23189 12.5095C3.87194 14.5331 5.76467 16 8 16C10.2353 16 12.1281 14.5331 12.7681 12.5095L15.2253 13.2115L15.7747 11.2885L13 10.4957V9H16V7H9V12H7V7H0Z' }))
                return svg
            }
        },

        braveGPT: {
            create() {
                const icon = document.createElement('img')
                icon.src = GM_getResourceText('bgptIcon')
                return icon
            }
        },

        caretsInward: {
            create() {
                const svg = create.svgElem('svg', { width: 17, height: 17, viewBox: '0 0 24 24' })
                const svgPath = create.svgElem('path', { stroke: '',
                    d: 'M11.29,9.71a1,1,0,0,0,1.42,0l5-5a1,1,0,1,0-1.42-1.42L12,7.59,7.71,3.29A1,1,0,0,0,6.29,4.71Zm1.42,4.58a1,1,0,0,0-1.42,0l-5,5a1,1,0,0,0,1.42,1.42L12,16.41l4.29,4.3a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42Z' })
                svg.append(svgPath) ; return svg
            }
        },

        checkmark: {
            create() {
                const svg = create.svgElem('svg', { id: 'checkmark-icon', width: 10, height: 10, viewBox: '0 0 20 20' }),
                      svgPath = create.svgElem('path', { stroke: 'none', d: 'M0 11l2-2 5 5L18 3l2 2L7 18z' })
                svg.append(svgPath) ; return svg
            }
        },

        checkmarkDouble: {
            create() {
                const svg = create.svgElem('svg', { width: 17, height: 17, viewBox: '0 0 24 24' })
                svg.append(
                    create.svgElem('path', { stroke: 'none', d: 'M23.228 8.01785C23.6186 7.62741 23.6187 6.99424 23.2283 6.60363L22.5213 5.89638C22.1309 5.50577 21.4977 5.50563 21.1071 5.89607L10.0862 16.9122C9.69563 17.3027 9.6955 17.9359 10.0859 18.3265L10.7929 19.0337C11.1833 19.4243 11.8165 19.4245 12.2071 19.034L23.228 8.01785Z' }),
                    create.svgElem('path', { stroke: 'none', d: 'M17.2285 8.01777C17.619 7.62724 17.619 6.99408 17.2285 6.60356L16.5214 5.89645C16.1309 5.50592 15.4977 5.50592 15.1072 5.89645L5.54542 15.4582L2.76773 12.6805C2.37721 12.29 1.74404 12.29 1.35352 12.6805L0.646409 13.3876C0.255884 13.7782 0.255885 14.4113 0.646409 14.8019L4.83831 18.9938C5.22883 19.3843 5.862 19.3843 6.25252 18.9938L17.2285 8.01777Z' })
                )
                return svg
            }
        },

        chevronDown: {
            create() {
                const svg = create.svgElem('svg', { width: 20, height: 20, viewBox: '0 0 16 16' }),
                      svgPath = create.svgElem('path', { stroke: 'none', d: 'M1 5l7 4.61L15 5v2.39L8 12 1 7.39z' })
                svg.append(svgPath) ; return svg
            }
        },

        chevronUp: {
            create() {
                const svg = create.svgElem('svg', { width: 20, height: 20, viewBox: '0 0 16 16' }),
                      svgPath = create.svgElem('path', { stroke: 'none', d: 'M15 11L8 6.39 1 11V8.61L8 4l7 4.61z' })
                svg.append(svgPath) ; return svg
            }
        },

        copy: {
            create() {
                const svg = create.svgElem('svg', { width: 18, height: 18, viewBox: '0 0 1024 1024' })
                svg.append(
                    create.svgElem('path', { stroke: 'none', d: 'M768 832a128 128 0 0 1-128 128H192A128 128 0 0 1 64 832V384a128 128 0 0 1 128-128v64a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64h64z' }),
                    create.svgElem('path', { stroke: 'none', d: 'M384 128a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64H384zm0-64h448a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H384a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64z' }))
                return svg
            }
        },

        fontSize: {
            create() {
                const svg = create.svgElem('svg', { width: 17, height: 17, viewBox: '0 0 512 512' })
                svg.append(
                    create.svgElem('path', { stroke: 'none', d: 'M234.997 448.199h-55.373a6.734 6.734 0 0 1-6.556-5.194l-11.435-48.682a6.734 6.734 0 0 0-6.556-5.194H86.063a6.734 6.734 0 0 0-6.556 5.194l-11.435 48.682a6.734 6.734 0 0 1-6.556 5.194H7.74c-4.519 0-7.755-4.363-6.445-8.687l79.173-261.269a6.734 6.734 0 0 1 6.445-4.781h69.29c2.97 0 5.59 1.946 6.447 4.79l78.795 261.269c1.303 4.322-1.933 8.678-6.448 8.678zm-88.044-114.93l-19.983-84.371c-1.639-6.921-11.493-6.905-13.111.02l-19.705 84.371c-.987 4.224 2.22 8.266 6.558 8.266H140.4c4.346 0 7.555-4.056 6.553-8.286z' }),
                    create.svgElem('path', { stroke: 'none', d: 'M502.572 448.199h-77.475a9.423 9.423 0 0 1-9.173-7.268l-16-68.114a9.423 9.423 0 0 0-9.173-7.268H294.19a9.423 9.423 0 0 0-9.173 7.268l-16 68.114a9.423 9.423 0 0 1-9.173 7.268h-75.241c-6.322 0-10.851-6.104-9.017-12.155L286.362 70.491a9.422 9.422 0 0 1 9.017-6.69h96.947a9.422 9.422 0 0 1 9.021 6.702l110.245 365.554c1.825 6.047-2.703 12.142-9.02 12.142zM379.385 287.395l-27.959-118.047c-2.293-9.683-16.081-9.661-18.344.029l-27.57 118.047c-1.38 5.91 3.106 11.565 9.175 11.565h55.529c6.082-.001 10.571-5.676 9.169-11.594z' })
                )
                return svg
            }
        },

        languageChars: {
            create() {
                const svg = create.svgElem('svg', { width: 15, height: 15, viewBox: '0 -960 960 960' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'm459-48 188-526h125L960-48H847l-35-100H603L568-48H459ZM130-169l-75-75 196-196q-42-45-78-101t-55-105h117q17 32 40.5 67.5T325-514q35-37 70-93t64-119H0v-106h290v-80h106v80h290v106H572q-23 74-70 152T399-438l82 85-39 111-118-121-194 194Zm508-79h139l-69-197-70 197Z' })
                svg.append(svgPath) ; return svg
            }
        },

        moon: {
            create() {
                const svg = create.svgElem('svg', { width: 17, height: 17, viewBox: '0 0 24 24' })
                const svgPath = create.svgElem('path', { fill: 'none', stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
                    d: 'M3.32031 11.6835C3.32031 16.6541 7.34975 20.6835 12.3203 20.6835C16.1075 20.6835 19.3483 18.3443 20.6768 15.032C19.6402 15.4486 18.5059 15.6834 17.3203 15.6834C12.3497 15.6834 8.32031 11.654 8.32031 6.68342C8.32031 5.50338 8.55165 4.36259 8.96453 3.32996C5.65605 4.66028 3.32031 7.89912 3.32031 11.6835Z' })
                svg.append(svgPath) ; return svg
            }
        },

        pin: {
            create() {
                const svg = create.svgElem('svg', { id: 'pin-icon', width: 17, height: 17, viewBox: '0 0 16 16' })
                const svgPath = create.svgElem('path', { stroke: '',
                    d: 'M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a5.927 5.927 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707-.195-.195.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a5.922 5.922 0 0 1 1.013.16l3.134-3.133a2.772 2.772 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146z' })
                svg.append(svgPath) ; return svg
            }
        },

        questionMark: {
            create() {
                const svg = create.svgElem('svg', { width: 18, height: 18, viewBox: '0 -960 960 960' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M428-383q0-71 16-111t63-74q47-35 58.5-55.5T577-683q0-35-25-57.5T488-763q-26 0-61 18t-50 70l-114-47q27-82 90.5-122.5T488-885q93 0 151.5 59.5T698-682q0 55-17 95t-70 83q-37 29-48.5 55T550-383H428Zm60 265q-41 0-69.5-28.5T390-216q0-41 28.5-69.5T488-314q41 0 69.5 28.5T586-216q0 41-28.5 69.5T488-118Z' })
                svg.append(svgPath) ; return svg
            }
        },

        questionMarkCircle: {
            create() {
                const svg = create.svgElem('svg', { width: 17, height: 17, viewBox: '0 0 56.693 56.693' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M28.765,4.774c-13.562,0-24.594,11.031-24.594,24.594c0,13.561,11.031,24.594,24.594,24.594  c13.561,0,24.594-11.033,24.594-24.594C53.358,15.805,42.325,4.774,28.765,4.774z M31.765,42.913c0,0.699-0.302,1.334-0.896,1.885  c-0.587,0.545-1.373,0.82-2.337,0.82c-0.993,0-1.812-0.273-2.431-0.814c-0.634-0.551-0.954-1.188-0.954-1.891v-1.209  c0-0.703,0.322-1.34,0.954-1.891c0.619-0.539,1.438-0.812,2.431-0.812c0.964,0,1.75,0.277,2.337,0.82  c0.594,0.551,0.896,1.186,0.896,1.883V42.913z M38.427,24.799c-0.389,0.762-0.886,1.432-1.478,1.994  c-0.581,0.549-1.215,1.044-1.887,1.473c-0.643,0.408-1.248,0.852-1.798,1.315c-0.539,0.455-0.99,0.963-1.343,1.512  c-0.336,0.523-0.507,1.178-0.507,1.943v0.76c0,0.504-0.247,1.031-0.735,1.572c-0.494,0.545-1.155,0.838-1.961,0.871l-0.167,0.004  c-0.818,0-1.484-0.234-1.98-0.699c-0.532-0.496-0.801-1.055-0.801-1.658c0-1.41,0.196-2.611,0.584-3.572  c0.385-0.953,0.86-1.78,1.416-2.459c0.554-0.678,1.178-1.27,1.854-1.762c0.646-0.467,1.242-0.93,1.773-1.371  c0.513-0.428,0.954-0.885,1.312-1.354c0.328-0.435,0.489-0.962,0.489-1.608c0-1.066-0.289-1.83-0.887-2.334  c-0.604-0.512-1.442-0.771-2.487-0.771c-0.696,0-1.294,0.043-1.776,0.129c-0.471,0.083-0.905,0.223-1.294,0.417  c-0.384,0.19-0.745,0.456-1.075,0.786c-0.346,0.346-0.71,0.783-1.084,1.301c-0.336,0.473-0.835,0.83-1.48,1.062  c-0.662,0.239-1.397,0.175-2.164-0.192c-0.689-0.344-1.11-0.793-1.254-1.338c-0.135-0.5-0.135-1.025-0.002-1.557  c0.098-0.453,0.369-1.012,0.83-1.695c0.451-0.67,1.094-1.321,1.912-1.938c0.814-0.614,1.847-1.151,3.064-1.593  c1.227-0.443,2.695-0.668,4.367-0.668c1.648,0,3.078,0.249,4.248,0.742c1.176,0.496,2.137,1.157,2.854,1.967  c0.715,0.809,1.242,1.738,1.568,2.762c0.322,1.014,0.486,2.072,0.486,3.146C39.024,23.075,38.823,24.024,38.427,24.799z' })
                svg.append(svgPath) ; return svg
            }
        },

        scheme: {
            create() {
                const svg = create.svgElem('svg', { width: 15, height: 15, viewBox: '0 -960 960 960' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M479.92-34q-91.56 0-173.4-35.02t-142.16-95.34q-60.32-60.32-95.34-142.24Q34-388.53 34-480.08q0-91.56 35.02-173.4t95.34-142.16q60.32-60.32 142.24-95.34Q388.53-926 480.08-926q91.56 0 173.4 35.02t142.16 95.34q60.32 60.32 95.34 142.24Q926-571.47 926-479.92q0 91.56-35.02 173.4t-95.34 142.16q-60.32 60.32-142.24 95.34Q571.47-34 479.92-34ZM530-174q113-19 186.5-102.78T790-480q0-116.71-73.5-201.35Q643-766 530-785v611Z' })
                svg.append(svgPath) ; return svg
            }
        },

        sidebar: {
            create() {
                const svg = create.svgElem('svg', { width: 15, height: 15, viewBox: '0 -975 900 1000' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M800-160q33 0 56.5-23.5T880-240v-480q0-33-23.5-56.5T800-800H160q-33 0-56.5 23.5T80-720v480q0 33 23.5 56.5T160-160h640Zm-240-80H160v-480h400v480Zm80 0v-480H800v480H640Zm160 0v-480 480Zm-160 0h-80 80Zm0-480h-80 80Z' })
                svg.append(svgPath) ; return svg
            }
        },

        signalStream: {
            create() {
                const svg = create.svgElem('svg', { width: 16, height: 16, viewBox: '0 0 32 32' })
                const svgPath = create.svgElem('path', { stroke: '', 'stroke-width': 0.5,
                    d: 'M16 11.75c-2.347 0-4.25 1.903-4.25 4.25s1.903 4.25 4.25 4.25c2.347 0 4.25-1.903 4.25-4.25v0c-0.003-2.346-1.904-4.247-4.25-4.25h-0zM16 17.75c-0.966 0-1.75-0.784-1.75-1.75s0.784-1.75 1.75-1.75c0.966 0 1.75 0.784 1.75 1.75v0c-0.001 0.966-0.784 1.749-1.75 1.75h-0zM3.25 16c0.211-3.416 1.61-6.471 3.784-8.789l-0.007 0.008c0.223-0.226 0.361-0.536 0.361-0.879 0-0.69-0.56-1.25-1.25-1.25-0.344 0-0.655 0.139-0.881 0.363l0-0c-2.629 2.757-4.31 6.438-4.506 10.509l-0.001 0.038c0.198 4.109 1.879 7.79 4.514 10.553l-0.006-0.006c0.226 0.228 0.54 0.369 0.886 0.369 0.69 0 1.249-0.559 1.249-1.249 0-0.346-0.141-0.659-0.368-0.885l-0-0c-2.173-2.307-3.573-5.363-3.774-8.743l-0.002-0.038zM9.363 16c0.149-2.342 1.109-4.436 2.6-6.026l-0.005 0.005c0.224-0.226 0.363-0.537 0.363-0.88 0-0.69-0.56-1.25-1.25-1.25-0.345 0-0.657 0.139-0.883 0.365l0-0c-1.94 2.035-3.179 4.753-3.323 7.759l-0.001 0.028c0.145 3.032 1.384 5.75 3.329 7.79l-0.005-0.005c0.226 0.228 0.54 0.369 0.886 0.369 0.69 0 1.249-0.559 1.249-1.249 0-0.346-0.141-0.659-0.368-0.885l-0-0c-1.49-1.581-2.451-3.676-2.591-5.993l-0.001-0.027zM26.744 5.453c-0.226-0.227-0.54-0.368-0.886-0.368-0.691 0-1.251 0.56-1.251 1.251 0 0.345 0.139 0.657 0.365 0.883l-0-0c2.168 2.31 3.567 5.365 3.775 8.741l0.002 0.040c-0.21 3.417-1.609 6.471-3.784 8.789l0.007-0.008c-0.224 0.226-0.362 0.537-0.362 0.88 0 0.691 0.56 1.251 1.251 1.251 0.345 0 0.657-0.14 0.883-0.365l-0 0c2.628-2.757 4.308-6.439 4.504-10.509l0.001-0.038c-0.198-4.108-1.878-7.79-4.512-10.553l0.006 0.007zM21.811 8.214c-0.226-0.224-0.537-0.363-0.881-0.363-0.69 0-1.25 0.56-1.25 1.25 0 0.343 0.138 0.653 0.361 0.879l-0-0c1.486 1.585 2.447 3.678 2.594 5.992l0.001 0.028c-0.151 2.343-1.111 4.436-2.601 6.027l0.005-0.005c-0.224 0.226-0.362 0.537-0.362 0.88 0 0.691 0.56 1.251 1.251 1.251 0.345 0 0.657-0.14 0.883-0.365l-0 0c1.939-2.036 3.178-4.754 3.323-7.759l0.001-0.028c-0.145-3.033-1.385-5.751-3.331-7.791l0.005 0.005z' })
                svg.append(svgPath) ; return svg
            }
        },

        slash: {
            create() {
                const svg = create.svgElem('svg', { width: 15, height: 15, viewBox: '0 0 15 15' }),
                      svgPath = create.svgElem('path', { stroke: '', d: 'M4.10876 14L9.46582 1H10.8178L5.46074 14H4.10876Z' })
                svg.append(svgPath) ; return svg
            }
        },

        sliders: {
            create() {
                const svg = create.svgElem('svg', { width: 19, height: 19, viewBox: '0 0 25 25' })

                // Top track
                svg.append(create.svgElem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
                    x1: 2, y1: 5.5, x2: 12, y2: 5.5 }))
                svg.append(create.svgElem('circle', { stroke: '', fill: 'none', 'stroke-width': 1.5,
                    cx: 18, cy: 5.5, r: 3 }))
                svg.append(create.svgElem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
                    x1: 21, y1: 5.5, x2: 23, y2: 5.5 }))

                // Middle track
                svg.append(create.svgElem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
                    x1: 2, y1: 12.5, x2: 4, y2: 12.5 }))
                svg.append(create.svgElem('circle', { stroke: '', fill: 'none', 'stroke-width': 1.5,
                    cx: 8, cy: 12.5, r: 3 }))
                svg.append(create.svgElem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
                    x1: 14, y1: 12.5, x2: 23, y2: 12.5 }))

                // Bottom track
                svg.append(create.svgElem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
                    x1: 2, y1: 19.5, x2: 8, y2: 19.5 }))
                svg.append(create.svgElem('circle', { stroke: '', fill: 'none', 'stroke-width': 1.5,
                    cx: 14.5, cy: 19.5, r: 3 }))
                svg.append(create.svgElem('line', { stroke: '', 'stroke-width': 2, 'stroke-linecap': 'round',
                    x1: 17, y1: 19.5, x2: 23, y2: 19.5 }))

                return svg
            }
        },

        sparkles: {
            create(style) { // style = ( 'fg' ? filled front sparkle : 'bg' ? filled rear sparkles )
                const svg = create.svgElem('svg', { width: 18, height: 18, viewBox: '0 0 512 512' })
                svg.append(create.svgElem('path', { // large front sparkle
                    fill: style == 'bg' ? 'none' : '', stroke: '', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': 32,
                    d: 'M259.92,262.91,216.4,149.77a9,9,0,0,0-16.8,0L156.08,262.91a9,9,0,0,1-5.17,5.17L37.77,311.6a9,9,0,0,0,0,16.8l113.14,43.52a9,9,0,0,1,5.17,5.17L199.6,490.23a9,9,0,0,0,16.8,0l43.52-113.14a9,9,0,0,1,5.17-5.17L378.23,328.4a9,9,0,0,0,0-16.8L265.09,268.08A9,9,0,0,1,259.92,262.91Z' }))
                svg.append(create.svgElem('polygon', { // small(est) rear-left sparkle
                    fill: style == 'fg' ? 'none' : '', stroke: '', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': 24,
                    points: '108 68 88 16 68 68 16 88 68 108 88 160 108 108 160 88 108 68' }))
                svg.append(create.svgElem('polygon', { // small rear-right sparkle
                    fill: style == 'fg' ? 'none' : '', stroke: '', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': 32,
                    points: '426.67 117.33 400 48 373.33 117.33 304 144 373.33 170.67 400 240 426.67 170.67 496 144 426.67 117.33' }))
                return svg
            }
        },

        speaker: {
            create() {
                const svg = create.svgElem('svg', { width: 22, height: 22, viewBox: '0 0 32 32' })
                svg.append(
                    create.svgElem('path', { stroke: '', 'stroke-width': '2px', fill: 'none',
                        d: 'M24.5,26c2.881,-2.652 4.5,-6.249 4.5,-10c0,-3.751 -1.619,-7.348 -4.5,-10' }),
                    create.svgElem('path', { stroke: '', 'stroke-width': '2px', fill: 'none',
                        d: 'M22,20.847c1.281,-1.306 2,-3.077 2,-4.924c0,-1.846 -0.719,-3.617 -2,-4.923' }),
                    create.svgElem('path', { stroke: 'none', fill: '',
                        d: 'M9.957,10.88c-0.605,0.625 -1.415,0.98 -2.262,0.991c-4.695,0.022 -4.695,0.322 -4.695,4.129c0,3.806 0,4.105 4.695,4.129c0.846,0.011 1.656,0.366 2.261,0.991c1.045,1.078 2.766,2.856 4.245,4.384c0.474,0.49 1.18,0.631 1.791,0.36c0.611,-0.272 1.008,-0.904 1.008,-1.604c0,-4.585 0,-11.936 0,-16.52c0,-0.7 -0.397,-1.332 -1.008,-1.604c-0.611,-0.271 -1.317,-0.13 -1.791,0.36c-1.479,1.528 -3.2,3.306 -4.244,4.384Z' })
                )
                return svg
            }
        },

        speechBalloonLasso: {
            create() {
                const svg = create.svgElem('svg', { width: 17, height: 17, viewBox: '0 -960 960 960' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M323-41v-247h-10q-105 0-172.5-67T73-528q0-105 74-179t179-74h36l-44-44 69-69 162 162-162 162-69-69 44-44h-36q-64 0-109.5 45.5T171-528q0 64 45.5 109.5T326-373h95v96l96-96h117q64 0 109.5-45.5T789-528q0-64-45.5-109.5T634-683h10v-98h-10q105 0 179 74t74 179q0 105-74 179t-179 74h-77L323-41Z' })
                svg.append(svgPath) ; return svg
            }
        },

        speechBalloons: {
            create() {
                const svg = create.svgElem('svg', { width: 16, height: 16, viewBox: '0 -960 960 960' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M350-212q-32.55 0-55.27-22.73Q272-257.45 272-290v-64h492v-342h63.67q33.33 0 55.83 22.72Q906-650.55 906-618v576L736-212H350ZM54-256v-582.4q0-32.38 22.72-54.99Q99.45-916 132-916h482q32.55 0 55.28 22.72Q692-870.55 692-838v334q0 32.55-22.72 55.27Q646.55-426 614-426H224L54-256Zm540-268v-294H152v294h442Zm-442 0v-294 294Z' })
                svg.append(svgPath) ; return svg
            }
        },

        sun: {
            create() {
                const svg = create.svgElem('svg', { width: 17, height: 17, viewBox: '0 -960 960 960' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M440-760v-160h80v160h-80Zm266 110-55-55 112-115 56 57-113 113Zm54 210v-80h160v80H760ZM440-40v-160h80v160h-80ZM254-652 140-763l57-56 113 113-56 54Zm508 512L651-255l54-54 114 110-57 59ZM40-440v-80h160v80H40Zm157 300-56-57 112-112 29 27 29 28-114 114Zm283-100q-100 0-170-70t-70-170q0-100 70-170t170-70q100 0 170 70t70 170q0 100-70 170t-170 70Zm0-80q66 0 113-47t47-113q0-66-47-113t-113-47q-66 0-113 47t-47 113q0 66 47 113t113 47Zm0-160Z' })
                svg.append(svgPath) ; return svg
            }
        },

        sunglasses: {
            create() {
                const svg = create.svgElem('svg', { width: 17, height: 17, viewBox: '0 0 512 512' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M507.44,185.327c-4.029-5.124-10.185-8.112-16.704-8.112c0,0-48.021,0-156.827,0h-65.774H243.87h-65.774c-108.806,0-156.827,0-156.827,0c-6.519,0-12.675,2.988-16.714,8.112c-4.028,5.125-5.486,11.815-3.965,18.152c0,0,12.421,56.269,19.927,82.534c7.506,26.265,26.265,48.772,86.29,48.772s59.827,0,74.828,0c21.258,0,46.256-19.99,55.028-45.023c4.97-14.16,12.756-32.738,19.338-47.876c6.582,15.138,14.368,33.716,19.338,47.876c8.773,25.033,33.77,45.023,55.028,45.023c15.001,0,14.803,0,74.828,0s78.784-22.507,86.29-48.772c7.496-26.264,19.918-82.534,19.918-82.534C512.935,197.142,511.478,190.452,507.44,185.327z M90.339,278.734C45.314,263.732,40.318,198.7,40.318,198.7s22.507,0,55.028,0L90.339,278.734z M340.464,278.734c-45.015-15.001-50.022-80.034-50.022-80.034s22.508,0,55.029,0L340.464,278.734z' })
                svg.append(svgPath) ; return svg
            }
        },

        webCorner: {
            create() {
                const svg = create.svgElem('svg', { width: 18, height: 18, viewBox: '0 0 32 32' })
                const svgPath = create.svgElem('path', { stroke: 'none',
                    d: 'M29.9,2.6c-0.1-0.2-0.3-0.4-0.5-0.5C29.3,2,29.1,2,29,2H3C2.4,2,2,2.4,2,3s0.4,1,1,1h2c5,0,9,4,9,9c0,1.9-0.6,3.8-1.8,5.4l-4.9,4.9c-0.4,0.4-0.4,1,0,1.4C7.5,24.9,7.7,25,8,25s0.5-0.1,0.7-0.3l4.9-4.9c1.6-1.2,3.4-1.8,5.4-1.8c5,0,9,4,9,9v2    c0,0.6,0.4,1,1,1s1-0.4,1-1V3C30,2.9,30,2.7,29.9,2.6zM26.6,4l-4.8,4.8c0-1.9-0.8-3.5-2-4.8H26.6z M11.3,4H15c2.7,0,4.8,2.2,4.8,4.8c0,1-0.3,2-0.9,2.9l-3,3C16,14.2,16,13.6,16,13C16,9.3,14.1,6,11.3,4z M19,16c-0.6,0-1.2,0-1.7,0.1l3-3c0.8-0.6,1.8-0.9,2.9-0.9c2.7,0,4.8,2.2,4.8,4.8v3.7C26,17.9,22.7,16,19,16z M23.2,10.2L28,5.4v6.8C26.8,11,25.1,10.2,23.2,10.2z' })
                svg.append(svgPath) ; return svg
            }
        },

        widescreen: {
            wideSVGpath() { return create.svgElem('path', {
                stroke: '', fill: '', 'fill-rule': 'evenodd', d: 'm26,13 0,10 -16,0 0,-10 z m-14,2 12,0 0,6 -12,0 0,-6 z'
            })},

            tallSVGpath() { return create.svgElem('path', {
                stroke: '', fill: '', 'fill-rule': 'evenodd', d: 'm28,11 0,14 -20,0 0,-14 z m-18,2 16,0 0,10 -16,0 0,-10 z'
            })},

            create() {
                const svg = create.svgElem('svg', { id: 'widescreen-icon', width: 18, height: 18, viewBox: '8 8 20 20' })
                icons.widescreen.update(svg)
                return svg
            },

            update(...targetIcons) {
                targetIcons = targetIcons.flat() // flatten array args nested by spread operator
                if (targetIcons.length == 0)
                    targetIcons = document.querySelectorAll('#widescreen-icon:not(.chatgpt-notif *)')
                targetIcons.forEach(icon => {
                    icon.firstChild?.remove() // clear prev paths
                    icon.append(icons.widescreen[config.widerSidebar ? 'wideSVGpath' : 'tallSVGpath']())
                })
            }
        },

        x: {
            create() {
                const svg = create.svgElem('svg', { height: 10, viewBox: '0 0 14 14', fill: 'none' })
                const svgPath = create.svgElem('path', {
                    d: 'M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.0976312 12.6834 -0.0976312 12.2929 0.292893L7 5.58579L1.70711 0.292893C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292893C-0.0976312 0.683417 -0.0976312 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976312 12.6834 -0.0976312 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z' })
                svg.append(svgPath) ; return svg
            }
        }
    }

    // Define LOGO functions

    const logos = {
        braveGPT: {

            create() {
                const braveGPTlogo = document.createElement('img')
                braveGPTlogo.id = 'bravegpt-logo' ; braveGPTlogo.className = 'no-mobile-tap-outline'
                logos.braveGPT.update(braveGPTlogo)
                return braveGPTlogo
            },

            update(...targetLogos) {
                targetLogos = targetLogos.flat() // flatten array args nested by spread operator
                if (targetLogos.length == 0) targetLogos = document.querySelectorAll('#bravegpt-logo')
                targetLogos.forEach(logo => logo.src = GM_getResourceText(`bgpt${ ui.app.scheme == 'dark' ? 'DS' : 'LS' }logo`))
            }
        }
    }

    // Define UPDATE functions

    const update = {

        appBottomPos() { appDiv.style.bottom = `${ config.minimized ? 56 - appDiv.offsetHeight : -32 }px` },

        footerContent() {
            get.json('https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/index.json',
                (err, advertisersData) => { if (err) return

                    // Init vars
                    let chosenAdvertiser, adSelected
                    const re_appName = 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
                    })()

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

                    // Fetch a random, active creative
                    if (chosenAdvertiser) {
                        const campaignsURL = 'https://cdn.jsdelivr.net/gh/KudoAI/ads-library/advertisers/'
                                           + chosenAdvertiser + '/text/campaigns.json'
                        get.json(campaignsURL, (err, campaignsData) => { if (err) return

                            // 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$/i.test(groupName) && !re_appName.test(campaignName) // self-group for other apps
                                        || re_appName.test(campaignName) && !/^self$/i.test(groupName) // non-self group for this app
                                        || 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...
                                            !config.userLocale || !adGroup.targetLocations.some( // ...but user locale is missing or excluded
                                                loc => loc.includes(config.userLocale) || config.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 == 0) continue // to next group since no ads active
                                    const chosenAd = activeAds[Math.floor(chatgpt.randomFloat() * activeAds.length)] // random active one

                                    // 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 ? create.anchor(destinationURL)
                                                                            : document.createElement('span')
                                    footerContent.replaceWith(newFooterContent) ; footerContent = newFooterContent
                                    footerContent.classList.add('feedback', 'svelte-8js1iq') // Brave classes
                                    footerContent.textContent = chosenAd.text.length < 49 ? chosenAd.text
                                                              : chosenAd.text.slice(0, 49) + '...'
                                    footerContent.setAttribute('title', chosenAd.tooltip ||
                                        footerContent.textContent.includes('...') ? chosenAd.text : '')
                                    adSelected = true ; break
                                }
                                if (adSelected) break // out of campaign loop after ad selection
            }})}})

            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,
                              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 = appDiv.querySelector('pre p')
            if (!firstP) return
            const prefixNeeded = ui.app.scheme == 'dark' && !config.bgAnimationsDisabled,
                  prefixExists = firstP.textContent.startsWith('>> ')
            if (prefixNeeded && !prefixExists) firstP.prepend('>> ')
            else if (!prefixNeeded && prefixExists) firstP.textContent = firstP.textContent.replace(/^>> /, '')
        },

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

        scheme(newScheme) {
            log.caller = `update.scheme('${newScheme}')`
            log.debug(`Updating ${app.name} scheme to ${log.toTitleCase(newScheme)}...`)
            ui.app.scheme = newScheme ; logos.braveGPT.update() ; update.style.app()
            update.stars() ; update.replyPrefix() ; toggle.btnGlow() ; modals.settings.updateSchemeStatus()
            log.debug(`Success! ${app.name} updated to ${log.toTitleCase(newScheme)} scheme`)
        },

        stars() {
            ['sm', 'med', 'lg'].forEach(size =>
                document.querySelectorAll(`[id*="stars-${size}"]`).forEach(starsDiv =>
                    starsDiv.id = config.bgAnimationsDisabled ? `stars-${size}-off`
                    : `${ ui.app.scheme == 'dark' ? 'white' : 'black' }-stars-${size}`
            ))
        },

        style: {
            app() {
                const isStarryDM = ui.app.scheme == 'dark' && !config.bgAnimationsDisabled
                appStyle.innerText = (
                    ':root {' // color vars
                      + '--app-bg-color-light-scheme: #ffffff ; --app-bg-color-dark-scheme: #282828 ;'
                      + '--pre-bg-color-light-scheme: #e7e7e7cf ; --pre-bg-color-dark-scheme: #3a3a3a ;'
                      + '--font-color-light-scheme: #282828 ; --font-color-dark-scheme: #f2f2f2 }'
                  + '@keyframes modal-zoom-fade-out {'
                      + '0% { opacity: 1 } 50% { opacity: 0.25 ; transform: scale(1.05) }'
                      + '100% { opacity: 0 ; transform: scale(1.35) }}'
                  + '@keyframes btn-zoom-fade-out {'
                      + '0% { opacity: 1 } 50% { opacity: 0.65 ; transform: scale(1.85) }'
                      + '75% { opacity: 0.05 ; transform: scale(3.15) } 100% { opacity: 0 ; transform: scale(5.85) }}'
                  + '.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
                        '#bravegpt *::-webkit-scrollbar { width: 7px }'
                      + '#bravegpt *::-webkit-scrollbar-thumb { background: #cdcdcd }'
                      + '#bravegpt *::-webkit-scrollbar-thumb:hover { background: #a6a6a6 }'
                      + '#bravegpt *::-webkit-scrollbar-track { background: none }' )
                  + '#bravegpt * { 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 }'
                  + '#bravegpt {'
                      + 'z-index: 5555 ; word-wrap: break-word ; white-space: pre-wrap ;'
                      + 'border: 1px solid var(--color-divider-subtle) ; border-radius: 18px ;'
                      + `margin: ${ env.browser.isMobile ? '0 8px 16px' : '0 0 20px' } ; padding: 24px 23px 45px 23px ;`
                      + ( config.bgAnimationsDisabled ? // classic flat bg
                            `background: var(--app-bg-color-${ui.app.scheme}-scheme) ; color: var(--font-color-${ui.app.scheme}-scheme) ;`
                      : `background-image: linear-gradient(180deg, ${ // gradient bg to match stars
                              ui.app.scheme == 'dark' ? '#99a8a6 -245px, black 185px' : '#b6ebff -163px, white 65px' }) ;`
                      + ( ui.app.scheme == 'dark' ? 'border: none ;' : '' ))
                      + ( !config.fgAnimationsDisabled ?
                            'transition: bottom 0.1s cubic-bezier(0, 0, 0.2, 1),' // smoothen Anchor vertical minimize/restore
                                      + 'width 0.167s cubic-bezier(0, 0, 0.2, 1),' // smoothen Anchor horizontal expand/shrink
                                      + 'opacity 0.5s ease, transform 0.5s ease ;' : '' ) + '}' // smoothen 1st app fade-in
                  + '#bravegpt:hover { box-shadow: 0 9px 28px rgba(0, 0, 0, 0.09) ; transition: box-shadow 0.15s ease }'
                  + `#bravegpt p { margin: 0 ${ ui.app.scheme == 'dark' ? '; color: #ccc' : '' }}`
                  + `#bravegpt .alert-link { color: ${ ui.app.scheme == 'light' ? '#190cb0' : 'white ; text-decoration: underline' }}`
                  + '.app-name {'
                      + 'font-size: 20px ; font-family: var(--brand-font) ; text-decoration: none ;'
                      + `color: ${ ui.app.scheme == 'dark' ? 'white' : 'black' } !important }`
                  + '.kudoai { margin-left: 7px ; font-size: .65rem ; color: #aaa }'
                  + '.kudoai a { color: #aaa ; text-decoration: none !important }'
                  + `.kudoai a:hover { color: ${ ui.app.scheme == 'dark' ? 'white' : 'black' }}`
                  + '#corner-btns { float: right }'
                  + '.corner-btn {'
                      + 'float: right ; cursor: pointer ; position: relative ; top: 4px ;'
                      + `${ ui.app.scheme == 'dark' ? 'fill: white ; stroke: white' : 'fill: #adadad ; stroke: #adadad' };` // color
                      + 'transition: transform 0.15s ease,' // for hover zooms
                                  + 'opacity 0.3s ease-in-out, visibility 0.3s ease-in-out }' // for re-appearances from btn-zoom-fade-out ends
                  + '.corner-btn:hover'
                      + `{ ${ ui.app.scheme == 'dark' ? 'fill: #d9d9d9 ; stroke: #d9d9d9' : 'fill: black ; stroke: black' } ;`
                      + `${ config.fgAnimationsDisabled || env.browser.isMobile ? '' : 'transform: scale(1.285)' }}`
                  + `.corner-btn:active { ${ ui.app.scheme == 'dark' ? 'fill: #999999 ; stroke: #999999' : 'fill: #638ed4 ; stroke: #638ed4' } }`
                  + ( config.bgAnimationsDisabled ? '' : ( '#bravegpt-logo, .corner-btn svg, .standby-btn'
                      + `{ filter: drop-shadow(${ ui.app.scheme == 'dark' ? '#7171714d 10px' : '#aaaaaa21 7px' } 7px 3px) }` ))
                  + '#bravegpt .loading {'
                      + 'margin-bottom: -55px ;' // offset vs. #bravegpt bottom-padding footer accomodation
                      + 'color: #b6b8ba ; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite }'
                  + '@keyframes pulse { 0%, to { opacity: 1 } 50% { opacity: .5 }}'
                  + '#bravegpt section.loading { padding-left: 5px ; font-size: 90% }'
                  + '#font-size-slider-track {'
                      + 'width: 98% ; height: 7px ; margin: -8px auto -9px ; padding: 15px 0 ;'
                      + 'background-color: #ccc ; box-sizing: content-box; background-clip: content-box ; -webkit-background-clip: content-box }'
                  + '#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 }'
                  + '#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 }'
                  + '#font-size-slider-thumb {'
                      + 'z-index: 2 ; width: 10px ; height: 27px ; border-radius: 30% ; position: relative ; top: -9px ;'
                      + `transition: transform 0.05s ease ; background-color: ${ ui.app.scheme == 'dark' ? 'white' : '#4a4a4a' } ;`
                      + 'box-shadow: rgba(0, 0, 0, 0.21) 1px 1px 9px 0 ; cursor: ew-resize }'
                  + ( config.fgAnimationsDisabled || env.browser.isMobile ? '' : '#font-size-slider-thumb:hover { transform: scale(1.125) }' )
                  + '.standby-btn {'
                      + 'width: 100% ; margin: 14px 0 20px ; padding: 13px 0 ; cursor: pointer ;'
                      + `background-color: #f0f0f0${ config.bgAnimationsDisabled ? '' : '00' } ; color: ${ isStarryDM ? 'white' : 'black' } ;`
                      + `border-radius: 4px ; border: 1px solid ${ isStarryDM ? '#fff' : '#888' } ; transition: transform 0.15s ease }`
                  + '.standby-btn:hover {'
                      + `${ ui.app.scheme == 'dark' ? 'background: white ; color: black' : 'background: black ; color: white' };`
                      + `${ config.fgAnimationsDisabled || env.browser.isMobile ? '' : 'transform: scaleX(1.015) scaleY(1.03)' }}`
                  + '.reply-tip {'
                      + 'content: "" ; position: relative ; border: 7px solid transparent ;'
                      + 'float: left ; left: 7px ; margin: 36px -13px 0 0 ;' // positioning
                      + 'border-bottom-style: solid ; border-bottom-width: 16px ; border-top: 0 ; border-bottom-color:'
                          + `${ isStarryDM ? '#0000' : `var(--pre-bg-color-${ui.app.scheme}-scheme)` }}`// hide reply tip for terminal aesthetic
                  + '#bravegpt > pre {'
                      + `font-size: ${config.fontSize}px ; font-family: Consolas, Menlo, Monaco, monospace ; white-space: pre-wrap ;`
                      + `line-height: ${ config.fontSize * config.lineHeightRatio }px ; overscroll-behavior: contain ;`
                      + 'margin-top: 12px ; padding: 1.2em 1.2em 0 1.2em ; border-radius: 13px ; overflow: auto ;'
                      + ( config.bgAnimationsDisabled ? // classic opaque bg
                            `background: var(--pre-bg-color-${ui.app.scheme}-scheme) ; color: var(--font-color-${ui.app.scheme}-scheme)`
                      : `${ // slightly tranluscent bg
                            ui.app.scheme == 'dark' ? 'background: #2b3a40cf ; color: var(--font-color-dark-scheme) ; border: 1px solid white'
                                                    : 'background: var(--pre-bg-color-light-scheme) ;'
                                                         + 'color: var(--font-color-light-scheme) ; border: none' } ;` )
                      + ( !config.fgAnimationsDisabled ? // smoothen Anchor mode vertical expand/shrink
                            'transition: max-height 0.167s cubic-bezier(0, 0, 0.2, 1) ;' : '' ) + '}'
                  + '#bravegpt > pre a, #bravegpt > pre a:visited { color: #4495d4 }'
                  + `#bravegpt pre a:hover { color: ${ ui.app.scheme == 'dark' ? 'white' : '#ea7a28' }}`
                  + `#bravegpt footer { margin: ${ env.browser.isFF ? 32 : 27 }px 18px -26px 0 ; border-top: none !important }`
                  + '#bravegpt .feedback {'
                      + 'float: right ; font-family: var(--brand-font) ; font-size: .55rem; color: #aaa ;'
                      + 'letter-spacing: .02em ; position: relative ; right: -18px ; bottom: 15px }'
                  + '#bravegpt .feedback .icon {'
                      + ' fill: currentColor ; color: currentColor ; --size: 12px ; position: relative ; top: 0.19em ; right: 2px }'
                  + `#bravegpt footer a:hover { color: ${ ui.app.scheme == 'dark' ? 'white' : 'black' } ; text-decoration: none }`
                  + '@keyframes pulse { 0%, to { opacity: 1 } 50% { opacity: .5 }}'
                  + '.chatgpt-js { font-family: var(--brand-font) ; font-size: .65rem ; position: relative ; right: .9rem }'
                  + '.chatgpt-js > a { color: inherit ; top: .054rem }'
                  + '.chatgpt-js > svg { top: 3px ; position: relative ; margin-right: 1px }'
                  + '#copy-btn { float: right ; cursor: pointer }'
                  + `pre > #copy-btn > svg { margin: -5px -6px 0 0 ; height: 15px ; width: 15px ; ${ ui.app.scheme == 'dark' ? 'fill: white' : '' }}`
                  + 'code #copy-btn { position: relative ; top: -6px ; right: -9px }'
                  + 'code #copy-btn > svg { height: 13px ; width: 13px ; fill: white }'
                  + '#app-chatbar {'
                      + `border: solid 1px ${ ui.app.scheme == 'dark' ? ( config.bgAnimationsDisabled ? '#777' : '#aaa' ) : '#638ed4' } ;`
                      + 'border-radius: 12px 13px 12px 0 ;'
                      + 'border-radius: 15px 16px 15px 0 ; margin: -6px 0 -7px 0 ;  padding: 12px 51px 12px 10px ;'
                      + `position: relative ; z-index: 555 ; color: ${ ui.app.scheme == 'dark' ? '#eee' : '#222' } ;`
                      + 'height: 43px ; line-height: 17px ; width: 100% ; max-height: 200px ; resize: none ;'
                      + `background: ${ ui.app.scheme == 'dark' ? `#515151${ config.bgAnimationsDisabled ? '' : '9e' }` : '#eeeeee9e' } ;`
                      + `${ ui.app.scheme == 'light' ? 'box-shadow: 0 1px 2px rgba(15,17,17,.1) inset' : '' }}`
                  + '.related-queries {'
                      + 'display: flex ; flex-wrap: wrap ; width: 100% ; margin-bottom: -28px ;'
                      + 'position: relative ; top: -3px ;' // scooch up to hug feedback gap
                      + `${ env.browser.isFF ? '' : 'margin-top: -31px' }}`
                  + '.related-query {'
                      + 'font-size: 0.77em ; cursor: pointer ;'
                      + 'box-sizing: border-box ; width: fit-content ; max-width: 100% ;' // confine to .related-queries bounds
                      + 'margin: 4px 4px 7px 0 ; padding: 8px 13px 7px 14px ;'
                      + `color: ${ ui.app.scheme == 'dark' ? ( config.bgAnimationsDisabled ? '#ccc' : '#f2f2f2' ) : '#767676' } ;`
                      + `background: ${
                             config.bgAnimationsDisabled ? ( ui.app.scheme == 'dark' ? '#404040' : '#dadada12' )
                                                         : ( ui.app.scheme == 'dark' ? '#595858d6' : '#fbfbfbb0' )} ;`
                      + `border: 1px solid ${ ui.app.scheme == 'dark' ? ( config.bgAnimationsDisabled ? '#5f5f5f' : '#777' ) : '#e1e1e1' } ;`
                      + 'border-radius: 0 13px 12px 13px ; flex: 0 0 auto ;'
                      + `box-shadow: 1px 4px ${ ui.app.scheme == 'dark' ? `${ config.bgAnimationsDisabled ? 10 : 18 }px -8px lightgray`
                                                                        : '8px -6px rgba(169, 169, 169, 0.75)' };`
                      + `${ config.fgAnimationsDisabled ? '' : 'transition: transform 0.1s ease !important' }}`
                  + '.related-query:hover, .related-query:focus {'
                      + ( config.fgAnimationsDisabled || env.browser.isMobile ? '' : 'transform: scale(1.055) !important ;' )
                      + `background: ${ ui.app.scheme == 'dark' ? '#a2a2a270': '#dae5ffa3 ; color: #000000a8 ; border-color: #a3c9ff' }}`
                  + '.related-query svg {' // related query icon
                      + 'float: left ; margin: 0.09em 6px 0 0 ;'
                      + `color: ${ ui.app.scheme == 'dark' ? '#aaa' : '#c1c1c1' }}`
                  + '.fade-in { opacity: 0 ; transform: translateY(10px) }'
                  + '.fade-in-less { opacity: 0 ; transition: opacity 0.2s ease }'
                  + '.fade-in.active, .fade-in-less.active { opacity: 1 ; transform: translateY(0) }'
                  + '.chatbar-btn {'
                      + 'z-index: 560 ;'
                      + 'border: none ; float: right ; position: relative ; background: none ; cursor: pointer ;'
                      + `bottom: ${ env.browser.isFF ? 28 : 32 }px ; `
                      + `${ ui.app.scheme == 'dark' ? 'color: #aaa ; fill: #aaa ; stroke: #aaa' : 'color: lightgrey ; fill: lightgrey ; stroke: lightgrey' }}`
                  + '.chatbar-btn:hover {'
                      + `${ ui.app.scheme == 'dark' ? 'color: white ; fill: white ; stroke: white' : 'color: #638ed4 ; fill: #638ed4 ; stroke: #638ed4' }}`
                  + ( // markdown styles
                        '#bravegpt > pre h1 { font-size: 1.25em } #bravegpt > pre h2 { font-size: 1.1em }' // size headings
                      + '#bravegpt > pre ul { margin: -10px 0 -6px ; }' // reduce v-spacing
                      + '#bravegpt > pre ol { margin: -33px 0 -6px ; }' // reduce v-spacing
                      + '#bravegpt > pre li { margin: -10px 0 ; list-style: inside }' ) // reduce v-spacing, show left symbols
                  + '.katex-html { display: none }' // hide unrendered math
                  + '.chatgpt-notif { fill: white ; stroke: white ; font-size: 25px !important ; padding: 6.5px 14px 8.5px 11.5px !important }'
                  + '.notif-close-btn { display: none !important }' // hide notif close btn
                  + '.chatgpt-modal > div {'
                      + 'padding: 24px 20px 14px 20px !important ;' // increase modal padding
                      + 'background-color: white !important ; color: #202124 }'
                  + '.chatgpt-modal p { margin: 14px 0 -20px 4px ; font-size: 18px }' // pos/size modal msg
                  + `.chatgpt-modal a { color: #${ ui.app.scheme == 'dark' ? '00cfff' : '1e9ebb' } !important }`
                  + `.modal-buttons { margin: 38px 0 1px ${ env.browser.isMobile ? 0 : -7 }px !important ; width: 100% }` // pos/size modal buttons
                  + '.chatgpt-modal button {' // alert buttons
                      + 'font-size: 14px ; text-transform: uppercase ; min-width: 123px ; '
                      + `padding: ${ env.browser.isMobile ? '5px' : '4px 3px' } !important ;`
                      + 'cursor: pointer ; border-radius: 0 !important ; height: 39px ;'
                      + 'border: 1px solid ' + ( ui.app.scheme == 'dark' ? 'white' : 'black' ) + ' !important }'
                  + '.primary-modal-btn { background: black !important ; color: white !important }'
                  + '.chatgpt-modal button:hover { background-color: #9cdaff !important ; color: black !important }'
                  + ( ui.app.scheme == 'dark' ? // darkmode chatgpt.alert() styles
                      ( '.chatgpt-modal > div, .chatgpt-modal button:not(.primary-modal-btn) {'
                          + 'background-color: black !important ; color: white }'
                      + '.primary-modal-btn { background: hsl(186 100% 69%) !important ; color: black !important }'
                      + '.chatgpt-modal a { color: #00cfff !important }'
                      + '.chatgpt-modal button:hover { background-color: #00cfff !important ; color: black !important }' ) : '' )
                  + '[class*="-modal-bg"] {'
                      + 'position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ;' // expand to full view-port
                      + 'transition: background-color .15s ease ;' // speed to show bg dim
                      + 'display: flex ; justify-content: center ; align-items: center ; z-index: 9999 }' // align
                  + '[class*="-modal-bg"].animated > div { z-index: 13456 ; opacity: 0.98 ; transform: translateX(0) translateY(0) }'
                  + '[class$="-modal"] {' // native modals + chatgpt.alert()s
                      + 'z-index: 13456 ; position: absolute ;' // to be click-draggable
                      + 'opacity: 0 ;' // to fade-in
                      + `background-image: linear-gradient(180deg, ${ ui.app.scheme == 'dark' ? '#99a8a6 -200px, black 200px' : '#b6ebff -296px, white 171px' }) ;`
                      + `border: 1px solid ${ ui.app.scheme == 'dark' ? 'white' : '#b5b5b5' } !important ;`
                      + `color: ${ ui.app.scheme == 'dark' ? 'white' : 'black' } ;`
                      + 'transform: translateX(-3px) translateY(7px) ;' // offset to move-in from
                      + 'transition: opacity 0.65s cubic-bezier(.165,.84,.44,1),' // for fade-ins
                                  + 'transform 0.55s cubic-bezier(.165,.84,.44,1) !important }' // for move-ins
                  + ( config.fgAnimationsDisabled || env.browser.isMobile ? '' : (
                        '[class$="-modal"] button { transition: transform 0.15s ease }'
                      + '[class$="-modal"] button:hover { transform: scale(1.055) }' ))
                  + '.bravegpt-menu {'
                      + 'position: absolute ; z-index: 2250 ;'
                      + 'padding: 3.5px 5px 4.5px !important ; font-family: "Source Sans Pro", sans-serif ; font-size: 12px }'
                  + '.bravegpt-menu ul { margin: 0 ; padding: 0 ; list-style: none }'
                  + '.bravegpt-menu-item { padding: 0 5px ; line-height: 20.5px }'
                  + '.bravegpt-menu-item:not(.bravegpt-menu-header):hover {'
                      + 'cursor: pointer ; background: white ; color: black ; fill: black }'
                  + '#checkmark-icon { fill: #b3f96d } .bravegpt-menu-item:hover #checkmark-icon { fill: green }'

                  // Glowing modal btns
                  + ':root { --glow-color: hsl(186 100% 69%); }'
                  + '.glowing-btn {'
                      + 'perspective: 2em ; font-weight: 900 ; animation: border-flicker 2s linear infinite ;'
                      + '-webkit-box-shadow: inset 0 0 0.5em 0 var(--glow-color), 0 0 0.5em 0 var(--glow-color) ;'
                      + 'box-shadow: inset 0 0 0.5em 0 var(--glow-color), 0 0 0.5em 0 var(--glow-color) ;'
                      + '-moz-box-shadow: inset 0 0 0.5em 0 var(--glow-color), 0 0 0.5em 0 var(--glow-color) }'
                  + '.glowing-txt {'
                      + 'animation: text-flicker 3s linear infinite ;'
                      + '-webkit-text-shadow: 0 0 0.125em hsl(0 0% 100% / 0.3), 0 0 0.45em var(--glow-color) ;'
                      + '-moz-text-shadow: 0 0 0.125em hsl(0 0% 100% / 0.3), 0 0 0.45em var(--glow-color) ;'
                      + 'text-shadow: 0 0 0.125em hsl(0 0% 100% / 0.3), 0 0 0.45em var(--glow-color) }'
                  + '.faulty-letter {'
                      + 'opacity: 0.5 ; animation: faulty-flicker 2s linear infinite }'
                      + ( !env.browser.isMobile ? 'background: var(--glow-color) ; transform: translateY(120%) rotateX(95deg) scale(1, 0.35)' : '' ) + '}'
                  + '.glowing-btn::after {'
                      + 'content: "" ; position: absolute ; top: 0 ; bottom: 0 ; left: 0 ; right: 0 ;'
                      + 'opacity: 0 ; z-index: -1 ; box-shadow: 0 0 2em 0.2em var(--glow-color) ;'
                      + 'background-color: var(--glow-color) ; transition: opacity 100ms linear }'
                  + '.glowing-btn:hover { color: rgba(0, 0, 0, 0.8) ; text-shadow: none ; animation: none }'
                  + '.glowing-btn:hover .glowing-txt { animation: none }'
                  + '.glowing-btn:hover .faulty-letter { animation: none ; text-shadow: none ; opacity: 1 }'
                  + '.glowing-btn:hover:before { filter: blur(1.5em) ; opacity: 1 }'
                  + '.glowing-btn:hover:after { opacity: 1 }'
                  + '@keyframes faulty-flicker {'
                      + '0% { opacity: 0.1 } 2% { opacity: 0.1 } 4% { opacity: 0.5 } 19% { opacity: 0.5 }'
                      + '21% { opacity: 0.1 } 23% { opacity: 1 } 80% { opacity: 0.5 } 83% { opacity: 0.4 } 87% { opacity: 1 }}'
                  + '@keyframes text-flicker {'
                      + '0% { opacity: 0.1 } 2% { opacity: 1 } 8% { opacity: 0.1 } 9% { opacity: 1 }'
                      + '12% { opacity: 0.1 } 20% { opacity: 1 } 25% { opacity: 0.3 } 30% { opacity: 1 } 70% { opacity: 0.7 }'
                      + '72% { opacity: 0.2 } 77% { opacity: 0.9 } 100% { opacity: 0.9 }}'
                  + '@keyframes border-flicker {'
                      + '0% { opacity: 0.1 } 2% { opacity: 1 } 4% { opacity: 0.1 } 8% { opacity: 1 }'
                      + '70% { opacity: 0.7 } 100% { opacity: 1 }}'

                  // chatgpt.alert() + BraveGPT modals
                  + '.bravegpt-modal { 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 {${ ui.app.scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: #9f9f9f ; fill: #9f9f9f' }}`
                  + ( ui.app.scheme == 'dark' ? '[class*="modal-close-btn"]:hover path { stroke: black ; fill: black }' : '' ) // invert dark mode hover paths
                  + '[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: 26px ; line-height: 32px ; padding: 0 ; margin: 4px 0 -1px !important ;'
                      + `${ env.browser.isMobile ? 'text-align: center' : 'justify-self: start' }}` // left-align on desktop, center on mobile
                  + '[class*="-modal"] p { justify-self: start ; font-size: 20px }'
                  + '[class*="-modal"] button { font-size: 14px }'

                  // Settings modal
                  + '#bravegpt-settings {'
                      + 'font-family: var(--brand-font) ;'
                      + `min-width: ${ env.browser.isPortrait ? 288 : 758 }px ; max-width: 75vw ; word-wrap: break-word ;`
                      + 'margin: 12px 23px ; border-radius: 15px ; box-shadow: 0 30px 60px rgba(0, 0, 0, .12) ;'
                      + `${ ui.app.scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: black ; fill: black' }}` // icon color
                  + `#bravegpt-settings-title { font-weight: bold ; line-height: 19px ; text-align: center ; margin: 0 ${ env.browser.isMobile ? -31 : -6 }px -3px 0 }`
                  + `#bravegpt-settings-title h4 { font-size: ${ env.browser.isPortrait ? 26 : 30 }px ; font-weight: bold ; margin: -31px 17px 7px 0 }`
                  + '#bravegpt-settings ul {'
                      + 'list-style: none ; padding: 0 ; margin: 0 ;' // hide bullets, override Brave ul margins
                      + `width: ${ env.browser.isPortrait ? 100 : 50 }% }` // set width based on column cnt
                  + '#bravegpt-settings li {'
                      + `color: ${ ui.app.scheme == 'dark' ? 'rgb(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.45)' } ;` // for text
                      + `fill: ${ ui.app.scheme == 'dark' ? 'rgb(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.45)' } ;` // for icons
                      + `stroke: ${ ui.app.scheme == 'dark' ? 'rgb(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.45)' } ;` // for icons
                      + 'height: 37px ; font-size: 14.5px ; transition: transform 0.1s ease ;'
                      + `padding: 7px 10px ; border-bottom: 1px dotted ${ ui.app.scheme == 'dark' ? 'white' : 'black' } ;` // add settings separators
                      + 'border-radius: 3px }' // make highlight strips slightly rounded
                  + '#bravegpt-settings li.active {'
                      + `color: ${ ui.app.scheme == 'dark' ? 'rgb(255, 255, 255)' : 'rgba(0, 0, 0)' } ;` // for text
                      + `fill: ${ ui.app.scheme == 'dark' ? 'rgb(255, 255, 255)' : 'rgba(0, 0, 0)' } ;` // for icons
                      + `stroke: ${ ui.app.scheme == 'dark' ? 'rgb(255, 255, 255)' : 'rgba(0, 0, 0)' }}` // for icons
                  + '#bravegpt-settings li label { padding-right: 20px }' // right-pad labels so toggles don't hug
                  + '#bravegpt-settings li:last-of-type { border-bottom: none }' // remove last bottom-border
                  + '#bravegpt-settings li, #bravegpt-settings li label { cursor: pointer }' // add finger on hover
                  + '#bravegpt-settings li:hover {'
                      + 'opacity: 1 ;'
                      + 'background: rgba(100, 149, 237, 0.88) ; color: white ; fill: white ; stroke: white ;' // add highlight strip
                      + `${ config.fgAnimationsDisabled || env.browser.isMobile ? '' : 'transform: scale(1.22)' }}` // add zoom
                  + '#bravegpt-settings li > input { float: right }' // pos toggles
                  + '#scheme-menu-entry > span { margin: 0 -2px }' // align Scheme status
                  + '#scheme-menu-entry > span > svg { position: relative ; top: 3px ; margin-left: 4px }' // v-align/left-pad Scheme status icon
                  + ( config.fgAnimationsDisabled ? '' : '#arrows-cycle { animation: rotation 5s linear infinite }' )
                  + '@keyframes rotation { from { transform: rotate(0deg) } to { transform: rotate(360deg) }}'
                  + `#about-menu-entry span { color: ${ ui.app.scheme == 'dark' ? '#28ee28' : 'green' }}`
                  + '#about-menu-entry > span {'
                      + `width: ${ env.browser.isPortrait ? '15vw' : '92px' } ; height: 20px ; overflow: hidden ;` // outer About status span
                      + `${ 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-menu-entry > span > div {'
                      + `text-wrap: nowrap ; ${ config.fgAnimationsDisabled ? '' : 'animation: ticker linear 60s infinite' }}`
                  + '@keyframes ticker { 0% { transform: translateX(100%) } 100% { transform: translateX(-2000%) }}'
                  + `.about-em { color: ${ ui.app.scheme == 'dark' ? 'white' : 'green' } !important }`
                )
            },

            tweaks() {

                // Update tweaks style based on settings
                tweaksStyle.innerText = ( config.widerSidebar ? wsbStyles : '' )
                                      + ( config.stickySidebar ? ssbStyles
                                        : config.anchored ? ( anchorStyles + ( config.expanded ? expandedStyles : '' )) : '' )

                // Update 'by KudoAI' visibility based on corner space available
                const kudoAIspan = appDiv.querySelector('.kudoai')
                if (kudoAIspan) {
                    const visibleBtnCnt = [...appDiv.querySelectorAll('.corner-btn')]
                        .filter(btn => getComputedStyle(btn).display != 'none').length
                    kudoAIspan.style.display = visibleBtnCnt <= (
                        config.anchored && config.expanded ? 10
                     : !config.anchored && config.widerSidebar ? 9
                     :  config.anchored && !config.expanded ? 6 : 4 ) ? '' : 'none'
                }

                // Update <pre> max-height for various mode toggles
                const answerPre = appDiv.querySelector('pre'),
                      relatedQueries = appDiv.querySelector('.related-queries'),
                      shorterPreHeight = window.innerHeight - relatedQueries?.offsetHeight - 304,
                      longerPreHeight = window.innerHeight - 278
                if (answerPre) answerPre.style.maxHeight = (
                    config.stickySidebar ? ( relatedQueries?.offsetHeight > 0 ? `${shorterPreHeight}px` : `${longerPreHeight}px` )
                  : config.anchored ? `${ longerPreHeight - ( config.expanded ? 115 : 365 ) }px` : 'none'
                )
            }
        }
    }

    // Define UI functions

    function isDarkMode() {
        return document.documentElement.classList.contains('dark') ? true
             : document.documentElement.classList.contains('light') ? false
             : window.matchMedia?.('(prefers-color-scheme: dark)')?.matches
    }

    function fillStarryBG(targetNode) {
        const starsDivsContainer = document.createElement('div')
        starsDivsContainer.style.cssText = 'position: absolute ; top: 0 ; left: 0 ;' // hug targetNode's top-left corner
                                         + 'height: 100% ; width: 100% ; border-radius: 15px ; overflow: clip ;' // bound innards exactly by targetNode
                                         + 'z-index: -1'; // allow interactive elems to be clicked
        ['sm', 'med', 'lg'].forEach(starSize => {
            const starsDiv = document.createElement('div')
            starsDiv.id = config.bgAnimationsDisabled ? `stars-${starSize}-off`
                : `${ ui.app.scheme == 'dark' ? 'white' : 'black' }-stars-${starSize}`
            starsDivsContainer.append(starsDiv)
        })
        targetNode.prepend(starsDivsContainer)
    }

    function visibilizeOverflow() { // for boundless hover fx
        let appAncestor = appDivContainer
        while (appAncestor) {
            if (getComputedStyle(appAncestor).overflow != 'visible') appAncestor.style.overflow = 'visible'
            appAncestor = appAncestor.parentElement
        }
    }

    const listenerize = {

        appDiv() {
            appDiv.addEventListener(inputEvents.down, event => { // to dismiss visible font size slider
                if (event.button != 0) return // prevent non-left-click dismissal
                if (document.getElementById('font-size-slider-track') // slider is visible
                    && !event.target.closest('[id*="font-size"]') // not clicking slider elem
                    && getComputedStyle(event.target).cursor != 'pointer') // ...or other interactive elem
                        fontSizeSlider.toggle('off')
            })
        },

        cornerBtns() {
            appDiv.querySelectorAll('.corner-btn').forEach(btn => { // from right to left
                if (btn.id == 'chevron-btn') btn.onclick = () => {
                    if (appDiv.querySelector('#font-size-slider-track')?.classList.contains('active')) fontSizeSlider.toggle('off')
                    toggle.minimized()
                }
                else if (btn.id == 'about-btn') btn.onclick = modals.about.show
                else if (btn.id == 'settings-btn') btn.onclick = modals.settings.show
                else if (btn.id == 'speak-btn') btn.onclick = () => {
                    const wholeAnswer = appDiv.querySelector('pre').textContent
                    const cjsSpeakOptions = { voice: 2, pitch: 1, speed: 1.5 }
                    const sgtDialectMap = [
                        { code: 'en', regex: /^(eng(lish)?|en(-\w\w)?)$/i, rate: 2 },
                        { code: 'ar', regex: /^(ara?(bic)?|اللغة العربية)$/i, rate: 1.5 },
                        { code: 'cs', regex: /^(cze(ch)?|[cč]e[sš].*|cs)$/i, rate: 1.4 },
                        { code: 'da', regex: /^dan?(ish|sk)?$/i, rate: 1.3 },
                        { code: 'de', regex: /^(german|deu?(tsch)?)$/i, rate: 1.5 },
                        { code: 'es', regex: /^(spa(nish)?|espa.*|es(-\w\w)?)$/i, rate: 1.5 },
                        { code: 'fi', regex: /^(fin?(nish)?|suom.*)$/i, rate: 1.4 },
                        { code: 'fr', regex: /^fr/i, rate: 1.2 },
                        { code: 'hu', regex: /^(hun?(garian)?|magyar)$/i, rate: 1.5 },
                        { code: 'it', regex: /^ita?(lian[ao]?)?$/i, rate: 1.4 },
                        { code: 'ja', regex: /^(ja?pa?n(ese)?|日本語|ja)$/i, rate: 1.5 },
                        { code: 'nl', regex: /^(dut(ch)?|flemish|nederlandse?|vlaamse?|nld?)$/i, rate: 1.3 },
                        { code: 'pl', regex: /^po?l(ish|ski)?$/i, rate: 1.4 },
                        { code: 'pt', regex: /^(por(tugu[eê]se?)?|pt(-\w\w)?)$/i, rate: 1.5 },
                        { code: 'ru', regex: /^(rus?(sian)?|русский)$/i, rate: 1.3 },
                        { code: 'sv', regex: /^(swe?(dish)?|sv(enska)?)$/i, rate: 1.4 },
                        { code: 'tr', regex: /^t[uü]?r(k.*)?$/i, rate: 1.6 },
                        { code: 'vi', regex: /^vi[eệ]?t?(namese)?$/i, rate: 1.5 },
                        { code: 'zh-CHS', regex: /^(chi(nese)?|zh|中[国國])/i, rate: 2 }
                    ]
                    const sgtReplyDialect = sgtDialectMap.find(entry => entry.regex.test(config.replyLanguage)) || sgtDialectMap[0],
                          payload = { text: wholeAnswer, curTime: Date.now(), spokenDialect: sgtReplyDialect.code, rate: sgtReplyDialect.rate.toString() },
                          key = CryptoJS.enc.Utf8.parse('76350b1840ff9832eb6244ac6d444366'),
                          iv = CryptoJS.enc.Utf8.parse(atob('AAAAAAAAAAAAAAAAAAAAAA==') || '76350b1840ff9832eb6244ac6d444366')
                    const securePayload = CryptoJS.AES.encrypt(JSON.stringify(payload), key, {
                        iv: iv, mode: CryptoJS.mode.CBC, pad: CryptoJS.pad.Pkcs7 }).toString()
                    xhr({ // audio from Sogou TTS
                        url: 'https://fanyi.sogou.com/openapi/external/getWebTTS?S-AppId=102356845&S-Param='
                            + encodeURIComponent(securePayload),
                        method: 'GET', responseType: 'arraybuffer',
                        onload: async resp => {
                            if (resp.status != 200) chatgpt.speak(wholeAnswer, cjsSpeakOptions)
                            else {
                                const audioContext = new (window.AudioContext || window.webkitAudioContext)()
                                audioContext.decodeAudioData(resp.response, buffer => {
                                    const audioSrc = audioContext.createBufferSource()
                                    audioSrc.buffer = buffer
                                    audioSrc.connect(audioContext.destination) // connect source to speakers
                                    audioSrc.start(0) // play audio
                                }).catch(() => chatgpt.speak(wholeAnswer, cjsSpeakOptions))
                    }}})
                }
                else if (btn.id == 'font-size-btn') btn.onclick = () => fontSizeSlider.toggle()
                else if (btn.id == 'pin-btn') btn.onmouseover = btn.onmouseout = menus.pin.toggle
                else if (btn.id == 'wsb-btn') btn.onclick = () => toggle.sidebar('wider')
                else if (btn.id == 'arrows-btn') btn.onclick = () => toggle.expandedMode()
                if (!env.browser.isMobile && btn.id != 'pin-btn') // add hover listeners for tooltips
                    btn.onmouseover = btn.onmouseout = toggle.tooltip
                if (/about|settings|speak/.test(btn.id)) btn.onmouseup = () => { // add zoom/fade-out to select buttons
                    if (config.fgAnimationsDisabled) return
                    btn.style.animation = 'btn-zoom-fade-out .220s ease-out'
                    setTimeout(() => { // hide btn after animation nears completion
                        Object.assign(btn.style, { opacity: '0', visibility: 'hidden', animation: '' })
                        setTimeout(() => // show btn after short delay
                            Object.assign(btn.style, { visibility: 'visible', opacity: '1' }), 200)
                    }, 170)
                }
            })
        },

        replySection() {

            // Add form key listener
            const replyForm = appDiv.querySelector('form')
            replyForm.onkeydown = event => {
                if (event.key == 'Enter' || event.keyCode == 13) {
                    if (event.ctrlKey) { // add newline
                        const chatTextarea = appDiv.querySelector('#app-chatbar'),
                              caretPos = chatTextarea.selectionStart,
                              textBefore = chatTextarea.value.substring(0, caretPos),
                              textAfter = chatTextarea.value.substring(caretPos)
                        chatTextarea.value = textBefore + '\n' + textAfter // add newline
                        chatTextarea.selectionStart = chatTextarea.selectionEnd = caretPos + 1 // preserve caret pos
                        listenerize.replySection.chatbarAutoSizer()
                    } else if (!event.shiftKey) listenerize.replySection.submitHandler(event)
            }}

            // Add form submit listener
            listenerize.replySection.submitHandler = event => {
                event.preventDefault()
                const chatTextarea = appDiv.querySelector('#app-chatbar')

                // No reply, change placeholder + focus chatbar
                if (chatTextarea.value.trim() == '') {
                    chatTextarea.placeholder = `${app.msgs.placeholder_typeSomething}...`
                    chatTextarea.focus()

                // Yes reply, submit it + transform to loading UI
                } else {

                    // Modify/submit msg chain
                    if (msgChain.length > 2) msgChain.splice(0, 2) // keep token usage maintainable
                    msgChain = stripQueryAugments(msgChain)
                    const prevReplyTrimmed = appDiv.querySelector('pre')?.textContent.substring(0, 250 - chatTextarea.value.length) || ''
                    msgChain.push({ role: 'assistant', content: prevReplyTrimmed })
                    msgChain.push({ role: 'user', content: augmentQuery(chatTextarea.value) })
                    get.reply(msgChain)

                    // Hide/remove elems
                    appDiv.querySelector('.related-queries')?.remove() // remove related queries
                    if (!env.browser.isMobile) tooltipDiv.style.opacity = 0 // hide 'Send reply' tooltip post-send btn click
                    const appFooter = appDiv.querySelector('footer')
                    appFooter.textContent = ''

                    // Show loading status
                    const replySection = appDiv.querySelector('section')
                    replySection.classList.add('loading', 'no-user-select')
                    replySection.innerText = app.alerts.waitingResponse

                    // Set flags
                    show.reply.src = null ; show.reply.chatbarFocused = false ; show.reply.userInteracted = true
                }
            }
            replyForm.onsubmit = listenerize.replySection.submitHandler

            // Add chatbar autosizer
            const chatTextarea = appDiv.querySelector('#app-chatbar')
            let prevLength = chatTextarea.value.length
            listenerize.replySection.chatbarAutoSizer = () => {
                const newLength = chatTextarea.value.length
                if (newLength < prevLength) { // if deleting txt
                    chatTextarea.style.height = 'auto' // ...auto-fit height
                    if (parseInt(getComputedStyle(chatTextarea).height) < 55) { // if down to one line
                        chatTextarea.style.height = '43px' } // ...reset to original height
                }
                chatTextarea.style.height = `${ chatTextarea.scrollHeight > 60 ? ( chatTextarea.scrollHeight +2 ) : 43 }px`
                prevLength = newLength
            }
            chatTextarea.oninput = listenerize.replySection.chatbarAutoSizer

            // Add button listeners
            appDiv.querySelectorAll('.chatbar-btn').forEach(btn => {
                if (btn.id == 'shuffle-btn') btn.onclick = () => {
                    const randQAprompt = 'Generate a single random question on any topic then answer it. '
                                       + 'Don\'t talk about Canberra, Tokyo, blue whales, photosynthesis, oceans, '
                                           + 'deserts, mindfulness meditation, the Fibonacci sequence, the liver, '
                                           + 'Jupiter, the Great Wall of China, Sheakespeare or da Vinci. '
                                       + 'Try to give an answer that is 50-100 words. '
                                       + 'Do not type anything but the question and answer. Reply in markdown.'
                    chatTextarea.value = augmentQuery(randQAprompt)
                    chatTextarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
                    show.reply.src = 'shuffle'
                }
                if (!env.browser.isMobile) // add hover listener for tooltips
                    btn.onmouseover = btn.onmouseout = toggle.tooltip
            })
        }
    }

    const fontSizeSlider = {
        fadeInDelay: 5, // ms
        hWheelDistance: 10, // px

        createAppend() {
            log.caller = 'fontSizeSlider.createAppend()'
            log.debug('Creating/appending Font Size slider...')

            // Create/ID/classify slider elems
            fontSizeSlider.cursorOverlay = document.createElement('div')
            fontSizeSlider.cursorOverlay.classList.add('cursor-overlay') // for resize cursor
            const slider = document.createElement('div') ; slider.id = 'font-size-slider-track'
            slider.className = 'fade-in-less' ; slider.style.display = 'none'
            const sliderThumb = document.createElement('div') ; sliderThumb.id = 'font-size-slider-thumb'
            sliderThumb.title = Math.floor(config.fontSize *10) /10 + 'px' // font size tooltip
            const sliderTip = document.createElement('div') ; sliderTip.id = 'font-size-slider-tip'

            // Assemble/insert elems
            slider.append(sliderThumb, sliderTip)
            appDiv.insertBefore(slider, appDiv.querySelector('.btn-tooltip,' // desktop
                                                           + 'pre')) // mobile
            // Init thumb pos
            setTimeout(() => {
                const iniLeft = (config.fontSize - config.minFontSize) / (config.maxFontSize - config.minFontSize) // left ratio
                              * (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.appendChild(fontSizeSlider.cursorOverlay)
            })
            document.addEventListener(inputEvents.move, event => {
                if (isDragging) moveThumb(startLeft + event.clientX - startX) })
            document.addEventListener(inputEvents.up, () => {
                isDragging = false
                if (fontSizeSlider.cursorOverlay.parentNode)
                    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.appendChild(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 answerPre = appDiv.querySelector('pre'),
                      fontSizePercent = newLeft / sliderWidth,
                      fontSize = config.minFontSize + fontSizePercent * (config.maxFontSize - config.minFontSize)
                answerPre.style.fontSize = fontSize + 'px'
                answerPre.style.lineHeight = fontSize * config.lineHeightRatio + 'px'
                settings.save('fontSize', fontSize)
                sliderThumb.title = Math.floor(config.fontSize *10) /10 + 'px'
            }

            return slider
        },

        toggle(state = '') {
            log.caller = `fontSizeSlider.toggle(${ state ? `'${state}'` : '' })`
            const slider = document.getElementById('font-size-slider-track') || fontSizeSlider.createAppend(),
                  replyTip = appDiv.querySelector('.reply-tip'),
                  sliderTip = document.getElementById('font-size-slider-tip')

            // Show slider
            if (state == 'on' || (!state && slider.style.display == 'none')) {
                log.debug('Showing Font Size slider...')

                // Position slider tip
                const btnSpan = document.getElementById('font-size-btn'),
                      rects = { appDiv: appDiv.getBoundingClientRect(), btnSpan: btnSpan.getBoundingClientRect() }
                sliderTip.style.right = `${ rects.appDiv.right - ( rects.btnSpan.left + rects.btnSpan.right )/2 -32 }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)

                log.debug('Success! Font Size slider shown')

            // Hide slider
            } else if (state == 'off' || (!state && slider.style.display != 'none')) {
                log.debug('Hiding Font Size slider...')
                slider.classList.remove('active') ; if (replyTip) replyTip.style.display = ''
                sliderTip.style.display = slider.style.display = 'none'
                log.debug('Success! Font Size slider hidden')
            }
        }
    }

    // Define TOGGLE functions

    const toggle = {

        anchorMode(state = '') {
            log.caller = `toggle.anchorMode(${ state ? `'${state}'` : '' })`
            const prevState = config.anchored // for restraining notif if no change from #pin-menu 'Sidebar' click
            if (state == 'on' || !state && !config.anchored) {
                log.debug('Toggling Anchor Mode on...')
                settings.save('anchored', true)
                if (config.stickySidebar) toggle.sidebar('sticky') // off
            } else {
                log.debug('Toggling Anchor Mode off...')
                settings.save('anchored', false)
                if (config.expanded) toggle.expandedMode('off')
            }
            update.style.tweaks() ; update.rqVisibility() // apply new state to UI
            if (modals.settings.get()) { // update visual state of Settings toggle
                const anchorToggle = document.querySelector('[id*="anchor"][id*="menu-entry"] input')
                if (anchorToggle.checked != config.anchored) modals.settings.toggle.switch(anchorToggle)
            }
            if (prevState != config.anchored) {
                menus.pin.topPos = menus.pin.rightPos = null
                notify(`${app.msgs.mode_anchor} ${menu.state.words[+config.anchored]}`)
                log.debug(`Success! Anchor Mode toggled ${ config.anchored ? 'ON' : 'OFF' }`)
            }
        },

        animations(layer) {
            log.caller = `toggle.animations('${layer}')`
            const configKey = layer + 'AnimationsDisabled'
            log.debug(`Toggling ${layer.toUpperCase()} animations ${ config[configKey] ? 'ON' : 'OFF' }...`)
            settings.save(configKey, !config[configKey])
            update.style.app() ; if (layer == 'bg') { update.stars() ; update.replyPrefix() }
            if (layer == 'fg' && modals.settings.get()) {

                // Toggle ticker-scroll of About status label
                const aboutStatusLabel = document.querySelector('#about-menu-entry > span > div')
                aboutStatusLabel.innerHTML = modals.settings.aboutContent[config.fgAnimationsDisabled ? 'short' : 'long']
                aboutStatusLabel.style.float = config.fgAnimationsDisabled ? 'right' : ''

                // Toggle button glow
                if (ui.app.scheme == 'dark') toggle.btnGlow()
            }
            log.caller = `toggle.animations('${layer}')`
            log.debug(`Success! ${layer.toUpperCase()} animations toggled ${ config[configKey] ? 'OFF' : 'ON' }`)
            notify(`${settings.controls[layer + 'AnimationsDisabled'].label} ${menu.state.words[+!config[layer + 'AnimationsDisabled']]}`)
        },

        autoGet() {
            log.caller = 'toggle.autoGet()'
            log.debug(`Toggling Auto-Get ${ config.autoGetDisabled ?  'ON' : 'OFF' }...`)
            settings.save('autoGetDisabled', !config.autoGetDisabled)
            if (appDiv.querySelector('.standby-btn')) show.reply.standbyBtnClickHandler()
            if (!config.autoGetDisabled) // disable Prefix/Suffix mode if enabled
                ['prefix', 'suffix'].forEach(manualMode => {
                    if (config[manualMode + 'Enabled']) toggle.manualGet(manualMode) })
            notify(`${settings.controls.autoGetDisabled.label} ${menu.state.words[+!config.autoGetDisabled]}`)
            if (modals.settings.get()) { // update visual state of Settings toggle
                const autoGetToggle = document.querySelector('[id*="autoGet"][id*="menu-entry"] input')
                if (autoGetToggle.checked == config.autoGetDisabled) modals.settings.toggle.switch(autoGetToggle)
            }
            log.caller = 'toggle.autoGet()'
            log.debug(`Success! config.autoGetDisabled = ${config.autoGetDisabled}`)
        },

        btnGlow(state = '') {
            log.caller = `toggle.btnGlow(${ state ? `'${state}'` : '' })`
            const removeCondition = state == 'off' || ui.app.scheme != 'dark' || config.fgAnimationsDisabled
            document.querySelectorAll('[class*="-modal"] button').forEach((btn, idx) => {
                setTimeout(() => btn.classList[removeCondition ? 'remove' : 'add']('glowing-btn'),
                    (idx +1) *50 *chatgpt.randomFloat()) // to unsync flickers
                let btnTextSpan = btn.querySelector('span')
                if (!btnTextSpan) { // wrap btn.textContent for .glowing-txt
                    btnTextSpan = document.createElement('span')
                    btnTextSpan.textContent = btn.textContent ; btn.textContent = ''
                    btn.append(btnTextSpan)
                }
                btnTextSpan.classList[removeCondition ? 'remove' : 'add']('glowing-txt')
            })
        },

        expandedMode(state = '') {
            log.caller = `toggle.expandedMode(${ state ? `'${state}'` : '' })`
            const toExpand = state == 'on' || !state && !config.expanded
            log.debug(`${ toExpand ? 'Expanding' : 'Shrinking' } ${app.name}...`)
            settings.save('expanded', toExpand)
            if (config.minimized) toggle.minimized('off') // since user wants to see stuff
            update.style.tweaks() // apply new state to UI
            icons.arrowsDiagonal.update() ; tooltipDiv.style.opacity = 0 // update icon/tooltip
            log.caller = `toggle.expandedMode(${ state ? `'${state}'` : '' })`
            log.debug(`Success! ${app.name} ${ toExpand ? 'expanded' : 'shrunk' }`)
        },

        manualGet(mode) { // Prefix/Suffix modes
            log.caller = `toggle.manualGet('${mode}')`
            const modeKey = mode + 'Enabled'
            log.debug(`Toggling ${log.toTitleCase(mode)} Mode ${ config[modeKey] ? 'OFF' : 'ON' }...`)
            settings.save(modeKey, !config[modeKey])
            if (config[modeKey] && !config.autoGetDisabled) toggle.autoGet() // disable Auto-Get mode if enabled
            notify(`${settings.controls[modeKey].label} ${menu.state.words[+config[modeKey]]}`)
            if (modals.settings.get()) { // update visual state of Settings toggle
                const modeToggle = document.querySelector(`[id*="${modeKey}"][id*="menu-entry"] input`)
                if (modeToggle.checked != config[modeKey]) modals.settings.toggle.switch(modeToggle)
            }
            log.caller = `toggle.manualGet('${mode}')`
            log.debug(`Success! config.${modeKey} = ${config[modeKey]}`)
        },

        minimized(state = '') {
            log.caller = `toggle.minimized(${ state ? `'${state}'` : '' })`
            const toMinimize = state == 'on' || !state && !config.minimized
            log.debug(`${ toMinimize ? 'Mimizing' : 'Restoring' } ${app.name}...`)
            settings.save('minimized', toMinimize)
            const chevronBtn = appDiv.querySelector('#chevron-btn')
            if (chevronBtn) { // update icon
                const chevronSVG = icons[`chevron${ config.minimized ? 'Up' : 'Down' }`].create()
                chevronBtn.firstChild.remove() ; chevronBtn.append(chevronSVG)
                chevronBtn.onclick = () => {
                    if (appDiv.querySelector('#font-size-slider-track')?.classList.contains('active')) fontSizeSlider.toggle('off')
                    toggle.minimized()
                }
            }
            update.appBottomPos() // toggle visual minimization
            if (!env.browser.isMobile) setTimeout(() => tooltipDiv.style.opacity = 0, 1) // remove lingering tooltip
            log.caller = `toggle.minimized(${ state ? `'${state}'` : '' })`
            log.debug(`Success! ${app.name} ${ toMinimize ? 'minimized' : 'restored' }`)
        },

        proxyMode() {
            log.caller = 'toggle.proxyMode()'
            log.debug(`Toggling Proxy Mode ${ config.proxyAPIenabled ? 'OFF' : 'ON' }...`)
            settings.save('proxyAPIenabled', !config.proxyAPIenabled)
            notify(`${app.msgs.menuLabel_proxyAPImode} ${menu.state.words[+config.proxyAPIenabled]}`)
            menu.refresh()
            if (modals.settings.get()) { // update visual states of Settings toggles
                const proxyToggle = document.querySelector('[id*="proxy"][id*="menu-entry"] input'),
                      streamingToggle = document.querySelector('[id*="streaming"][id*="menu-entry"] input')
                if (proxyToggle.checked != config.proxyAPIenabled) // Proxy state out-of-sync (from using toolbar menu)
                        modals.settings.toggle.switch(proxyToggle)
                if (streamingToggle.checked && !config.proxyAPIenabled // Streaming checked but OpenAI mode
                    || !streamingToggle.checked && config.proxyAPIenabled && !config.streamingDisabled) // or Streaming unchecked but enabled in Proxy mode
                        modals.settings.toggle.switch(streamingToggle)
            }
            if (appDiv.querySelector('#bravegpt-alert')) location.reload() // re-send query if user alerted
            else {
                log.caller = 'toggle.proxyMode()'
                log.debug(`Success! config.proxyAPIenabled = ${config.proxyAPIenabled}`)
            }
        },

        relatedQueries() {
            log.caller = 'toggle.relatedQueries()'
            log.debug(`Toggling Related Queries ${ config.rqDisabled ? 'ON' : 'OFF' }...`)
            settings.save('rqDisabled', !config.rqDisabled)
            update.rqVisibility()
            if (!config.rqDisabled && !appDiv.querySelector('.related-queries')) // get related queries for 1st time
                get.related(stripQueryAugments(msgChain)[msgChain.length - 1].content).then(queries => show.related(queries))
                    .catch(err => { log.error(err.message) ; api.tryNew(get.related) })
            update.style.tweaks() // toggle <pre> max-height
            notify(`${app.msgs.menuLabel_relatedQueries} ${menu.state.words[+!config.rqDisabled]}`)
            log.debug(`Success! config.rqDisabled = ${config.rqDisabled}`)
        },

        sidebar(mode, state = '') {
            log.caller = `toggle.sidebar('${mode}'${ state ? `, '${state}'` : '' })`
            const configKeyName = mode + 'Sidebar',
                  toToggleOn = state == 'on' || !state && !config[configKeyName],
                  prevStickyState = config.stickySidebar // for restraining notif if no change from #pin-menu Sidebar-click
            log.debug(`Toggling ${log.toTitleCase(mode)} Sidebar ${ toToggleOn ? 'ON' : 'OFF' }`)
            if (state == 'on' || !state && !config[configKeyName]) { // toggle on
                if (mode == 'sticky' && config.anchored) toggle.anchorMode()
                settings.save(configKeyName, true)
            } else settings.save(configKeyName, false)
            update.style.tweaks() // apply new state to UI
            if (mode == 'wider') icons.widescreen.update() // toggle icons everywhere
            if (modals.settings.get()) { // update visual state of Settings toggle
                const stickySidebarToggle = document.querySelector('[id*="sticky"][id*="menu-entry"] input')
                if (stickySidebarToggle.checked != config.stickySidebar) modals.settings.toggle.switch(stickySidebarToggle)
            }
            if (mode == 'sticky' && prevStickyState == config.stickySidebar)
                return log.debug(`No change to ${log.toTitleCase(mode)} Sidebar`)
            notify(`${ app.msgs[`menuLabel_${ mode }Sidebar`] || mode.charAt(0).toUpperCase() + mode.slice(1) + ' Sidebar' } ${
                       menu.state.words[+config[configKeyName]]}`)
            log.debug(`Success! ${log.toTitleCase(mode)} Sidebar toggled ${ toToggleOn ? 'ON' : 'OFF' }`)
        },


        streaming() {
            log.caller = 'toggle.streaming()'
            const scriptCatLink = 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'
            if (!streamingSupported.byScriptManager) { // alert userscript manager unsupported, suggest TM/SC
                log.debug(`Streaming Mode unsupported in ${env.scriptManager.name}`)
                const suggestAlertID = siteAlert(`${settings.controls.streamingDisabled.label} ${app.msgs.alert_unavailable}`,
                    `${settings.controls.streamingDisabled.label} ${app.msgs.alert_isOnlyAvailFor}`
                        + ( !env.browser.isEdge && !env.browser.isBrave ? // suggest TM for supported browsers
                            ` <a target="_blank" rel="noopener" href="https://tampermonkey.net">Tampermonkey</a> ${app.msgs.alert_and}`
                                : '' )
                        + ` <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a>.` // suggest SC
                        + ` (${app.msgs.alert_userscriptMgrNoStream}.)`
                )
                const suggestAlert = document.getElementById(suggestAlertID).firstChild
                modals.init(suggestAlert) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
            } else if (!streamingSupported.byBrowser) { // alert TM/browser unsupported, suggest SC
                log.debug('Streaming Mode unsupported in browser')
                const suggestAlertID = siteAlert(`${settings.controls.streamingDisabled.label} ${app.msgs.alert_unavailable}`,
                    `${settings.controls.streamingDisabled.label} ${app.msgs.alert_isUnsupportedIn} `
                        + `${ env.browser.isChrome ? 'Chrome' : env.browser.isEdge ? 'Edge' : 'Brave' } ${app.msgs.alert_whenUsing} Tampermonkey. `
                        + `${app.msgs.alert_pleaseUse} <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a> `
                            + `${app.msgs.alert_instead}.`
                )
                const suggestAlert = document.getElementById(suggestAlertID).firstChild
                modals.init(suggestAlert) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
            } else if (!config.proxyAPIenabled) { // alert OpenAI API unsupported, suggest Proxy Mode
                log.debug('Streaming Mode unsupported in OpenAI 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 alertID = siteAlert(`${app.msgs.mode_streaming} ${app.msgs.alert_unavailable}`, msg),
                      alert = document.getElementById(alertID).firstChild
                modals.init(alert) // add classes/stars, disable wheel-scrolling, dim bg, glowup btns
                alert.querySelector('[href="#"]').onclick = () => { alert.querySelector('.modal-close-btn').click() ; toggle.proxyMode() }
            } else { // functional toggle
                log.debug(`Toggling Streaming Mode ${ config.streamingDisabled ? 'ON' : 'OFF' }`)
                settings.save('streamingDisabled', !config.streamingDisabled)
                notify(`${settings.controls.streamingDisabled.label} ${menu.state.words[+!config.streamingDisabled]}`)
                log.debug(`Success! config.streamingDisabled = ${config.streamingDisabled}`)
            }
        },

        tooltip(event) {
            const btnElem = event.currentTarget, btnType = btnElem.id.replace(/-btn$/, ''),
                  cornerBtnTypes = ['chevron', 'about', 'settings', 'speak', 'font-size', 'pin', 'wsb', 'arrows']

            // Update text
            tooltipDiv.innerText = (
                btnType == 'chevron' ? ( config.minimized ? `${app.msgs.tooltip_restore}`
                                                          : `${app.msgs.tooltip_minimize}` )
              : btnType == 'about' ? app.msgs.menuLabel_about
              : btnType == 'settings' ? app.msgs.menuLabel_settings
              : btnType == 'speak' ? app.msgs.tooltip_playAnswer
              : btnType == 'font-size' ? app.msgs.tooltip_fontSize
              : btnType == 'wsb' ? (( config.widerSidebar ? `${app.msgs.prefix_exit} ` :  '' )
                               + ( app.msgs.menuLabel_widerSidebar ))
              : btnType == 'arrows' ? ( config.expanded ? `${app.msgs.tooltip_shrink}`
                                                        : `${app.msgs.tooltip_expand}` )
              : btnType == 'copy' ? ( btnElem.firstChild.id == 'copy-icon' ? `${app.msgs.tooltip_copy} ${
                  ( btnElem.parentNode.tagName == 'PRE' ? app.msgs.tooltip_reply : app.msgs.tooltip_code ).toLowerCase() }`
                      : `${app.msgs.notif_copiedToClipboard}!` )
              : btnType == 'send' ? app.msgs.tooltip_sendReply
              : btnType == 'shuffle' ? app.msgs.tooltip_askRandQuestion : '' )

            // Update position
            const elems = { appDiv, btnElem, tooltipDiv },
                  rects = {} ; Object.keys(elems).forEach(key => rects[key] = elems[key].getBoundingClientRect())
            tooltipDiv.style.top = `${ cornerBtnTypes.includes(btnType) ? -6 : rects.btnElem.top - rects.appDiv.top -36 }px`
            tooltipDiv.style.right = `${ rects.appDiv.right - ( rects.btnElem.left + rects.btnElem.right )/2 - rects.tooltipDiv.width/2 }px`

            // Toggle visibility
            tooltipDiv.style.opacity = event.type == 'mouseover' ? 1 : 0
        }
    }

    // Define SESSION functions

    const session = {

        deleteOpenAIcookies() {
            log.caller = 'session.deleteOpenAIcookies()'
            log.debug('Deleting OpenAI cookies...')
            GM_deleteValue(app.configKeyPrefix + '_openAItoken')
            if (env.scriptManager.name != 'Tampermonkey') return
            GM_cookie.list({ url: apis.OpenAI.endpoints.auth }, (cookies, error) => {
                if (!error) { for (const cookie of cookies) {
                    GM_cookie.delete({ url: apis.OpenAI.endpoints.auth, name: cookie.name })
            }}})
        },

        generateGPTFLkey() {
            log.caller = 'session.generateGPTFLkey()'
            log.debug('Generating GPTforLove key...')
            let nn = Math.floor(new Date().getTime() / 1e3)
            const fD = e => {
                let t = CryptoJS.enc.Utf8.parse(e),
                    o = CryptoJS.AES.encrypt(t, 'vrewbhjvbrejhbevwjh156645', {
                        mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7
                })
                return o.toString()
            }
            const gptflKey = fD(nn)
            log.debug(gptflKey) ; return gptflKey
        },

        getOAItoken() {
            log.caller = 'session.getOAItoken()'
            log.debug('Getting OpenAI token...')
            return new Promise(resolve => {
                const accessToken = GM_getValue(app.configKeyPrefix + '_openAItoken')
                if (accessToken) { log.debug(accessToken) ; resolve(accessToken) }
                else {
                    log.debug(`No token found. Fetching from ${apis.OpenAI.endpoints.session}...`)
                    xhr({ url: apis.OpenAI.endpoints.session, onload: resp => {
                        if (session.isBlockedByCF(resp.responseText)) {
                            appAlert('checkCloudflare') ; return }
                        try {
                            const newAccessToken = JSON.parse(resp.responseText).accessToken
                            GM_setValue(app.configKeyPrefix + '_openAItoken', newAccessToken)
                            log.debug(`Success! newAccessToken = ${newAccessToken}`)
                            resolve(newAccessToken)
                        } catch { if (get.reply.api == 'OpenAI') appAlert('login') ; return }
            }})}})
        },

        isBlockedByCF(resp) {
            try {
                const html = new DOMParser().parseFromString(resp, 'text/html'),
                      title = html.querySelector('title')
                if (title.innerText == 'Just a moment...') {
                    log.caller = 'session.isBlockedByCF'
                    log.debug('Blocked by CloudFlare')
                    return true
                }
            } catch (err) { return false }
        }
    }
    // Define API functions

    const api = {

        clearTimedOut(triedAPIs) { // to retry on new queries
            triedAPIs.splice(0, triedAPIs.length, // empty apiArray
                ...triedAPIs.filter(entry => Object.values(entry)[0] != 'timeout')) // replace w/ err'd APIs
        },

        createHeaders(api) {
            const ip = ipv4.generate({ verbose: false })
            const headers = {
                'Accept-Encoding': 'gzip, deflate, br, zstd',
                'Connection': 'keep-alive', 'Content-Type': 'application/json', 'DNT': '1',
                'Host': new URL(apis[api].endpoints?.completions || apis[api].endpoint).hostname,
                'Origin': apis[api].expectedOrigin.url,
                'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors',
                'TE': 'trailers', 'X-Forwarded-For': ip, 'X-Real-IP': ip
            }
            headers.Referer = headers.Origin + '/'
            if (api == 'OpenAI') headers.Authorization = 'Bearer ' + config.openAIkey
            Object.assign(headers, apis[api].expectedOrigin.headers)
            return headers
        },

        createPayload(api, msgs) {
            let payload = {}
            if (api == 'OpenAI')
                payload = { messages: msgs, model: 'gpt-3.5-turbo', max_tokens: 4000 }
            else if (api == 'AIchatOS') {
                payload = {
                    prompt: msgs[msgs.length - 1].content,
                    withoutContext: false, userId: apis.AIchatOS.userID, network: true
                }
            } else if (api == 'GPTforLove') {
                payload = {
                    prompt: msgs[msgs.length - 1].content,
                    secret: session.generateGPTFLkey(), top_p: 1, temperature: 0.8,
                    systemMessage: 'You are ChatGPT, the version is GPT-4o, a large language model trained by OpenAI.'
                                 + 'Follow the user\'s instructions carefully.'
                }
                if (apis.GPTforLove.parentID) payload.options = { parentMessageId: apis.GPTforLove.parentID }
            } else if (api == 'MixerBox AI')
                payload = { prompt: msgs, model: 'gpt-3.5-turbo' }
            return JSON.stringify(payload)
        },

        pick(caller) {
            log.caller = `get.${caller.name}() » api.pick()`
            const untriedAPIs = Object.keys(apis).filter(api =>
                   api != ( caller == get.reply ? 'OpenAI' : '' ) // exclude OpenAI for get.reply() since Proxy Mode
                && !caller.triedAPIs.some(entry => Object.prototype.hasOwnProperty.call(entry, api)) // exclude tried APIs
                && (config.streamingDisabled || apis[api].streamable)) // exclude unstreamable APIs if !config.streamingDisabled
            const chosenAPI = untriedAPIs[ // pick random array entry
                Math.floor(chatgpt.randomFloat() * untriedAPIs.length)]
            if (!chosenAPI) { log.error('No proxy APIs left untried') ; return null }
            log.debug('Endpoint chosen', apis[chosenAPI].endpoints?.completions || apis[chosenAPI].endpoint)
            return chosenAPI
        },

        tryNew(caller, reason = 'err') {
            log.caller = `get.${caller.name}() » api.tryNew()`
            if (caller.status == 'done') return
            log.error(`Error using ${ apis[caller.api].endpoints?.completions || apis[caller.api].endpoint } due to ${reason}`)
            caller.triedAPIs.push({ [caller.api]: reason })
            if (caller.attemptCnt < Object.keys(apis).length -+(caller == get.reply)) {
                log.debug('Trying another endpoint...')
                caller.attemptCnt++
                caller(caller == get.reply ? msgChain : get.related.query)
                    .then(result => { if (caller == get.related) show.related(result) ; else return })
            } else {
                log.debug('No remaining untried endpoints')
                if (caller == get.reply) appAlert('proxyNotWorking', 'suggestOpenAI')
            }
        }
    }

    // Define QUERY AUGMENT functions

    function augmentQuery(query) { return query + ` (reply in ${config.replyLanguage})` }

    function stripQueryAugments(msgChain) {
        const augmentCnt = augmentQuery.toString().match(/\+/g).length
        return msgChain.map(msg => { // stripped chain
            if (msg.role == 'user') {
                let content = msg.content
                const augments = content.match(/\s*\([^)]*\)\s*/g)
                if (augments) for (let i = 0 ; i < augmentCnt ; i++) // strip augments
                    content = content.replace(augments[augments.length - 1 - i], '')
                return { ...msg, content: content.trim() }
            } else return msg // agent's unstripped
        })
    }

    // Define GET functions

    const get = {

        json(url, callback) { // for dynamic footer
            xhr({ method: 'GET', url: url, onload: resp => {
                if (resp.status >= 200 && resp.status < 300) {
                    try { const data = JSON.parse(resp.responseText) ; callback(null, data) }
                    catch (err) { callback(err, null) }
                } else callback(new Error('Failed to load data: ' + resp.statusText), null)
            }})
        },

        async reply(msgChain) {

            // Init API attempt props
            get.reply.status = 'waiting'
            if (!get.reply.triedAPIs) get.reply.triedAPIs = []
            if (!get.reply.attemptCnt) get.reply.attemptCnt = 1

            // Pick API
            get.reply.api = config.proxyAPIenabled ? api.pick(get.reply) : 'OpenAI'
            if (!get.reply.api) { // no more proxy APIs left untried
                appAlert('proxyNotWorking', 'suggestOpenAI') ; return }

            // Init OpenAI key
            if (!config.proxyAPIenabled)
                config.openAIkey = await Promise.race([session.getOAItoken(), new Promise(reject => setTimeout(reject, 3000))])

            // Try diff API after 6-9s of no response
            else {
                const iniAPI = get.reply.api
                setTimeout(() => {
                    if (config.proxyAPIenabled // only do in Proxy mode
                        && get.reply.status != 'done' && !get.reply.sender // still no reply received
                        && get.reply.api == iniAPI // not already trying diff API from err
                        && get.reply.triedAPIs.length != Object.keys(apis).length -1 // untried APIs remain
                    ) api.tryNew(get.reply, 'timeout')
                }, config.streamingDisabled ? 9000 : 6000)
            }

            // Get/show answer from AI
            xhr({
                method: apis[get.reply.api].method,
                url: apis[get.reply.api].endpoints?.completions || apis[get.reply.api].endpoint,
                responseType: config.streamingDisabled || !config.proxyAPIenabled ? 'text' : 'stream',
                headers: api.createHeaders(get.reply.api), data: api.createPayload(get.reply.api, msgChain),
                onload: resp => dataProcess.text(get.reply, resp),
                onloadstart: resp => dataProcess.stream(get.reply, resp),
                onerror: err => { log.error(err)
                    if (!config.proxyAPIenabled) appAlert(!config.openAIkey ? 'login' : ['openAInotWorking', 'suggestProxy'])
                    else api.tryNew(get.reply)
                }
            })

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

            update.footerContent()
        },

        async related(query) {

            // Init API attempt props
            get.related.status = 'waiting'
            if (!get.related.triedAPIs) get.related.triedAPIs = []
            if (!get.related.attemptCnt) get.related.attemptCnt = 1

            // Pick API
            get.related.api = api.pick(get.related)
            if (!get.related.api) return // no more proxy APIs left untried

            // Init OpenAI key
            if (get.related.api == 'OpenAI')
                config.openAIkey = await Promise.race([session.getOAItoken(), new Promise(reject => setTimeout(reject, 3000))])

            // Init prompt
            const rqPrompt = 'Print me a numbered list of '
                + `${ get.related.replyIsQuestion ? 'possible answers to this question' : 'queries related to this one' }:\n\n"${query}"\n\n`
                +   ( get.related.replyIsQuestion ? 'Generate answers as if in reply to a search engine chatbot asking the question.'

                  // Extended instructions for non-question queries
                  : ( 'Make sure to suggest a variety that can even greatly deviate from the original topic. '
                    + 'For example, if the original query asked about someone\'s wife, '
                        + ' a good related query could involve a different relative and using their name. '
                    + 'Another example, if the query asked about a game/movie/show, '
                        + ' good related queries could involve pertinent characters. '
                    + 'Another example, if the original query asked how to learn JavaScript, '
                       + ' good related queries could ask why/when/where instead, even replacing JS w/ other languages. '
                    + 'But the key is variety. Do not be repetitive. '
                       + ' You must entice user to want to ask one of your related queries.' ))

                + ` Reply in ${config.replyLanguage}`

            // Try diff API after 7s of no response
            const iniAPI = get.related.api
            get.related.query = query // expose to api.tryNew() in case modded
            setTimeout(() => {
                if (get.related.status != 'done' // still no queries received
                    && get.related.api == iniAPI // not already trying diff API from err
                    && get.related.triedAPIs.length != Object.keys(apis).length // untried APIs remain
                ) api.tryNew(get.related, 'timeout')
            }, 7000)

            // Get queries
            return new Promise(resolve => xhr({
                method: apis[get.related.api].method,
                url: apis[get.related.api].endpoints?.completions || apis[get.related.api].endpoint,
                responseType: 'text', headers: api.createHeaders(get.related.api),
                data: api.createPayload(get.related.api, [{ role: 'user', content: rqPrompt }]),
                onload: resp => dataProcess.text(get.related, resp).then(resolve),
                onerror: err => { log.error(err) ; api.tryNew(get.related) }
            }))
        }
    }

    // Define PROCESS functions

    const dataProcess = {

        initFailFlags(api) { // escape/merge URLs w/ fail flags
            const { failFlags = [], endpoint = apis[api].endpoints.completions, expectedOrigin } = apis[api],
                  escapedAPIurls = [endpoint, expectedOrigin.url].map(url => url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
            return new RegExp([...failFlags, ...escapedAPIurls].join('|'))
        },

        stream(caller, stream) {
            if (config.streamingDisabled || !config.proxyAPIenabled) return
            log.caller = `get.${caller.name}() » dataProcess.stream()`
            const failFlagsAndURLs = dataProcess.initFailFlags(caller.api),
                  reader = stream.response.getReader() ; let accumulatedChunks = ''
            reader.read().then(processStreamText).catch(err => log.error('Error processing stream', err.message))

            function processStreamText({ done, value }) {
                if (done) { caller.sender = null
                    if (appDiv.querySelector('.loading')) // no text shown
                        api.tryNew(caller)
                    else { // text was shown
                        caller.status = 'done' ; caller.attemptCnt = null
                        show.copyBtns() ; api.clearTimedOut(caller.triedAPIs)
                    } return
                }
                let chunk = new TextDecoder('utf8').decode(new Uint8Array(value))
                if (caller.api == 'MixerBox AI') { // pre-process chunks
                    const extractedChunks = Array.from(chunk.matchAll(/data:(.*)/g), match => match[1]
                        .replace(/\[SPACE\]/g, ' ').replace(/\[NEWLINE\]/g, '\n'))
                        .filter(match => !/message_(?:start|end)|done/.test(match))
                    chunk = extractedChunks.join('')
                }
                accumulatedChunks = apis[caller.api].accumulatesText ? chunk : accumulatedChunks + chunk
                if (/^(?:\{|event:)/.test(accumulatedChunks)) api.tryNew(caller)
                else {
                    try { // to show stream text
                        let textToShow = ''
                        if (caller.api == 'GPTforLove') { // extract parentID + latest chunk text
                            const jsonLines = accumulatedChunks.split('\n'),
                                  nowResult = JSON.parse(jsonLines[jsonLines.length - 1])
                            if (nowResult.id) apis.GPTforLove.parentID = nowResult.id // for contextual replies
                            textToShow = nowResult.text
                        } else textToShow = accumulatedChunks
                        const failMatch = failFlagsAndURLs.exec(textToShow)
                        if (failMatch) {
                            log.debug('Response text', textToShow)
                            log.error('Fail flag detected', `'${failMatch[0]}'`)
                            if (caller.status != 'done' && !caller.sender) api.tryNew(caller)
                            return
                        } else if (caller.status != 'done') { // app waiting or sending
                            if (!caller.sender) caller.sender = caller.api // app is waiting, become sender
                            if (caller.sender == caller.api // app is sending from this caller.api
                                && textToShow.trim() != '' // empty chunk not read
                            ) show.reply(textToShow, footerContent)
                        }
                    } catch (err) { log.error('Error showing stream', err.message) }
                    return reader.read().then(({ done, value }) => {
                        if (caller.sender == caller.api) // am designated sender, recurse
                            processStreamText({ done, value })
                    }).catch(err => log.error('Error reading stream', err.message))
                }
            }
        },

        text(caller, resp) {
            return new Promise(resolve => {
                if (caller == get.reply && config.proxyAPIenabled && !config.streamingDisabled || caller.status == 'done') return
                log.caller = `get.${caller.name}() » dataProcess.text()`
                const failFlagsAndURLs = dataProcess.initFailFlags(caller.api) ; let respText = ''
                if (resp.status != 200) {
                    log.error('Response status', resp.status)
                    log.info('Response text', resp.response || resp.responseText)
                    if (caller == get.reply && caller.api == 'OpenAI')
                        appAlert(resp.status == 401 ? 'login'
                               : resp.status == 403 ? 'checkCloudflare'
                               : resp.status == 429 ? ['tooManyRequests', 'suggestProxy']
                                                    : ['openAInotWorking', 'suggestProxy'] )
                    else api.tryNew(caller)
                } else if (caller.api == 'OpenAI' && resp.response) {
                    const failMatch = failFlagsAndURLs.exec(resp.response)
                    if (failMatch) { // suggest proxy or try diff API
                        log.debug('Response text', resp.response)
                        log.error('Fail flag detected', `'${failMatch[0]}'`)
                        if (caller == get.reply) appAlert('openAInotWorking', 'suggestProxy')
                        else api.tryNew(caller)
                    } else {
                        try { // to show response or return related queries
                            respText = JSON.parse(resp.response).choices[0].message.content
                            handleProcessCompletion()
                        } catch (err) { handleProcessError(err) }
                    }
                } else if (resp.responseText) {
                    if (caller.api == 'AIchatOS') {
                        try { // to show response or return related queries
                            const text = resp.responseText, chunkSize = 1024
                            let currentIdx = 0
                            while (currentIdx < text.length) {
                                const chunk = text.substring(currentIdx, currentIdx + chunkSize)
                                respText += chunk ; currentIdx += chunkSize
                            }
                            handleProcessCompletion()
                        } catch (err) { handleProcessError(err) }
                    } else if (caller.api == 'GPTforLove') {
                        try { // to show response or return related queries
                            let chunks = resp.responseText.trim().split('\n'),
                                lastObj = JSON.parse(chunks[chunks.length - 1])
                            if (lastObj.id) apis.GPTforLove.parentID = lastObj.id
                            respText = lastObj.text ; handleProcessCompletion()
                        } catch (err) { handleProcessError(err) }
                    } else if (caller.api == 'MixerBox AI') {
                        try { // to show response or return related queries
                            const extractedData = Array.from(resp.responseText.matchAll(/data:(.*)/g), match => match[1]
                                .replace(/\[SPACE\]/g, ' ').replace(/\[NEWLINE\]/g, '\n'))
                                .filter(match => !/message_(?:start|end)|done/.test(match))
                            respText = extractedData.join('') ; handleProcessCompletion()
                        } catch (err) { handleProcessError(err) }
                    }
                } else if (caller.status != 'done') { // proxy 200 response failure
                    log.info('Response text', resp.responseText) ; api.tryNew(caller) }

                function handleProcessCompletion() {
                    if (caller.status != 'done') {
                        log.debug('Response text', respText)
                        const failMatch = failFlagsAndURLs.exec(respText)
                        if (failMatch || /^(?:\{|event:)/.test(respText)) {
                            if (failMatch) log.error('Fail flag detected', `'${failMatch[0]}'`)
                            api.tryNew(caller)
                        } else {
                            caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                            if (caller == get.reply) { show.reply(respText, footerContent) ; show.copyBtns() }
                            else resolve(arrayify(respText))
                }}}

                function handleProcessError(err) { // suggest proxy or try diff API
                    log.debug('Response text', resp.response)
                    log.error(app.alerts.parseFailed, err)
                    if (caller.api == 'OpenAI' && caller == get.reply) appAlert('openAInotWorking', 'suggestProxy')
                    else api.tryNew(caller)
                }

                /* eslint-disable regexp/no-super-linear-backtracking */
                function arrayify(strList) { // for get.related() calls
                    log.caller = 'dataProcess.text » arrayify()'
                    log.debug('Arrayifying related queries...')
                    return (strList.trim().match(/\d+\.?\s*([^\n]+?)(?=\n|\\n|$)/g) || [])
                        .slice(0, 5) // limit to 1st 5
                        .map(match => match.replace(/\*\*/g, '') // strip markdown boldenings
                            .replace(/^['"]*(?:\d+\.?\s*)?['"]*(.*?)['"]*$/g, '$1')) // strip numbering + quotes
                } /* eslint-enable regexp/no-super-linear-backtracking */
        })}
    }

    // Define SHOW functions

    const show = {

        copyBtns() {
            if (document.getElementById('copy-btn')) return

            appDiv.querySelectorAll('#bravegpt > pre, code').forEach(parentElem => {
                const copyBtn = document.createElement('btn'),
                      copySVG = icons.copy.create(parentElem)
                copyBtn.id = 'copy-btn' ; copySVG.id = 'copy-icon'
                copyBtn.className = 'no-mobile-tap-outline'
                copyBtn.append(copySVG) ; let elemToPrepend = copyBtn

                // Wrap code button in div for v-offset
                if (parentElem.tagName == 'CODE') {
                    elemToPrepend = document.createElement('div')
                    elemToPrepend.style.height = '11px'
                    elemToPrepend.append(copyBtn)
                }

                // Add listeners
                if (!env.browser.isMobile) copyBtn.onmouseover = copyBtn.onmouseout = toggle.tooltip
                copyBtn.onclick = event => { // copy text, update icon + tooltip status
                    const copySVG = copyBtn.querySelector('#copy-icon')
                    if (!copySVG) return // since clicking on copied icon
                    const textContainer = copyBtn.parentNode.tagName == 'PRE' ? copyBtn.parentNode // reply container
                                                                              : copyBtn.parentNode.parentNode, // code container
                          textToCopy = textContainer.textContent.replace(/^>> /, '').trim(),
                          checkmarksSVG = icons.checkmarkDouble.create() ; checkmarksSVG.id = 'copied-icon'
                    copyBtn.replaceChild(checkmarksSVG, copySVG) // change to copied icon
                    setTimeout(() => copyBtn.replaceChild(copySVG, checkmarksSVG), 1355) // change back to copy icon
                    navigator.clipboard.writeText(textToCopy) // copy text to clipboard
                    if (!env.browser.isMobile) toggle.tooltip(event) // show copied status in tooltip
                }

                // Prepend button
                parentElem.prepend(elemToPrepend)
            })
        },

        reply(answer) {

            // Build answer interface up to reply section if missing
            if (!appDiv.querySelector('pre')) {
                appDiv.textContent = ''
                fillStarryBG(appDiv) // add stars

                // Create/append title
                const appHeaderLogo = logos.braveGPT.create() ; appHeaderLogo.width = 143
                const appTitleAnchor = create.anchor(app.urls.app, appHeaderLogo)
                appTitleAnchor.classList.add('app-name', 'no-user-select')
                appDiv.append(appTitleAnchor)

                // Create/append corner buttons div
                const cornerBtnsDiv = document.createElement('div')
                cornerBtnsDiv.id = 'corner-btns' ; cornerBtnsDiv.className = 'no-mobile-tap-outline'
                appDiv.append(cornerBtnsDiv)

                // Create/append Chevron button
                if (!env.browser.isMobile) {
                    var chevronBtn = document.createElement('btn'),
                        chevronSVG = icons[`chevron${ config.minimized ? 'Up' : 'Down' }`].create()
                    chevronBtn.id = 'chevron-btn' // for toggle.tooltip()
                    chevronBtn.classList.add('corner-btn')
                    chevronBtn.style.margin = '0.5px 1px 0 11px'
                    chevronBtn.style.display = 'none' // to activate from anchorStyles only
                    chevronBtn.append(chevronSVG) ; cornerBtnsDiv.append(chevronBtn)
                }

                // Create/append About button
                const aboutBtn = document.createElement('btn'),
                      aboutSVG = icons.questionMarkCircle.create()
                aboutBtn.id = 'about-btn' // for toggle.tooltip()
                aboutBtn.className = 'corner-btn' ; aboutBtn.style.marginTop = '0.8px'
                aboutBtn.append(aboutSVG) ; cornerBtnsDiv.append(aboutBtn)

                // Create/append Settings button
                const settingsBtn = document.createElement('btn'),
                      settingsSVG = icons.sliders.create()
                settingsBtn.id = 'settings-btn' // for toggle.tooltip()
                settingsBtn.className = 'corner-btn' ; settingsBtn.style.margin = '2px 9px 0 1px'
                settingsBtn.append(settingsSVG) ; cornerBtnsDiv.append(settingsBtn)

                // Create/append Speak button
                if (answer != 'standby') {
                    var speakerBtn = document.createElement('btn'),
                        speakerSVG = icons.speaker.create()
                    speakerBtn.id = 'speak-btn' // for toggle.tooltip()
                    speakerBtn.className = 'corner-btn' ; speakerBtn.style.marginRight = '7px'
                    speakerBtn.append(speakerSVG) ; cornerBtnsDiv.append(speakerBtn)
                }

                // Create/append Font Size button
                if (answer != 'standby') {
                    var fontSizeBtn = document.createElement('btn'),
                        fontSizeSVG = icons.fontSize.create()
                    fontSizeBtn.id = 'font-size-btn' // for toggle.tooltip()
                    fontSizeBtn.className = 'corner-btn' ; fontSizeBtn.style.margin = '1px 10px 0 2px'
                    fontSizeBtn.append(fontSizeSVG) ; cornerBtnsDiv.append(fontSizeBtn)
                }

                // Create/append Pin button
                if (!env.browser.isMobile) {
                    var pinBtn = document.createElement('btn'),
                        pinSVG = icons.pin.create()
                    pinBtn.id = 'pin-btn' // for toggle.sidebar() + toggle.tooltip()
                    pinBtn.className = 'corner-btn' ; pinBtn.style.margin = '1px 9px 0 0'
                    pinBtn.append(pinSVG) ; cornerBtnsDiv.append(pinBtn)

                // Create/append Wider Sidebar button
                    var wsbBtn = document.createElement('btn'),
                        wsbSVG = icons.widescreen.create()
                    wsbBtn.id = 'wsb-btn' // for toggle.sidebar() + toggle.tooltip()
                    wsbBtn.className = 'corner-btn' ; wsbBtn.style.margin = '0.151em 11px 0 0'
                    wsbBtn.append(wsbSVG) ; cornerBtnsDiv.append(wsbBtn)

                // Create/append Expand/Shrink button
                    var arrowsBtn = document.createElement('btn'),
                        arrowsSVG = icons.arrowsDiagonal.create()
                    arrowsSVG.style.transform = 'rotate(-7deg)' // tilt slightly to hint expansions are often horizontal-only
                    arrowsBtn.id = 'arrows-btn' // for toggle.tooltip()
                    arrowsBtn.className = 'corner-btn' ; arrowsBtn.style.margin = '0.5px 12px 0 0'
                    arrowsBtn.style.display = 'none' // to activate from anchorStyles only
                    arrowsBtn.append(arrowsSVG) ; cornerBtnsDiv.append(arrowsBtn)
                }

                // Add tooltips
                if (!env.browser.isMobile) appDiv.append(tooltipDiv)

                // Add corner button listeners
                listenerize.cornerBtns()

                // Create/append 'by KudoAI'
                const kudoAIspan = document.createElement('span')
                kudoAIspan.classList.add('kudoai', 'no-user-select') ; kudoAIspan.textContent = 'by '
                kudoAIspan.style.cssText = 'position: relative ; bottom: 8px ; font-size: 12px'
                kudoAIspan.append(create.anchor(app.urls.publisher, 'KudoAI'))
                appDiv.querySelector('.app-name').insertAdjacentElement('afterend', kudoAIspan)

                // Show standby state if prefix/suffix mode on
                if (answer == 'standby') {
                    const standbyBtn = document.createElement('button')
                    standbyBtn.classList.add('standby-btn', 'no-mobile-tap-outline')
                    standbyBtn.textContent = app.msgs.btnLabel_sendQueryToApp || `Send search query to ${app.name}`
                    appDiv.append(standbyBtn)
                    show.reply.standbyBtnClickHandler = function() {
                        appAlert('waitingResponse')
                        msgChain.push({ role: 'user', content: augmentQuery(new URL(location.href).searchParams.get('q')) })
                        show.reply.userInteracted = true ; show.reply.chatbarFocused = false
                        menus.pin.topPos = menus.pin.rightPos = null
                        get.reply(msgChain)
                    }
                    standbyBtn.onclick = show.reply.standbyBtnClickHandler

                // Otherwise create/append answer bubble
                } else {
                    const answerPre = document.createElement('pre'),
                          balloonTipSpan = document.createElement('span')
                    balloonTipSpan.className = 'reply-tip'
                    appDiv.append(balloonTipSpan, answerPre)
                }

                update.style.tweaks() // show/hide 'by KudoAI', update pre-height based on mode
            }

            // Build reply section if missing
            if (!appDiv.querySelector('#app-chatbar')) {

                // Init/clear reply section content/classes
                const replySection = appDiv.querySelector('section') || document.createElement('section')
                replySection.textContent = '' ; replySection.classList.remove('loading', 'no-user-select')

                // Create/append section elems
                const replyForm = document.createElement('form'),
                      continueChatDiv = document.createElement('div'),
                      chatTextarea = document.createElement('textarea')
                continueChatDiv.className = 'continue-chat'
                chatTextarea.id = 'app-chatbar' ; chatTextarea.rows = '1'
                chatTextarea.placeholder = ( answer == 'standby' ? app.msgs.placeholder_askSomethingElse
                                                                 : app.msgs.tooltip_sendReply ) + '...'
                continueChatDiv.append(chatTextarea)
                replyForm.append(continueChatDiv) ; replySection.append(replyForm)
                appDiv.insertBefore(replySection, appDiv.querySelector('footer'));

                // Create/append chatbar buttons
                ['send', 'shuffle'].forEach(btnType => {
                    const btnElem = document.createElement('button')
                    btnElem.id = `${btnType}-btn` ; btnElem.classList.add('chatbar-btn', 'no-mobile-tap-outline')
                    btnElem.style.right = `${ btnType == 'send' ? 12 : 20 }px`
                    btnElem.append(icons[btnType == 'send' ? 'arrowUp' : 'arrowsTwistedRight'].create())
                    continueChatDiv.append(btnElem)
                })

                // Init/fill/append footer
                const appFooter = appDiv.querySelector('footer') || document.createElement('footer')
                appFooter.append(footerContent)
                if (!appDiv.querySelector('footer')) appDiv.append(appFooter)

                // Add listeners
                listenerize.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
                }
            }

            saveAppDiv() // to fight Brave mutations

            // Render/show answer if query sent
            if (answer != 'standby') {
                const answerPre = appDiv.querySelector('pre')
                try { // to render markdown
                    answerPre.innerHTML = marked.parse(answer) } catch (err) { log.error(err.message) }
                hljs.highlightAll() // highlight code
                update.replyPrefix() // prepend '>> ' if dark scheme w/ bg animations to emulate terminal

                // Typeset math
                answerPre.querySelectorAll('code').forEach(codeBlock => { // add linebreaks after semicolons
                    codeBlock.innerHTML = codeBlock.innerHTML.replace(/;\s*/g, ';<br>') })
                const elemsToRenderMathIn = [answerPre, ...answerPre.querySelectorAll('*')]
                elemsToRenderMathIn.forEach(elem => {
                    renderMathInElement(elem, { // typeset math
                        delimiters: [
                            { left: '$$', right: '$$', display: true },
                            { left: '$', right: '$', display: false },
                            { left: '\\(', right: '\\)', display: false },
                            { left: '\\[', right: '\\]', display: true },
                            { left: '\\begin{equation}', right: '\\end{equation}', display: true },
                            { left: '\\begin{align}', right: '\\end{align}', display: true },
                            { left: '\\begin{alignat}', right: '\\end{alignat}', display: true },
                            { left: '\\begin{gather}', right: '\\end{gather}', display: true },
                            { left: '\\begin{CD}', right: '\\end{CD}', display: true },
                            { left: '\\[', right: '\\]', display: true }
                        ],
                        throwOnError: false
                })})

                if (config.stickySidebar) update.style.tweaks() // to reset answerPre height
                saveAppDiv() // to fight Brave mutations

                // Auto-scroll if active
                if (config.autoScroll && !env.browser.isMobile && config.proxyAPIenabled && !config.streamingDisabled) {
                    if (config.stickySidebar || config.anchored) answerPre.scrollTop = answerPre.scrollHeight
                    window.scrollBy({ top: appDiv.querySelector('footer').getBoundingClientRect().bottom - window.innerHeight + 13 })
                }
            }

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

            // Restore minimized/restored state if anchored
            if (config.anchored) update.appBottomPos()

            show.reply.userInteracted = false
        },

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

            // Re-get.related() if current reply is question to suggest answers
            const currentReply = appDiv.querySelector('#bravegpt > pre')?.textContent.trim()
            if (show.reply.src != 'shuffle' && !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 && !appDiv.querySelector('.related-queries')) {

                // Create/classify/append parent div
                const relatedQueriesDiv = document.createElement('div') ; relatedQueriesDiv.className = 'related-queries'
                appDiv.append(relatedQueriesDiv)

                // Fill each child div, add attributes + icon + listener
                queries.forEach((query, idx) => {
                    const relatedQueryDiv = document.createElement('div'),
                          relatedQuerySVG = icons.arrowDownRight.create()

                    // Add attributes
                    relatedQueryDiv.title = app.msgs.tooltip_sendRelatedQuery
                    relatedQueryDiv.classList.add('related-query', 'fade-in', 'no-user-select', 'no-mobile-tap-outline')
                    relatedQueryDiv.setAttribute('tabindex', 0)
                    relatedQueryDiv.textContent = query

                    // Assemble/insert elems
                    relatedQueryDiv.prepend(relatedQuerySVG) ; relatedQueriesDiv.append(relatedQueryDiv)

                    // Add fade + listeners
                    setTimeout(() => {
                        relatedQueryDiv.classList.add('active')
                        relatedQueryDiv.onclick = relatedQueryDiv.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 relatedQuery = event.target.textContent, chatbar = appDiv.querySelector('textarea')
                                if (chatbar) {
                                    chatbar.value = relatedQuery
                                    if (/\[[^[\]]+\]/.test(relatedQuery)) { // highlight 1st bracket-enclosed placeholder
                                        chatbar.focus()
                                        listenerize.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 * 100)
                })

                get.related.replyIsQuestion = null
                update.style.tweaks() // to shorten <pre> max-height
            }
        }
    }

    // Run MAIN routine

    log.debug('Registering toolbar menu...') ; menu.register() ; log.debug('Success! Menu registered')

    // Init UI props
    log.debug('Initializing UI properties...')
    const ui = { app: { scheme: config.scheme || ( isDarkMode() ? 'dark' : 'light' )}}
    log.debug(`Success! ui = '${log.prettifyObj(ui)}`)

    // Create/ID/classify/listenerize BRAVEGPT container
    let appDiv = document.createElement('div') ; appDiv.id = 'bravegpt'
    appDiv.classList.add('fade-in',  'snippet') ; listenerize.appDiv()

    // Stylize APP elems
    const appStyle = create.style() ; update.style.app() ; document.head.append(appStyle);
    ['hljs', 'wsbg', 'bsbg'].forEach(cssType => // code highlighting, white stars, black stars
        document.head.append(create.style(GM_getResourceText(`${cssType}CSS`))))

    // Stylize SITE elems
    const tweaksStyle = create.style(),
          wsbStyles = 'main.main-column, aside.sidebar { max-width: 521px !important }'
                    + '#bravegpt { width: 521px }',
          ssbStyles = '#bravegpt { position: sticky ; top: 83px }'
                    + '#bravegpt ~ * { display: none }', // hide sidebar contents
          anchorStyles = '#bravegpt { position: fixed ; bottom: -7px ; right: 35px ; width: 441px }'
                       + '[class*="feedback"], .related-queries, #wsb-btn  { display: none }'
                       + '#chevron-btn, #arrows-btn { display: block !important }',
          expandedStyles = '#bravegpt { width: 538px }'
    update.style.tweaks() ; document.head.append(tweaksStyle)

    // Create/stylize TOOLTIPs
    if (!env.browser.isMobile) {
        var tooltipDiv = document.createElement('div') ; tooltipDiv.classList.add('btn-tooltip', 'no-user-select')
        document.head.append(create.style('.btn-tooltip {'
            + 'background-color: rgba(0, 0, 0, 0.64) ; padding: 5px 6px 3px ; border-radius: 6px ; border: 1px solid #d9d9e3 ;' // bubble style
            + 'font-size: 0.58rem ; color: white ; fill: white ; stroke: white ;' // font/icon style
            + 'position: absolute ;' // for update.tooltip() calcs
            + 'box-shadow: 3px 5px 16px 0 rgb(0 0 0 / 21%) ;' // drop shadow
            + 'opacity: 0 ; transition: opacity 0.1s ; height: fit-content ; z-index: 1250 }' // visibility
        ))
    }

    // APPEND to Brave
    const appDivContainerSelector = env.browser.isMobile ? '#results' : '.sidebar'
    const appDivContainer = await new Promise(resolve => {
        const container = document.querySelector(appDivContainerSelector)
        if (container) resolve(container)
        else new MutationObserver((_, obs) => {
            const container = document.querySelector(appDivContainerSelector)
            if (container) { obs.disconnect() ; resolve(container) }
        }).observe(document.body, { childList: true, subtree: true })
    })
    appDivContainer.prepend(appDiv) ; visibilizeOverflow()
    setTimeout(() => appDiv.classList.add('active'), 100) // fade in

    // Init footer CTA to share feedback
    let footerContent = create.anchor('#', app.msgs.link_shareFeedback, { target: '_self' })
    footerContent.classList.add('feedback', 'svelte-8js1iq') // Brave classes
    footerContent.onclick = () => modals.feedback.show({ sites: 'feedback' })

    // REFERRALIZE links to support author
    setTimeout(() => document.querySelectorAll('a[href^="https://www.amazon."]').forEach(anchor => {
        const url = new URL(anchor.href) ; url.searchParams.set('tag', 'kudo-ai-20')
        anchor.href = url.toString()
    }), 1500)

    // Show STANDBY mode or get/show ANSWER
    let msgChain = [{ role: 'user', content: augmentQuery(new URL(location.href).searchParams.get('q')) }]
    if (config.autoGetDisabled && !/src=(?:first-run|asktip)/.test(location.href) // Auto-Get disabled and not queried from other site or 1st run
        || config.prefixEnabled && !/.*q=%2F/.test(location.href) // prefix required but not present
        || config.suffixEnabled && !/.*q=.*(?:%3F|?|%EF%BC%9F)(?:&|$)/.test(location.href)) { // suffix required but not present
            show.reply('standby', footerContent)
            if (!config.rqDisabled)
                get.related(stripQueryAugments(msgChain)[msgChain.length - 1].content).then(queries => show.related(queries))
                    .catch(err => { log.error(err.message) ; api.tryNew(get.related) })
    } else { appAlert('waitingResponse') ; get.reply(msgChain) }
    saveAppDiv() // to fight Brave mutations

    // Add key listener to DISMISS modals
    document.addEventListener('keydown', modals.keyHandler)

    // Observe/listen for Brave Search + system SCHEME CHANGES to update BraveGPT scheme if auto-scheme mode
    new MutationObserver(handleSchemeChange).observe( // class changes from Brave Search theme settings
        document.documentElement, { attributes: true, attributeFilter: ['class'] })
    window.matchMedia('(prefers-color-scheme: dark)') // window.matchMedia changes from browser/system settings
        .onchange = handleSchemeChange
    function handleSchemeChange() {
        if (config.scheme) return // since light/dark hard-set
        const newScheme = isDarkMode() ? 'dark' : 'light'
        if (newScheme != ui.app.scheme) update.scheme(newScheme)
    }

    // Observe DOM for need to re-insert BraveGPT
    new MutationObserver((_, obs) => {
        if (!document.getElementById('bravegpt')) { restoreAppDiv() ; obs.disconnect() }
    }).observe(document.body, { subtree: true, childList: true })
    function saveAppDiv() { if (restoreAppDiv.restored) return ; saveAppDiv.html = appDiv.innerHTML }
    function restoreAppDiv() {
        log.caller = 'restoreAppDiv()'
        log.debug(`Restoring ${app.name} from mutation...`)
        appDiv = document.createElement('div') ; appDiv.id = 'bravegpt'
        appDiv.classList.add('fade-in', 'active', 'snippet') ; listenerize.appDiv()
        appDiv.innerHTML = saveAppDiv.html
        if (!env.browser.isMobile) appDiv.append(tooltipDiv)
        if (appDiv.querySelector('.corner-btn')) listenerize.cornerBtns()
        const standbyBtn = appDiv.querySelector('.standby-btn')
        if (standbyBtn) standbyBtn.onclick = show.reply.standbyBtnClickHandler
        else if (appDiv.querySelector('pre')) show.copyBtns()
        if (appDiv.querySelector('.chatbar-btn')) listenerize.replySection()
        appDivContainer.prepend(appDiv) ; visibilizeOverflow() ; restoreAppDiv.restored = true
    }

})()