Greasy Fork is available in English.

BraveGPT 🤖

Lägger till AI-svar till Brave Search (driven av GPT-4o!)

Installera detta skript?
Författaren's rekommenderade skript

Du kanske också gillar DuckDuckGPT 🤖.

Installera detta skript
// ==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.7.5.2
// @license               MIT
// @icon                  https://media.bravegpt.com/images/icons/bravegpt/icon48.png?0a9e287
// @icon64                https://media.bravegpt.com/images/icons/bravegpt/icon64.png?0a9e287
// @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
// @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               binjie.fun
// @connect               chatgpt.com
// @connect               gptforlove.com
// @connect               greasyfork.org
// @connect               jsdelivr.net
// @connect               mixerbox.com
// @connect               openai.com
// @connect               sogou.com
// @require               https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js@2.9.3/dist/chatgpt.min.js#sha256-EDN+mCc+0Y4YVzJEoNikd4/rAIaJDLAdb+erWvupXTM=
// @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.2/dist/generate-ip.min.js#sha256-PRvQIDVWK/a+aAqEFVQv7RePbRe/tX6tWQVM80rAe2M=
// @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_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

setTimeout(async () => {

    // Init BROWSER FLAGS
    const isChrome = !!JSON.stringify(navigator.userAgentData?.brands)?.includes('Chrome'),
          isFirefox = chatgpt.browser.isFirefox(),
          isEdge = !!JSON.stringify(navigator.userAgentData?.brands)?.includes('Edge'),
          isBrave = !!JSON.stringify(navigator.userAgentData?.brands)?.includes('Brave'),
          isMobile = chatgpt.browser.isMobile()

    // Init CONFIG
    const config = {
        appName: 'BraveGPT', appSymbol: '🤖', keyPrefix: 'braveGPT',
        appURL: 'https://www.bravegpt.com', gitHubURL: 'https://github.com/KudoAI/bravegpt',
        greasyForkURL: 'https://greasyfork.org/scripts/462440-bravegpt',
        minFontSize: 11, maxFontSize: 24, lineHeightRatio: 1.313,
        latestAssetCommitHash: 'ca48dfb' } // for cached messages.json
    config.updateURL = config.greasyForkURL.replace('https://', 'https://update.')
        .replace(/(\d+)-?([a-zA-Z-]*)$/, (_, id, name) => `${ id }/${ !name ? 'script' : name }.meta.js`)
    config.supportURL = config.gitHubURL + '/issues/new'
    config.assetHostURL = config.gitHubURL.replace('github.com', 'cdn.jsdelivr.net/gh') + `@${config.latestAssetCommitHash}/`
    config.userLanguage = chatgpt.getUserLanguage()
    config.userLocale = config.userLanguage.includes('-') ? config.userLanguage.split('-')[1].toLowerCase() : ''
    loadSetting('autoGetDisabled', 'autoFocusChatbarDisabled', 'autoScroll', 'bgAnimationsDisabled', 'fgAnimationsDisabled',
                'fontSize', 'prefixEnabled', 'proxyAPIenabled', 'replyLanguage', 'rqDisabled', 'scheme',
                'stickySidebar', 'streamingDisabled', 'suffixEnabled', 'widerSidebar')
    if (!config.replyLanguage) saveSetting('replyLanguage', config.userLanguage) // init reply language if unset
    if (!config.fontSize) saveSetting('fontSize', 16) // init reply font size if unset
    if ( // disable streaming in unspported envs
        !/Tampermonkey|ScriptCat/.test(getUserscriptManager()) // unsupported userscript manager
        || getUserscriptManager() == 'Tampermonkey' && (isChrome || isEdge || isBrave) // TM in browser that triggers STATUS_ACCESS_VIOLATION
    ) saveSetting('streamingDisabled', true)

    // Init FETCHER
    const xhr = getUserscriptManager() == 'OrangeMonkey' ? GM_xmlhttpRequest : GM.xmlHttpRequest

    // Init API props
    const openAIendpoints = { auth: 'https://auth0.openai.com', session: 'https://chatgpt.com/api/auth/session' }
    const apis = {
        'AIchatOS': {
            endpoint: 'https://api.binjie.fun/api/generateStream', expectedOrigin: 'https://chat18.aichatos8.com',
            method: 'POST', streamable: true, accumulatesText: false, failFlags: ['很抱歉地', '系统公告'] },
        'GPTforLove': {
            endpoint: 'https://api11.gptforlove.com/chat-process', expectedOrigin: 'https://ai27.gptforlove.com',
            method: 'POST', streamable: true, accumulatesText: true },
        'MixerBox AI': {
            endpoint: 'https://chatai.mixerbox.com/api/chat/stream', expectedOrigin: 'https://chatai.mixerbox.com',
            method: 'POST', streamable: true, accumulatesText: false },
        'OpenAI': {
            endpoint: 'https://api.openai.com/v1/chat/completions', expectedOrigin: 'https://chatgpt.com',
            method: 'POST', streamable: true }
    }
    const apiIDs = { gptForLove: { parentID: '' }, aiChatOS: { userID: '#/chat/' + Date.now() }}

    // Init INPUT EVENTS
    const inputEvents = {} ; ['down', 'move', 'up'].forEach(action =>
          inputEvents[action] = ( window.PointerEvent ? 'pointer' : isMobile ? 'touch' : 'mouse' ) + action)

    // Init MESSAGES
    let msgs = {}
    const msgsLoaded = new Promise(resolve => {
        const msgHostDir = config.assetHostURL + 'greasemonkey/_locales/',
              msgLocaleDir = ( config.userLanguage ? config.userLanguage.replace('-', '_') : 'en' ) + '/'
        let msgHref = msgHostDir + msgLocaleDir + 'messages.json', msgXHRtries = 0
        xhr({ method: 'GET', url: msgHref, onload: onLoad })
        function onLoad(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
                xhr({ method: 'GET', url: msgHref, onload: onLoad })
            }
        }
    }) ; if (!config.userLanguage.startsWith('en')) try { msgs = await msgsLoaded } catch (err) {}

    // Init SETTINGS props
    const settingsProps = {
        proxyAPIenabled: { type: 'toggle',
            label: msgs.menuLabel_proxyAPImode || 'Proxy API Mode',
            helptip: msgs.helptip_proxyAPImode || 'Uses a Proxy API for no-login access to AI' },
        streamingDisabled: { type: 'toggle',
            label: msgs.mode_streaming || 'Streaming Mode',
            helptip: msgs.helptip_streamingMode || 'Receive replies in a continuous text stream' },
        autoGetDisabled: { type: 'toggle',
            label: msgs.menuLabel_autoGetAnswers || 'Auto-Get Answers',
            helptip: msgs.helptip_autoGetAnswers || 'Auto-send queries to BraveGPT when using search engine' },
        autoFocusChatbarDisabled: { type: 'toggle', mobile: false,
            label: msgs.menuLabel_autoFocusChatbar || 'Auto-Focus Chatbar',
            helptip: msgs.helptip_autoFocusChatbar || 'Auto-focus chatbar whenever it appears' },
        autoScroll: { type: 'toggle', mobile: false,
            label: `${ msgs.mode_autoScroll || 'Auto-Scroll' } (${ msgs.menuLabel_whenStreaming || 'when streaming' })`,
            helptip: msgs.helptip_autoScroll || 'Auto-scroll responses as they generate in Streaming Mode' },
        rqDisabled: { type: 'toggle',
            label: `${ msgs.menuLabel_show || 'Show' } ${ msgs.menuLabel_relatedQueries || 'Related Queries' }`,
            helptip: msgs.helptip_showRelatedQueries || 'Show related queries below chatbar' },
        prefixEnabled: { type: 'toggle',
            label: `${ msgs.menuLabel_require || 'Require' } "/" ${ msgs.menuLabel_beforeQuery || 'before query' }`,
            helptip: msgs.helptip_prefixMode || 'Require "/" before queries for answers to show' },
        suffixEnabled: { type: 'toggle',
            label: `${ msgs.menuLabel_require || 'Require' } "?" ${ msgs.menuLabel_afterQuery || 'after query' }`,
            helptip: msgs.helptip_suffixMode || 'Require "?" after queries for answers to show' },
        widerSidebar: { type: 'toggle', mobile: false, centered: false,
            label: msgs.menuLabel_widerSidebar || 'Wider Sidebar',
            helptip: msgs.helptip_widerSidebar || 'Horizontally expand search page sidebar' },
        stickySidebar: { type: 'toggle', mobile: false, centered: false,
            label: msgs.menuLabel_stickySidebar || 'Sticky Sidebar',
            helptip: msgs.helptip_stickySidebar || 'Makes BraveGPT visible in sidebar even as you scroll' },
        bgAnimationsDisabled: { type: 'toggle',
            label: `${ msgs.menuLabel_background || 'Background' } ${ msgs.menuLabel_animations || 'Animations' }`,
            helptip: msgs.helptip_bgAnimations || 'Show animated backgrounds in UI components' },
        fgAnimationsDisabled: { type: 'toggle',
            label: `${ msgs.menuLabel_foreground || 'Foreground' } ${ msgs.menuLabel_animations || 'Animations' }`,
            helptip: msgs.helptip_fgAnimations || 'Show foreground animations in UI components' },
        replyLanguage: { type: 'prompt',
            label: msgs.menuLabel_replyLanguage || 'Reply Language',
            helptip: msgs.helptip_replyLanguage || 'Language for BraveGPT to reply in' },
        scheme: { type: 'modal',
            label: msgs.menuLabel_colorScheme || 'Color Scheme',
            helptip: msgs.helptip_colorScheme || 'Scheme to display BraveGPT UI components in' },
        about: { type: 'modal',
            label: `${ msgs.menuLabel_about || 'About' } ${config.appName}...` }
    }

    // Init MENU objs
    const menuIDs = [] // to store registered cmds for removal while preserving order
    const menuState = {
        symbol: ['❌', '✔️'], separator: getUserscriptManager() == 'Tampermonkey' ? ' — ' : ': ',
        word: [(msgs.state_off || 'Off').toUpperCase(), (msgs.state_on || 'On').toUpperCase()]
    }

    // Define SCRIPT functions

    function loadSetting(...keys) { keys.forEach(key => config[key] = GM_getValue(config.keyPrefix + '_' + key, false)) }
    function saveSetting(key, value) { GM_setValue(config.keyPrefix + '_' + key, value) ; config[key] = value }
    function safeWindowOpen(url) { window.open(url, '_blank', 'noopener') } // to prevent backdoor vulnerabilities
    function getUserscriptManager() { try { return GM_info.scriptHandler } catch (err) { return 'other' }}

    // Define MENU functions

    function registerMenu() {

        // Add command to toggle proxy API mode
        const pmLabel  = menuState.symbol[+config.proxyAPIenabled] + ' '
                       + settingsProps.proxyAPIenabled.label + ' '
                       + menuState.separator + menuState.word[+config.proxyAPIenabled]
        menuIDs.push(GM_registerMenuCommand(pmLabel, toggle.proxyMode))

        // Add command to launch About modal
        const aboutLabel = `💡 ${settingsProps.about.label}`
        menuIDs.push(GM_registerMenuCommand(aboutLabel, modals.about.show))

        // Add command to launch Settings modal
        const settingsLabel = `⚙️ ${ msgs.menuLabel_settings || 'Settings' }`
        menuIDs.push(GM_registerMenuCommand(settingsLabel, modals.settings.show))
    }

    function promptReplyLang() {
        while (true) {
            let replyLanguage = prompt(
                ( msgs.prompt_updateReplyLang || 'Update reply language' ) + ':', 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() )
                saveSetting('replyLanguage', replyLanguage || config.userLanguage)
                siteAlert(( msgs.alert_langUpdated || 'Language updated' ) + '!', // title
                    `${ config.appName } ${ msgs.alert_willReplyIn || 'will reply in' } `
                        + ( replyLanguage || msgs.alert_yourSysLang || 'your system language' ) + '.',
                    '', '', 447) // confirmation width
                if (modals.settings.get()) // update settings menu status label
                    document.querySelector('#replyLanguage-menu-entry span').textContent = replyLanguage
                break
    }}}

    function refreshMenu() {
        if (getUserscriptManager() == 'OrangeMonkey') return
        for (const id of menuIDs) { GM_unregisterMenuCommand(id) } registerMenu()
    }

    function updateCheck() {

        // Fetch latest meta
        const currentVer = GM_info.script.version
        xhr({
            method: 'GET', url: config.updateURL + '?t=' + Date.now(),
            headers: { 'Cache-Control': 'no-cache' },
            onload: resp => { const updateAlertWidth = 489

                // Compare 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
                        const updateModalID = siteAlert(( msgs.alert_updateAvail || 'Update available' ) + '! 🚀', // title
                            `${ msgs.alert_newerVer || 'An update to' } ${ config.appName } `
                                + `(v${ latestVer }) ${ msgs.alert_isAvail || 'is available' }!  `
                                + '<a target="_blank" rel="noopener" style="font-size: 0.93rem" '
                                    + 'href="' + config.gitHubURL + '/commits/main/greasemonkey/'
                                    + config.updateURL.replace(/.*\/(.*)meta\.js/, '$1user.js') + '"'
                                    + `>${ msgs.link_viewChanges || 'View changes' }</a>`,
                            function update() { // button
                                safeWindowOpen(config.updateURL.replace('meta.js', 'user.js') + '?t=' + Date.now())
                            }, '', updateAlertWidth
                        )
                        const updateModal = document.getElementById(updateModalID).firstChild

                        // Localize button labels if needed
                        if (!config.userLanguage.startsWith('en')) {
                            const updateAlert = document.querySelector(`[id="${ updateModalID }"]`),
                                  updateBtns = updateAlert.querySelectorAll('button')
                            updateBtns[1].textContent = msgs.buttonLabel_update || 'Update'
                            updateBtns[0].textContent = msgs.buttonLabel_dismiss || 'Dismiss'
                        }

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

                        return
                }}

                // Alert to no update found, nav back
                const noUpdateModalID = siteAlert(( msgs.alert_upToDate || 'Up-to-date' ) + '!', // title
                    `${ config.appName } (v${ currentVer }) ${ msgs.alert_isUpToDate || 'is up-to-date' }!`, // 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 FEEDBACK functions

    function notify(msg, position = '', notifDuration = '', shadow = '') {
        const notifIcon = icons.braveGPT.create() ; notifIcon.width = 29
        notifIcon.style.cssText = 'position: relative ; top: 4.8px ; margin-right: 6px'
        chatgpt.notify(msg, position, notifDuration, shadow || scheme == 'dark' ? '' : 'shadow')
        const notifs = document.querySelectorAll('.chatgpt-notif')
        notifs[notifs.length -1].prepend(notifIcon)
    }

    function siteAlert(title = '', msg = '', btns = '', checkbox = '', width = '') {
        return chatgpt.alert(`${ config.appSymbol } ${ title }`, msg, btns, checkbox, width)}

    function appAlert(...alerts) {
        alerts = alerts.flat() // flatten array args nested by spread operator
        while (appDiv.firstChild) appDiv.removeChild(appDiv.firstChild) // clear appDiv content
        const alertP = document.createElement('p') ; alertP.id = 'bravegpt-alert'
        alertP.className = 'no-user-select' ; alertP.style.marginBottom = '-22px'

        alerts.forEach((alert, idx) => { // process each alert for display
            let msg = appAlerts[alert] || alert // use string verbatim if not found in appAlerts
            if (idx > 0) msg = ' ' + msg // left-pad 2nd+ alerts
            if (msg.includes(appAlerts.login)) deleteOpenAIcookies()
            if (msg.includes(appAlerts.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>,'
                     + ` ${ msgs.alert_thenRefreshPage || 'then refresh this page' }.`
                     + ` (${ msgs.alert_ifIssuePersists || 'If issue persists' },`
                     + ` ${( msgs.alert_try || 'Try' ).toLowerCase() }`
                     + ` ${ msgs.alert_switchingOn || 'switching on' }`
                     + ` ${ msgs.mode_proxy || 'Proxy Mode' })`

            // Hyperlink msgs.alert_switching<On|Off>
            const foundState = ['On', 'Off'].find(state =>
                msg.includes(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 = 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 consoleInfo(msg) { console.info(`${ config.appSymbol } ${ config.appName } » ${ msg }`) }
    function consoleErr(label, msg) { console.error(`${config.appSymbol} ${config.appName} » ${label}${ msg ? `: ${msg}` : '' }`)}

    // Define MODAL functions

    const modals = {

        clickHandler(event) { // to dismiss modals
            if (event.target == event.currentTarget || event.target instanceof SVGPathElement)
                modals.hide(document.querySelector('[class$="-modal"]'))
        },

        dragHandlers: {
            mousedown(event) { // find modal, attach listeners, init XY offsets
                if (getComputedStyle(event.target).cursor == 'pointer') return // don't activate drag when clicking on interactive elems
                modals.dragHandlers.draggableElem = event.target.closest('[class$="-modal"]')
                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
                modals.dragHandlers.offsetY = event.clientY - draggableElemRect.top
            },

            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
                ['mousemove', 'mouseup'].forEach(event => document.removeEventListener(event, modals.dragHandlers[event]))
                modals.dragHandlers.draggableElem = null
            }
        },

        hide(modal) {
            const modalContainer = modal?.parentNode
            if (!modalContainer) return
            modalContainer.style.animation = 'alert-zoom-fade-out .135s ease-out'
            setTimeout(() => modalContainer.remove(), 105) // delay for fade-out
        },

        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, ${ scheme === 'dark' ? 0.62 : 0.33 })`
                modal.parentNode.classList.add('animated')
            }, 100) // delay for transition fx

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

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

        about: {
            show() {
                const settingsModal = modals.settings.get()
                if (settingsModal) modals.hide(settingsModal)

                // Create/init modal
                const chatgptJSver = (/chatgpt-([\d.]+)\.min/.exec(GM_info.script.header) || [null, ''])[1]
                const aboutModalID = chatgpt.alert('',
                    '🏷️ ' + ( msgs.about_version || 'Version' ) + ': ' + GM_info.script.version + '\n'
                        + '⚡ ' + ( msgs.about_poweredBy || 'Powered by' ) + ': '
                            + '<a href="https://chatgpt.js.org" target="_blank" rel="noopener">chatgpt.js</a>'
                            + ( chatgptJSver ? ( ' v' + chatgptJSver ) : '' ) + '\n'
                        + '📜 ' + ( msgs.about_sourceCode || 'Source code' )
                            + `: <a href="${ config.gitHubURL }" target="_blank" rel="nopener">`
                                + config.gitHubURL + '</a>',
                    [ // buttons
                        function checkForUpdates() { updateCheck() },
                        function getSupport() { safeWindowOpen(config.supportURL) },
                        function leaveAReview() { modals.feedback.show() },
                        function moreChatGPTapps() { safeWindowOpen('https://github.com/adamlui/chatgpt-apps') }
                    ], '', 617) // modal width
                const aboutModal = document.getElementById(aboutModalID).firstChild

                // Add logo
                const aboutHeaderLogo = logos.braveGPT.create()
                aboutHeaderLogo.width = 375 ; aboutHeaderLogo.style.margin = '-19px 16% 0'
                aboutModal.insertBefore(aboutHeaderLogo, aboutModal.firstChild.nextSibling) // after close btn

                // 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 = (
                        '🚀 ' + ( msgs.buttonLabel_updateCheck || 'Check for Updates' ))
                    else if (/support/i.test(btn.textContent)) btn.textContent = (
                        '🧠 ' + ( msgs.buttonLabel_getSupport || 'Get Support' ))
                    else if (/review/i.test(btn.textContent)) btn.textContent = (
                        '⭐ ' + ( msgs.buttonLabel_leaveReview || 'Leave a Review' ))
                    else if (/apps/i.test(btn.textContent)) btn.textContent = (
                        '🤖 ' + ( msgs.buttonLabel_moreApps || 'More ChatGPT Apps' ))
                    else btn.style.display = 'none' // hide Dismiss button
                })

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

        feedback: {
            show() {

                // Create/init modal
                const feedbackModalID = siteAlert(`${
                    msgs.alert_choosePlatform || 'Choose a platform' }:`, '',
                    [ // buttons
                        function greasyFork() { safeWindowOpen(
                            config.greasyForkURL + '/feedback#post-discussion') },
                        function github() { safeWindowOpen(
                            config.gitHubURL + '/discussions/new/choose') },
                        function productHunt() { safeWindowOpen(
                            'https://www.producthunt.com/products/bravegpt/reviews/new') },
                        function futurepedia() { safeWindowOpen(
                            'https://www.futurepedia.io/tool/bravegpt#tool-reviews') },
                        function alternativeTo() { safeWindowOpen(
                            'https://alternativeto.net/software/bravegpt/about/') }
                    ], '', 456) // modal width
                const feedbackModal = document.getElementById(feedbackModalID).firstChild

                // Re-style button cluster
                const btnsDiv = feedbackModal.querySelector('.modal-buttons')
                btnsDiv.style.cssText += 'display: flex ; flex-wrap: wrap ; justify-content: center ;'

                // Format button labels + add v-padding
                const btns = btnsDiv.querySelectorAll('button'), lastIdx = btns.length -1
                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 == lastIdx) 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 btn
            }
        },

        scheme: {
            show() {

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

                // Center button cluster
                schemeModal.querySelector('.modal-buttons').style.justifyContent = 'center'

                // 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
                            msgs['scheme_' + btnScheme] || 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
                        saveSetting('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

                function schemeNotify(scheme) {
                    notify(` ${ msgs.menuLabel_colorScheme || 'Color Scheme' }: `
                           + ( scheme == 'light' ? msgs.scheme_light   || 'Light' :
                               scheme == 'dark'  ? msgs.scheme_dark    || 'Dark'
                                                 : msgs.menuLabel_auto || 'Auto' ).toUpperCase()
                )}
            }
        },

        settings: {

            createAppend() {

                // Init core elems
                const settingsContainer = document.createElement('div')
                const settingsModal = document.createElement('div') ; settingsModal.id = 'bravegpt-settings'
                settingsContainer.append(settingsModal)
                modals.init(settingsModal) // add classes/stars, disable wheel-scrolling, dim bg
                const settingsIcon = icons.braveGPT.create()
                settingsIcon.style.cssText = 'width: 59px ; position: relative ; top: -33px ; margin: 0 41% -8px' // size/pos icon
                const settingsTitleDiv = document.createElement('div') ; settingsTitleDiv.id = 'bravegpt-settings-title'
                const settingsTitleH4 = document.createElement('h4') ; settingsTitleH4.textContent = msgs.menuLabel_settings || '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)
                const settingsList = document.createElement('ul')

                // Create/append setting icons/labels/toggles
                Object.keys(settingsProps).forEach((key, idx) => {
                    const setting = settingsProps[key]
                    if (isMobile && setting.mobile == false) return

                    // 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) ; settingsList.append(settingItem)

                    // Create/prepend icons
                    let settingIcon
                    if (key == 'proxyAPIenabled') {
                        settingIcon = icons.sunglasses.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -0.5px ; margin-right: 9px'
                    } else if (key == 'streamingDisabled') {
                        settingIcon = icons.signalStream.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: 0.5px ; margin-right: 9px'
                    } else if (key.includes('autoGet')) {
                        settingIcon = icons.autoSpeechBalloon.create()
                        settingIcon.style.cssText = 'position: relative ; top: 4.5px ; margin-right: 7px'
                    } else if (key == 'autoFocusChatbarDisabled') {
                        settingIcon = icons.caretsInward.create()
                        settingIcon.style.cssText = 'position: relative ; top: 4.5px ; margin-right: 7px'
                    } else if (key == 'autoScroll') {
                        settingIcon = icons.arrowsDown.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3.5px ; left: -1.5px ; margin-right: 6px'
                    } else if (key == 'rqDisabled') {
                        settingIcon = icons.speechBalloon.create()
                        settingIcon.style.cssText = 'position: relative ; top: 2.5px ; left: 0.5px ; margin-right: 9px ; transform: scaleY(-1)'
                    } else if (key == 'prefixEnabled') {
                        settingIcon = icons.slash.create()
                        settingIcon.style.cssText = 'position: relative ; top: 2.5px ; left: 0.5px ; margin-right: 9px'
                    } else if (key == 'suffixEnabled') {
                        settingIcon = icons.questionMark.create()
                        settingIcon.style.cssText = 'position: relative ; top: 4px ; left: -1.5px ; margin-right: 7px'
                    } else if (key == 'widerSidebar') {
                        settingIcon = icons.widescreen.create()
                        settingIcon.style.cssText = 'position: relative ; top: 4px ; left: -1.5px ; margin-right: 7.5px'
                    } else if (key == 'stickySidebar') {
                        settingIcon = icons.pin.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 7.5px'
                    } else if (key.includes('bgAnimation')) {
                        settingIcon = icons.sparkles.create('bg')
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 6.5px'
                    } else if (key.includes('fgAnimation')) {
                        settingIcon = icons.sparkles.create('fg')
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 6.5px'
                    } else if (key == 'replyLanguage') {
                        settingIcon = icons.language.create()
                        settingIcon.style.cssText = 'position: relative ; top: 3px ; left: -1.5px ; margin-right: 9px'
                    } else if (key == 'scheme') {
                        settingIcon = icons.scheme.create()
                        settingIcon.style.cssText = 'position: relative ; top: 2.5px ; left: -1.5px ; margin-right: 8px'
                    } else if (key == 'about') {
                        settingIcon = icons.about.create()
                        settingIcon.style.cssText = 'position: relative ; 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')
                        settingToggle.style.display = 'none' // hide checkbox

                        // Create/stylize switch
                        const switchSpan = document.createElement('span')
                        const switchStyles = {
                            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'
                        }
                        Object.assign(switchSpan.style, switchStyles)

                        // Create/stylize knob
                        const knobSpan = document.createElement('span')
                        const knobWidth = 11
                        const knobStyles = {
                            position: 'absolute', left: '1px', bottom: '1px',
                            width: `${ knobWidth }px`, height: `${ knobWidth }px`, 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'
                        }
                        Object.assign(knobSpan.style, knobStyles)

                        // 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 = () => {
                            modals.settings.toggle.switch(settingToggle) // visually switch toggle

                            // Call specialized toggle funcs
                            if (key.includes('proxy')) toggle.proxyMode()
                            else if (key.includes('streaming')) toggle.streaming()
                            else if (key.includes('rq')) toggle.relatedQueries()
                            else if (key.includes('Sidebar')) toggle.sidebar(key.match(/(.*?)Sidebar$/)[1])
                            else if (key.includes('bgAnimation')) toggle.animations('bg')
                            else if (key.includes('fgAnimation')) toggle.animations('fg')

                            // ...or generically toggle/notify
                            else {
                                saveSetting(key, !config[key]) // update config
                                notify(`${settingsProps[key].label} ${menuState.word[+key.includes('Disabled') ^ +config[key]]}`)
                            }
                        }

                    // Add config status + listeners to pop-up settings
                    } else {
                        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 = promptReplyLang
                        } 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 = `Version: <span class="about-em">v${ GM_info.script.version + textGap }</span>`
                                + `${ msgs.about_poweredBy || 'Powered by' } <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)
                    }
                })

                // Create close button
                const closeBtn = document.createElement('div') ; closeBtn.id = 'bravegpt-settings-close-btn'
                closeBtn.title = msgs.tooltip_close || 'Close'
                const closeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
                const closeSVGattrs = [['height', '8px'], ['viewBox', '0 0 14 14'], 'fill', 'none']
                closeSVGattrs.forEach(([attr, val]) => closeSVG.setAttribute(attr, val))
                const closeSVGpath = createSVGelem('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' })
                closeSVG.append(closeSVGpath) ; closeBtn.append(closeSVG)

                // Assemble/append elems
                settingsModal.append(settingsIcon, settingsTitleDiv, closeBtn, settingsList)
                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() {
                const settingsContainer = modals.settings.get()?.parentNode || modals.settings.createAppend()
                settingsContainer.style.display = '' // show modal
                if (isMobile) { // scale 93% to viewport sides
                    const settingsModal = settingsContainer.querySelector('#bravegpt-settings'),
                          scaleRatio = 0.93 * window.innerWidth / settingsModal.offsetWidth
                    settingsModal.style.transform = `scale(${scaleRatio})`
                }
            },

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

                updateStyles(settingToggle) { // for .toggle.show() + staggered switch animations in .createAppend()
                    const switchSpan = settingToggle.parentNode.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)'
                    }, 1) // min delay to trigger transition fx
                }
            },

            updateSchemeStatus(schemeStatusSpan = null) {
                schemeStatusSpan = schemeStatusSpan || document.querySelector('#scheme-menu-entry span')
                if (schemeStatusSpan) {
                    while (schemeStatusSpan.firstChild) schemeStatusSpan.removeChild(schemeStatusSpan.firstChild) // clear old status
                    schemeStatusSpan.append(...( // status txt + icon
                        config.scheme == 'dark' ? [document.createTextNode(msgs.scheme_dark || 'Dark'), icons.moon.create()]
                      : config.scheme == 'light' ? [document.createTextNode(msgs.scheme_light || 'Light'), icons.sun.create()]
                      : [document.createTextNode(msgs.menuLabel_auto || 'Auto'), icons.arrowsCycle.create()]
                ))}
            }
        }
    }

    // Define ICON functions

    const icons = {

        about: {
            create() {
                const aboutSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      aboutSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 56.693 56.693']]
                aboutSVGattrs.forEach(([attr, value]) => aboutSVG.setAttribute(attr, value))
                aboutSVG.append(createSVGelem('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' }
                ))
                return aboutSVG
            }
        },

        arrowUp: {
            create() {
                const arrowUpSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      arrowUpSVGattrs = [['width', 16], ['height', 16], ['viewBox', '4 2 16 16'],
                                         ['stroke-width', '2'], ['stroke-linecap', 'round'], ['stroke-linejoin', 'round']]
                arrowUpSVGattrs.forEach(([attr, value]) => arrowUpSVG.setAttribute(attr, value))
                arrowUpSVG.append(createSVGelem('path', { stroke: '', fill: 'none', 'stroke-width': '2', linecap: 'round', 'stroke-linejoin': 'round',
                    d: 'M7 11L12 6L17 11M12 18V7' }))
                return arrowUpSVG
            }
        },

        arrowsCycle: {
            create() {
                const arrowsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      arrowsSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 -1020 960 960']]
                arrowsSVGattrs.forEach(([attr, value]) => arrowsSVG.setAttribute(attr, value))
                arrowsSVG.append(createSVGelem('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' }))
                return arrowsSVG
            }
        },

        arrowsDown: {
            create() {
                const arrowsDownSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      arrowsDownSVGattrs = [['width', 19], ['height', 19], ['viewBox', '0 0 24 24']]
                arrowsDownSVGattrs.forEach(([attr, value]) => arrowsDownSVG.setAttribute(attr, value))
                arrowsDownSVG.append(
                    createSVGelem('path', { stroke: 'none', d: 'M18,13H6a1,1,0,0,1,0-2H18a1,1,0,0,1,0,2Z' }),
                    createSVGelem('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 arrowsDownSVG
            }
        },

        arrowsTwistedRight: {
            create() {
                const arrowsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      arrowsSVGattrs = [['width', 21], ['height', 21], ['viewBox', '-1 -1 32 32']]
                arrowsSVGattrs.forEach(([attr, value]) => arrowsSVG.setAttribute(attr, value))
                arrowsSVG.append(createSVGelem('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' }))
                return arrowsSVG
            }
        },

        autoSpeechBalloon: {
            create() {
                const autoSpeechBalloonSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      autoSpeechBalloonSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 -960 960 960']]
                autoSpeechBalloonSVGattrs.forEach(([attr, value]) => autoSpeechBalloonSVG.setAttribute(attr, value))
                autoSpeechBalloonSVG.append(createSVGelem('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' }))
                return autoSpeechBalloonSVG
            }
        },

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

        caretsInward: {
            create() {
                const caretsSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      caretsSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 24 24']]
                caretsSVGattrs.forEach(([attr, value]) => caretsSVG.setAttribute(attr, value))
                caretsSVG.append(createSVGelem('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' }))
                return caretsSVG
            }

        },

        fontSize: {
            create() {
                const fontSizeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      fontSizeSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 512 512']]
                fontSizeSVGattrs.forEach(([attr, value]) => fontSizeSVG.setAttribute(attr, value))
                fontSizeSVG.append(
                    createSVGelem('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' }),
                    createSVGelem('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 fontSizeSVG
            }
        },

        language: {
            create() {
                const languageSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      languageSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 -960 960 960']]
                languageSVGattrs.forEach(([attr, value]) => languageSVG.setAttribute(attr, value))
                languageSVG.append(createSVGelem('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' })                )
                return languageSVG                
            }
        },

        moon: {
            create() {
                const moonSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      moonSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 24 24']]
                moonSVGattrs.forEach(([attr, value]) => moonSVG.setAttribute(attr, value))
                moonSVG.append(createSVGelem('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' }))
                return moonSVG
            }
        },

        pin: {
            filledSVGpath() { return createSVGelem('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'
            })},

            hollowSVGpath() { return createSVGelem('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-.146zm.122 2.112v-.002.002zm0-.002v.002a.5.5 0 0 1-.122.51L6.293 6.878a.5.5 0 0 1-.511.12H5.78l-.014-.004a4.507 4.507 0 0 0-.288-.076 4.922 4.922 0 0 0-.765-.116c-.422-.028-.836.008-1.175.15l5.51 5.509c.141-.34.177-.753.149-1.175a4.924 4.924 0 0 0-.192-1.054l-.004-.013v-.001a.5.5 0 0 1 .12-.512l3.536-3.535a.5.5 0 0 1 .532-.115l.096.022c.087.017.208.034.344.034.114 0 .23-.011.343-.04L9.927 2.028c-.029.113-.04.23-.04.343a1.779 1.779 0 0 0 .062.46z'
            })},

            create() {
                const pinSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      pinSVGattrs = [['id', 'pin-icon'], ['width', 17], ['height', 17], ['viewBox', '0 0 16 16']]
                pinSVGattrs.forEach(([attr, value]) => pinSVG.setAttribute(attr, value))
                icons.pin.update(pinSVG)
                return pinSVG
            },

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

        questionMark: {
            create() {
                const questionMarkSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      questionMarkSVGattrs = [['width', 18], ['height', 18], ['viewBox', '0 -960 960 960']]
                questionMarkSVGattrs.forEach(([attr, value]) => questionMarkSVG.setAttribute(attr, value))
                questionMarkSVG.append(createSVGelem('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' }))
                return questionMarkSVG
            }
        },
        
        scheme: {
            create() {
                const schemeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      schemeSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 -960 960 960']]
                schemeSVGattrs.forEach(([attr, value]) => schemeSVG.setAttribute(attr, value))
                schemeSVG.append(createSVGelem('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' }))
                return schemeSVG
            }
        },
        
        signalStream: {
            create() {
                const signalStreamSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      signalStreamSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 0 32 32']]
                signalStreamSVGattrs.forEach(([attr, value]) => signalStreamSVG.setAttribute(attr, value))
                signalStreamSVG.append(createSVGelem('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' }))
                return signalStreamSVG
            }
        },
        
        slash: {
            create() {
                const slashSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      slashSVGattrs = [['width', 15], ['height', 15], ['viewBox', '0 0 15 15']]
                slashSVGattrs.forEach(([attr, value]) => slashSVG.setAttribute(attr, value))
                slashSVG.append(createSVGelem('path', { stroke: '', d: 'M4.10876 14L9.46582 1H10.8178L5.46074 14H4.10876Z' }))
                return slashSVG
            }
        },
        
        sliders: {
            create() {
                const slidersSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      slidersSVGattrs = [['width', 20], ['height', 20], ['viewBox', '0 -960 960 960']]
                slidersSVGattrs.forEach(([attr, value]) => slidersSVG.setAttribute(attr, value))
                slidersSVG.append(createSVGelem('path', { stroke: 'none', d: 'M435.48-102.48V-360H533v80h320v97.52H533v80h-97.52Zm-328.48-80V-280h257.52v97.52H107Zm160-169.04v-80H107v-96.96h160v-80h97.52v256.96H267Zm168.48-80v-96.96H853v96.96H435.48Zm160-168.48v-257.52H693v80h160V-680H693v80h-97.52ZM107-680v-97.52h417.52V-680H107Z' }))
                return slidersSVG
            }
        },
        
        sparkles: {
            create(style) { // style = ( 'fg' ? filled front sparkle : 'bg' ? filled rear sparkles )
                const sparklesSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      sparklesSVGattrs = [['width', 18], ['height', 18], ['viewBox', '0 0 512 512']]
                sparklesSVGattrs.forEach(([attr, value]) => sparklesSVG.setAttribute(attr, value))
                sparklesSVG.append(createSVGelem('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' }))
                sparklesSVG.append(createSVGelem('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' }))
                sparklesSVG.append(createSVGelem('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 sparklesSVG
            }
        },

        speaker: {
            create() {
                const speakerSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      speakerSVGattrs = [['width', 22], ['height', 22], ['viewBox', '0 0 32 32']]
                speakerSVGattrs.forEach(([attr, value]) => speakerSVG.setAttribute(attr, value))
                speakerSVG.append(
                    createSVGelem('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' }),
                    createSVGelem('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' }),
                    createSVGelem('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 speakerSVG
            }
        },

        speechBalloon: {
            create() {
                const speechBalloonSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      speechBalloonSVGattrs = [['width', 16], ['height', 16], ['viewBox', '0 -960 960 960']]
                speechBalloonSVGattrs.forEach(([attr, value]) => speechBalloonSVG.setAttribute(attr, value))
                speechBalloonSVG.append(createSVGelem('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' }))
                return speechBalloonSVG
            }
        },

        sun: {
            create() {
                const sunSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      sunSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 -960 960 960']]
                sunSVGattrs.forEach(([attr, value]) => sunSVG.setAttribute(attr, value))
                sunSVG.append(createSVGelem('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' }))
                return sunSVG
            }
        },

        sunglasses: {
            create() {
                const sunglassesSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      sunglassesSVGattrs = [['width', 17], ['height', 17], ['viewBox', '0 0 512 512']]
                sunglassesSVGattrs.forEach(([attr, value]) => sunglassesSVG.setAttribute(attr, value))
                sunglassesSVG.append(createSVGelem('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' }))
                return sunglassesSVG
            }
        },

        widescreen: {
            wideSVGpath() { return createSVGelem('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 createSVGelem('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 widescreenSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                      widescreenSVGattrs = [['id', 'widescreen-icon'], ['width', 18], ['height', 18], ['viewBox', '8 8 20 20']]
                widescreenSVGattrs.forEach(([attr, value]) => widescreenSVG.setAttribute(attr, value))
                icons.widescreen.update(widescreenSVG)
                return widescreenSVG
            },

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

    // Define LOGO functions

    const logos = {
        braveGPT: {

            create() {
                const braveGPTlogo = document.createElement('img') ; braveGPTlogo.id = 'bravegpt-logo'
                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${ scheme == 'dark' ? 'DS' : 'LS' }logo`))
            }
        }
    }

    // Define UPDATE functions

    const update = {

        appStyle() {
            appStyle.innerText = (
                '.no-user-select { -webkit-user-select: none ; -moz-user-select: none ; -ms-user-select: none ; user-select: none }'
              + ( // 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 { word-wrap: break-word ; white-space: pre-wrap ; margin-bottom: ${ isMobile ? -29 : 20}px ;`
                  + 'padding: 24px 23px 45px 23px ;'
                  + `background: radial-gradient(ellipse at bottom, ${ scheme == 'dark' ? '#2f3031 0%, #090a0f' : 'white 0%, white' } 100%) ;`
                  + `border: ${ scheme == 'dark' ? 'none' : '1px solid var(--color-divider-subtle)' } ; border-radius: 18px }`
              + '#bravegpt:hover { box-shadow: 0 9px 28px rgba(0, 0, 0, 0.09) }'
              + '#bravegpt p { margin: 0 }'
              + `#bravegpt .alert-link { color: ${ scheme == 'light' ? '#190cb0' : 'white ; text-decoration: underline' }}`
              + ( scheme == 'dark' ? '#bravegpt a { text-decoration: underline }' : '' ) // underline dark-mode links in alerts
              + '.app-name { font-size: 20px ; font-family: var(--brand-font) ; text-decoration: none ;'
                  + `color: ${ 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: ${ scheme == 'dark' ? 'white' : 'black' }}`
              + '.corner-btn { float: right ; cursor: pointer ; position: relative ; top: 4px ; transition: transform 0.15s ease ;'
                  + ( scheme == 'dark' ? 'fill: white ; stroke: white;' : 'fill: #adadad ; stroke: #adadad' ) + '}'
              + `.corner-btn:hover { ${ scheme == 'dark' ? 'fill: #d9d9d9 ; stroke: #d9d9d9' : 'fill: black ; stroke: black' } ;`
                  + `${ config.fgAnimationsDisabled ? '' : 'transform: scale(1.285)' }}`
              + '#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-thumb { width: 10px ; height: 27px ; border-radius: 30% ; position: relative ; top: -9px ;'
                  + `transition: transform 0.05s ease ; background-color: ${ scheme == 'dark' ? 'white' : '#4a4a4a' } ;`
                  + 'box-shadow: rgba(0, 0, 0, 0.21) 1px 1px 9px 0px ; cursor: ew-resize }'
              + ( config.fgAnimationsDisabled ? '' : '#font-size-slider-thumb:hover { transform: scale(1.125) }' )
              + '.standby-btn { width: 100% ; padding: 13px 0 ; cursor: pointer ; margin: 14px 0 20px ;'
                  + `color: ${ scheme == 'dark' ? 'white' : 'black' } ;`
                  + `border-radius: 4px ; border: 1px solid ${ scheme == 'dark' ? '#fff' : '#000' } ;`
                  + 'transform: scale(1) ; transition: transform 0.1s ease }'
              + '.standby-btn:hover { border-radius: 4px ;'
                  + `${ scheme == 'dark' ? 'background: white ; color: black' : 'background: black ; color: white' };`
                  + `${ config.fgAnimationsDisabled ? '' : 'transform: scale(1.025)' }}`
              + '#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 ;'
                  + `${ scheme == 'dark' ? 'background: #2b3a40cf ; color: #f2f2f2 ; border: 1px solid white'
                                         : 'background: #eaeaeacf ; color: #282828 ; border: none' }}`
              + `#bravegpt footer { margin: ${ isFirefox ? 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: ${ scheme == 'dark' ? 'white' : 'black' } ; text-decoration: none }`
              + '@keyframes pulse { 0%, to { opacity: 1 } 50% { opacity: .5 }}'
              + '.balloon-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:'
                      + ( scheme == 'dark' ? '#0000' : '#eaeaeacf' ) + '}'
              + '.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 }'
              + '#app-chatbar {'
                  + `border: solid 1px ${ scheme == 'dark' ? '#aaa' : '#638ed4' } ; border-radius: 12px 15px 12px 0 ;`
                  + 'border-radius: 15px 16px 15px 0 ; margin: -6px 0 -7px 0 ;  padding: 12px 51px 12px 10px ;'
                  + 'height: 43px ; line-height: 17px ; width: 100% ; max-height: 200px ; resize: none ;'
                  + `background: ${ scheme == 'dark' ? '#5151519e' : '#eeeeee9e' };`
                  + `position: relative ; z-index: 555 ; color: ${ scheme == 'dark' ? '#eee' : '#222' }}`
              + '.related-queries { display: flex ; flex-wrap: wrap ; width: 100% ; margin-bottom: -18px ;'
                  + 'position: relative ; top: -3px ;' // scooch up to hug feedback gap
                  + `${ isFirefox ? '' : 'margin-top: -31px' }}`
              + '.related-query { margin: 4px 4px 2px 0 ; padding: 8px 13px 7px 14px ;'
                  + `color: ${ scheme == 'dark' ? '#f2f2f2' : '#767676' } ;`
                  + `background: ${ scheme == 'dark' ? '#595858d6' : '#fbfbfbb0' } ;`
                  + `border: 1px solid ${ scheme == 'dark' ? '#777' : '#e1e1e1' } ; font-size: 0.77em ; cursor: pointer ;`
                  + 'border-radius: 0 13px 12px 13px ; width: fit-content ; flex: 0 0 auto ;'
                  + `box-shadow: 1px 3px ${ scheme == 'dark' ? '11px -8px lightgray' : '8px -6px rgba(169, 169, 169, 0.75)' };`
                  + `${ config.fgAnimationsDisabled ? '' : 'transform: scale(1) ; transition: transform 0.1s ease !important' }}`
              + '.related-query:hover, .related-query:focus {'
                  + ( config.fgAnimationsDisabled ? '' : 'transform: scale(1.025) !important ;' )
                  + `background: ${ scheme == 'dark' ? '#a2a2a270': '#dae5ffa3 ; color: #000000a8 ; border-color: #a3c9ff' }}`
              + '.related-query svg { float: left ; margin: 0.09em 6px 0 0 ;' // related query icon
                  + `color: ${ scheme == 'dark' ? '#aaa' : '#c1c1c1' }}`
              + '.fade-in { opacity: 0 ; transform: translateY(10px) ; transition: opacity 0.5s ease, transform 0.5s ease }'
              + '.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 ; bottom: ${ isFirefox ? 28 : 32 }px ; background: none ; cursor: pointer ;`
                  + `${ scheme == 'dark' ? 'color: #aaa ; fill: #aaa ; stroke: #aaa' : 'color: lightgrey ; fill: lightgrey ; stroke: lightgrey' }}`
              + '.chatbar-btn:hover {'
                  + `${ 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 { font-size: 25px !important }' // shrink notifications
              + '.chatgpt-modal > div { padding: 24px 20px 14px 20px !important ;' // increase modal padding
                  + 'background-color: white !important ; color: #202124 }'
              + '.chatgpt-modal h2 { font-size: 26px ; margin: 0 ; padding: 0 }' // shrink margin/padding around alert title + shrink it
              + '.modal-close-btn { top: -7px !important ; right: -7px !important }' // re-pos modal close button
              + `.modal-close-btn path {${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: black ; fill: black' }}`
              + `.modal-close-btn:hover { background-color: #${ scheme == 'dark' ? '666464' : 'f2f2f2' } !important }`
              + '.chatgpt-modal p { margin: 14px 0 -20px 4px ; font-size: 18px }' // pos/size modal msg
              + `.chatgpt-modal a { color: #${ scheme == 'dark' ? '00cfff' : '1e9ebb' } !important }`
              + `.modal-buttons { margin: 38px 0 1px ${ isMobile ? 0 : -7 }px !important }` // pos modal buttons
              + '.chatgpt-modal button {' // alert buttons
                  + 'font-size: 14px ; text-transform: uppercase ; min-width: 123px ; '
                  + `padding: ${ isMobile? '5px' : '4px 3px' } !important ;`
                  + 'cursor: pointer ; border-radius: 0 !important ; height: 39px ;'
                  + 'border: 1px solid ' + ( 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 ;'
                  + `box-shadow: ${ scheme == 'dark' ? '2px 1px 54px #00cfff' : '2px 1px 30px #9cdaff' } !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 { opacity: 0.98 ; transform: translateX(0) translateY(0) }'
            + '[class$="modal"] {' // native modals + chatgpt.alert()s
                + 'position: absolute ;' // to be click-draggable
                + 'opacity: 0 ;' // to fade-in
                + 'background-image: linear-gradient(180deg,' // bg
                    + `${ scheme == 'dark' ? '#99a8a6 -70%, black 57%' : '#b6ebff -64%, white 33%' }) ;`
                + `border: 1px solid ${ scheme == 'dark' ? 'white' : '#b5b5b5' } !important ;`
                + `color: ${ scheme == 'dark' ? 'white' : 'black' } ;`
                + 'transform: translateX(-3px) translateY(7px) ;' // offset to move-in from
                + 'transition: opacity 0.35s cubic-bezier(.165,.84,.44,1),' // for fade-ins
                            + 'transform 0.35s cubic-bezier(.165,.84,.44,1) !important }' // for move-ins
              + ( scheme == 'dark' ? // additional darkmode modal 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 button:hover { background-color: #00cfff !important ; color: black !important }' ) : '' )

              // 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 0px 0px 0.5em 0px var(--glow-color), 0px 0px 0.5em 0px var(--glow-color) ;' 
                  + 'box-shadow: inset 0px 0px 0.5em 0px var(--glow-color), 0px 0px 0.5em 0px var(--glow-color) ;' 
                  + '-moz-box-shadow: inset 0px 0px 0.5em 0px var(--glow-color), 0px 0px 0.5em 0px 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 }'
              + '.glowing-btn::before { content: "" ; position: absolute ; top: 0 ; bottom: 0 ; left: 0 ; right: 0 ;'
                  + 'opacity: 0.7 ; filter: blur(1em) ; transform: translateY(120%) rotateX(95deg) scale(1, 0.35) ;'
                  + 'background: var(--glow-color) ; pointer-events: none }'
              + '.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 }}'

              // Settings modal
              + '#bravegpt-settings { font-family: var(--brand-font) ;'
                  + 'min-width: 288px ; max-width: 75vw ; word-wrap: break-word ;'
                  + 'padding: 11px ; margin: 12px 23px ; border-radius: 15px ; box-shadow: 0 30px 60px rgba(0, 0, 0, .12) ;'
                  + `${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: black ; fill: black' }}` // icon color
              + '@keyframes alert-zoom-fade-out { 0% { opacity: 1 ; transform: scale(1) }'
                  + '50% { opacity: 0.25 ; transform: scale(1.05) }'
                  + '100% { opacity: 0 ; transform: scale(1.35) }}'
              + '#bravegpt-settings-title { font-weight: bold ; line-height: 19px ; text-align: center ; margin: 0 -6px -3px 0 }'
              + '#bravegpt-settings-title h4 { font-size: 26px ; font-weight: bold ; margin-top: -31px }' // 'Settings'
              + '#bravegpt-settings-close-btn {'
                  + 'cursor: pointer ; width: 20px ; height: 20px ; border-radius: 17px ; float: right ;'
                  + 'position: absolute ; top: 10px ; right: 13px }'
              + `#bravegpt-settings-close-btn path {${ scheme == 'dark' ? 'stroke: white ; fill: white' : 'stroke: #9f9f9f ; fill: #9f9f9f' }}`
              + '#bravegpt-settings-close-btn svg { margin: 6.5px }' // center SVG for hover underlay
              + `#bravegpt-settings-close-btn:hover { background-color: #f2f2f2${ scheme == 'dark' ? '00' : '' }}`
              + '#bravegpt-settings ul { list-style: none ; margin: 0 }' // hide bullets, override Brave ul margins
              + '#bravegpt-settings li { font-size: 14.5px ; transition: transform 0.1s ease ;'
                  + `padding: 7px 10px ; border-bottom: 1px dotted ${ scheme == 'dark' ? 'white' : 'black' } ;` // add settings separators
                  + 'border-radius: 3px }' // make highlight strips slightly rounded
              + '#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 {'
                  + 'background: rgba(100, 149, 237, 0.88) ; color: white ; fill: white ; stroke: white ;' // add highlight strip
                  + `${ config.fgAnimationsDisabled || isMobile ? '' : 'transform: scale(1.16)' }}` // add zoom
              + '#bravegpt-settings li > input { float: right }' // pos toggles
              + '#scheme-menu-entry > span { margin: 0 -2px !important }' // align Scheme status
              + '#scheme-menu-entry > span > svg { position: relative ; top: 3px ; margin-left: 3px }' // v-align/left-pad Scheme status icon
              + `#about-menu-entry span { color: ${ scheme == 'dark' ? '#28ee28' : 'green' }}`
              + '#about-menu-entry > span { width: 92px ; overflow: hidden ;' // outer About status span
                  + `${ config.fgAnimationsDisabled ? '' : '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: ${ scheme == 'dark' ? 'white' : 'green' } !important }`
            )
        },

        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(config.appName.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', config.appName.toLowerCase())
                                        queryParams.set('utm_content', 'app_footer_link')
                                        destinationURL = baseURL + '?' + queryParams.toString()
                                    }

                                    // Update footer content
                                    const newFooterContent = destinationURL ? createAnchor(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, 10) / 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
            }
        },

        scheme(newScheme) {
            scheme = newScheme ; logos.braveGPT.update() ; update.appStyle() ; update.stars() ; toggle.btnGlow() ; 
            modals.settings.updateSchemeStatus()
        },

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

        tooltip(buttonType) { // text & position
            const cornerBtnTypes = ['about', 'settings', 'speak', 'pin', 'font-size', 'wsb']
                      .filter(type => { // exclude invisible ones                                                
                          const btn = appDiv.querySelector(`#${type}-btn`)
                          return btn && getComputedStyle(btn).display != 'none' })
            const [ctrAddend, spreadFactor] = appDiv.querySelector('.standby-btn') ? [9, 25] : [5, 28],
                  iniRoffset = spreadFactor * ( buttonType == 'send' ? 1.35
                                              : buttonType == 'shuffle' ? 2.35
                                              : cornerBtnTypes.indexOf(buttonType) +1 ) + ctrAddend
            // Update text
            tooltipDiv.innerText = (
                buttonType == 'about' ? msgs.menuLabel_about || 'About'
              : buttonType == 'settings' ? msgs.menuLabel_settings || 'Settings'
              : buttonType == 'speak' ? msgs.tooltip_playAnswer || 'Play answer'
              : buttonType == 'pin' ? (( config.stickySidebar ? `${ msgs.prefix_exit || 'Exit' } ` :  '' )
                                       + ( msgs.menuLabel_stickySidebar || 'Sticky Sidebar' ))
              : buttonType == 'font-size' ? msgs.tooltip_fontSize || 'Font size'
              : buttonType == 'wsb' ? (( config.widerSidebar ? `${ msgs.prefix_exit || 'Exit' } ` :  '' )
                                       + ( msgs.menuLabel_widerSidebar || 'Wider Sidebar' ))
              : buttonType == 'send' ? msgs.tooltip_sendReply || 'Send reply'
              : buttonType == 'shuffle' ? msgs.tooltip_askRandQuestion || 'Ask random question' : '' )

            // Update position
            tooltipDiv.style.top = `${ !/send|shuffle/.test(buttonType) ? -6
              : tooltipDiv.eventYpos - appDiv.getBoundingClientRect().top - 34 }px`
            tooltipDiv.style.right = `${ iniRoffset - tooltipDiv.getBoundingClientRect().width / 2 }px`
        },

        tweaksStyle() {

            // Update tweaks style based on settings (for tweaks init + show.reply() + toggle.sidebar())
            const isStandbyMode = appDiv.querySelector('.standby-btn'),
                  answerIsLoaded = appDiv.querySelector('.corner-btn')
            tweaksStyle.innerText = ( config.widerSidebar ? wsbStyles : '' )
                                  + ( config.stickySidebar && !isStandbyMode && answerIsLoaded ? ssbStyles : '' )

            // Update 'by KudoAI' visibility based on corner space available
            const kudoAIspan = appDiv.querySelector('.kudoai')
            if (kudoAIspan) kudoAIspan.style.display = (
                appDiv.querySelectorAll('.corner-btn').length < ( config.widerSidebar ? 10 : 5 )) ? '' : 'none'
    
            // Update <pre> max-height in Sticky Sidebar mode based on RQ visibility (for get.reply()'s RQ show + menu RQ toggle)
            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 ? 'none' : (
                relatedQueries?.offsetHeight > 0 ? `${ shorterPreHeight }px` : `${ longerPreHeight }px` )
        }
    }

    // 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 ; height: 100% ; width: 100% ; overflow: clip ;'
                                         + 'z-index: -1'; // allow interactive elems to be clicked
        ['sm', 'med', 'lg'].forEach((starSize, idx) => {
            const starsDiv = document.createElement('div')
            starsDiv.id = config.bgAnimationsDisabled ? `stars-${starSize}-off`
                        : `${ scheme == 'dark' ? 'white' : 'black' }-stars-${starSize}`
            starsDiv.style.height = `${ idx +1 }px` // so toggle.bgAnimations() doesn't change height
            starsDivsContainer.append(starsDiv)
        })
        targetNode.prepend(starsDivsContainer)
    }

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

        createAppend: function() {

            // Create/append 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'
            slider.append(sliderThumb)
            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 => {
                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.parentNode.removeChild(fontSizeSlider.cursorOverlay)
            })

            // Add event listener for wheel-scrolling thumb
            if (!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 => {
                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'
                saveSetting('fontSize', fontSize)
            }

            return slider            
        },

        toggle: function(state = '') {
            const slider = document.getElementById('font-size-slider-track') || fontSizeSlider.createAppend()

            // Toggle visibility
            const balloonTip = appDiv.querySelector('.balloon-tip')
            if (state == 'on' || (!state && slider.style.display == 'none')) {
                slider.style.display = '' ; balloonTip.style.display = 'none'
                setTimeout(() => slider.classList.add('active'), fontSizeSlider.fadeInDelay)
            } else if (state == 'off' || (!state && slider.style.display != 'none')) {
                slider.classList.remove('active') ; balloonTip.style.display = ''
                setTimeout(() => slider.style.display = 'none', 55)
            }
        }
    }

    function handleRQevent(event) { // for attachment/removal in `get.reply()` + `show.reply().handleSubmit()`
        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
            appDiv.querySelector('.related-queries').remove() // remove related queries

            // Send related query
            const chatbar = appDiv.querySelector('textarea')
            if (chatbar) {
                chatbar.value = event.target.textContent
                show.reply.submitSrc = 'click' // for show.reply()'s mobile scroll-to-top if user interacted
                chatbar.dispatchEvent(new KeyboardEvent('keydown', {
                    key: 'Enter', bubbles: true, cancelable: true }))
            }
    }}

    // Define FACTORY functions

    function createAnchor(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
    }

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

    function createSVGelem(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 TOGGLE functions

    const toggle = {

        animations(layer) {
            saveSetting(layer + 'AnimationsDisabled', !config[layer + 'AnimationsDisabled'])
            update[layer == 'bg' ? 'stars' : 'appStyle']()
            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 (scheme == 'dark') toggle.btnGlow()
            }
            notify(`${settingsProps[layer + 'AnimationsDisabled'].label} ${menuState.word[+!config[layer + 'AnimationsDisabled']]}`)
        },

        btnGlow(state = '') {
            const removeCondition = state == 'off' || 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')
            })
        },

        proxyMode() {
            saveSetting('proxyAPIenabled', !config.proxyAPIenabled)
            notify(( msgs.menuLabel_proxyAPImode || 'Proxy API Mode' ) + ' ' + menuState.word[+config.proxyAPIenabled])
            refreshMenu()
            if (modals.settings.get()) { // update visual state of Settings toggle
                const proxyToggle = document.querySelector('[id*="proxy"][id*="menu-entry"] input')
                if (proxyToggle.checked != config.proxyAPIenabled) modals.settings.toggle.switch(proxyToggle)
            }
            if (appDiv.querySelector('bravegpt-alert')) location.reload() // re-send query if user alerted 
        },

        relatedQueries() {
            saveSetting('rqDisabled', !config.rqDisabled)
            const relatedQueriesDiv = appDiv.querySelector('.related-queries')
            if (relatedQueriesDiv) // update visibility based on latest setting
                relatedQueriesDiv.style.display = config.rqDisabled ? 'none' : 'flex'
            if (!config.rqDisabled && !relatedQueriesDiv) { // get related queries for 1st time
                const lastQuery = stripQueryAugments(msgChain)[msgChain.length - 1].content
                get.related(lastQuery).then(queries => show.related(queries))
                    .catch(err => { consoleErr(err.message)
                        if (get.related.status != 'done') api.tryNew(get.related) })
            }
            update.tweaksStyle() // toggle <pre> max-height
            notify(( msgs.menuLabel_relatedQueries || 'Related Queries' ) + ' ' + menuState.word[+!config.rqDisabled])
        },

        sidebar(mode) {
            saveSetting(mode + 'Sidebar', !config[mode + 'Sidebar'])
            update.tweaksStyle() // apply mode to UI
            icons[mode == 'wider' ? 'widescreen' : 'pin'].update() // toggle icons everywhere
            notify(( msgs[`menuLabel_${ mode }Sidebar`] || mode.charAt(0).toUpperCase() + mode.slice(1) + ' Sidebar' )
                + ' ' + menuState.word[+config[mode + 'Sidebar']])
        },

        streaming() {
            const streamingToggle = document.querySelector('[id*="streaming"][id*="menu-entry"] input'),
                  scriptCatLink = isFirefox ? 'https://addons.mozilla.org/firefox/addon/scriptcat/'
                                : isEdge    ? 'https://microsoftedge.microsoft.com/addons/detail/scriptcat/liilgpjgabokdklappibcjfablkpcekh'
                                            : 'https://chromewebstore.google.com/detail/scriptcat/ndcooeababalnlpkfedmmbbbgkljhpjf'
            if (!/Tampermonkey|ScriptCat/.test(getUserscriptManager())) { // alert userscript manager unsupported, suggest TM/SC
                siteAlert(`${settingsProps.streamingDisabled.label} ${ msgs.alert_unavailable || 'unavailable' }`,
                    `${settingsProps.streamingDisabled.label} ${ msgs.alert_isOnlyAvailFor || 'is only available for' }`
                        + ( !isEdge && !isBrave ? // suggest TM for supported browsers
                            ` <a target="_blank" rel="noopener" href="https://tampermonkey.net">Tampermonkey</a> ${ msgs.alert_and || 'and' }`
                                : '' )
                        + ` <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a>.` // suggest SC
                        + ` (${ msgs.alert_userscriptMgrNoStream || 'Your userscript manager does not support returning stream responses' }.)`
                )
                if (streamingToggle && streamingToggle.checked == config.streamingDisabled) // revert Settings auto-toggle
                    modals.settings.toggle.switch(streamingToggle)
            } else if (getUserscriptManager() == 'Tampermonkey' && (isChrome || isEdge || isBrave)) { // alert TM/browser unsupported, suggest SC
                siteAlert(`${settingsProps.streamingDisabled.label} ${ msgs.alert_unavailable || 'unavailable' }`,
                    `${settingsProps.streamingDisabled.label} ${ msgs.alert_isUnsupportedIn || 'is unsupported in' } `
                        + `${ isChrome ? 'Chrome' : isEdge ? 'Edge' : 'Brave' } ${ msgs.alert_whenUsing || 'when using' } Tampermonkey. `
                        + `${ msgs.alert_pleaseUse || 'Please use' } <a target="_blank" rel="noopener" href="${scriptCatLink}">ScriptCat</a> `
                            + `${ msgs.alert_instead || 'instead' }.`
                )
                if (streamingToggle && streamingToggle.checked == config.streamingDisabled) // revert Settings auto-toggle
                    modals.settings.toggle.switch(streamingToggle)
            } else if (!config.proxyAPIenabled) { // alert OpenAI API unsupported, suggest Proxy Mode
                let msg = `${settingsProps.streamingDisabled.label} `
                        + `${ msgs.alert_isCurrentlyOnlyAvailBy || 'is currently only available by' } `
                        + `${ msgs.alert_switchingOn || 'switching on' } ${ msgs.mode_proxy || 'Proxy Mode' }. `
                        + `(${ msgs.alert_openAIsupportSoon || 'Support for OpenAI API will be added shortly' }!)`
                const switchPhrase = msgs.alert_switchingOn || 'switching on'
                msg = msg.replace(switchPhrase, `<a class="alert-link" href="#">${switchPhrase}</a>`)
                const alertID = siteAlert(`${ msgs.mode_streaming || 'Streaming Mode' } ${ msgs.alert_unavailable || 'unavailable' }`, msg),
                      alert = document.getElementById(alertID)
                alert.querySelector('[href="#"]').onclick = () => { alert.querySelector('.modal-close-btn').click() ; toggle.proxyMode() }
                if (streamingToggle && streamingToggle.checked == config.streamingDisabled) // revert Settings auto-toggle
                    modals.settings.toggle.switch(streamingToggle)
            } else { // functional toggle
                saveSetting('streamingDisabled', !config.streamingDisabled)
                notify(settingsProps.streamingDisabled.label + ' ' + menuState.word[+!config.streamingDisabled])
            }
        },

        tooltip(event) { // visibility
            tooltipDiv.eventYpos = event.currentTarget.getBoundingClientRect().top // for update.tooltip() y-pos calc
            update.tooltip(event.currentTarget.id.replace(/-btn$/, ''))
            tooltipDiv.style.opacity = event.type == 'mouseover' ? 1 : 0
        }
    }

    // Define SESSION functions

    function isBlockedbyCloudflare(resp) {
        try {
            const html = new DOMParser().parseFromString(resp, 'text/html'),
                  title = html.querySelector('title')
            return title.innerText == 'Just a moment...'
        } catch (err) { return false }
    }

    function deleteOpenAIcookies() {
        GM_deleteValue(config.keyPrefix + '_openAItoken')
        if (getUserscriptManager() != 'Tampermonkey') return
        GM_cookie.list({ url: openAIendpoints.auth }, (cookies, error) => {
            if (!error) { for (const cookie of cookies) {
                GM_cookie.delete({ url: openAIendpoints.auth, name: cookie.name })
    }}})}

    function getOpenAItoken() {
        return new Promise(resolve => {
            const accessToken = GM_getValue(config.keyPrefix + '_openAItoken')
            consoleInfo('OpenAI access token: ' + accessToken)
            if (!accessToken) {
                xhr({ url: openAIendpoints.session, onload: resp => {
                    if (isBlockedbyCloudflare(resp.responseText)) {
                        appAlert('checkCloudflare') ; return }
                    try {
                        const newAccessToken = JSON.parse(resp.responseText).accessToken
                        GM_setValue(config.keyPrefix + '_openAItoken', newAccessToken)
                        resolve(newAccessToken)
                    } catch { if (get.reply.api == 'OpenAI') appAlert('login') ; return }
                }})
            } else resolve(accessToken)
    })}

    function generateGPTforLoveKey() {
        let nn = Math.floor(new Date().getTime() / 1e3)
        const fD = e => {
            let t = CryptoJS.enc.Utf8.parse(e),
                o = CryptoJS.AES.encrypt(t, 'fjfsd我w4真3dd服iuhf了wf', {
                    mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7
            })
            return o.toString()
        }
        return fD(nn)
    }

    // Define API functions

    const api = {

        pick: function(caller) {
            const logPrefix = `get.${caller.name}() » `
            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) { consoleErr('No proxy APIs left untried') ; return null }

            // Log chosen API endpoint
            consoleInfo(logPrefix + 'Endpoint used: ' + apis[chosenAPI].endpoint)
            return chosenAPI
        },

        tryNew: function(caller, reason = 'err') {
            consoleErr(`Error using ${apis[caller.api].endpoint} due to ${reason}`)
            if (caller.attemptCnt < Object.keys(apis).length -+(caller == get.reply)) {
                consoleInfo('Trying another endpoint...')
                caller.triedAPIs.push({ [caller.api]: reason }) ; caller.attemptCnt++
                caller(caller == get.reply ? msgChain : stripQueryAugments(msgChain)[msgChain.length - 1].content)
                    .then(result => { if (caller == get.related) show.related(result) ; else return })
            } else {
                consoleInfo('No remaining untried endpoints')
                if (caller == get.reply) appAlert('proxyNotWorking', 'suggestOpenAI')
            }
        },

        clearTimedOut: function(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: function(api) {
            const ip = ipv4.generate({ verbose: false })
            let headers = { 'Content-Type': 'application/json', 'X-Forwarded-For': ip, 'X-Real-IP': ip }
            if (api == 'OpenAI') headers.Authorization = 'Bearer ' + config.openAIkey
            headers.Referer = headers.Origin = apis[api].expectedOrigin || '' // preserve expected traffic src
            return headers
        },

        createPayload: function(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: apiIDs.aiChatOS.userID, network: true
                }
            } else if (api == 'GPTforLove') {
                payload = {
                    prompt: msgs[msgs.length - 1].content,
                    secret: generateGPTforLoveKey(), 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 (apiIDs.gptForLove.parentID) payload.options = { parentMessageId: apiIDs.gptForLove.parentID }
            } else if (api == 'MixerBox AI')
                payload = { prompt: msgs, model: 'gpt-3.5-turbo' }
            return JSON.stringify(payload)
        }
    }

    // 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 = {

        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 }

            if (!config.proxyAPIenabled) // init OpenAI key
                config.openAIkey = await Promise.race([getOpenAItoken(), new Promise(reject => setTimeout(reject, 3000))])
            else setTimeout(() => { // try diff API after 6-9s of no response
                if (config.proxyAPIenabled && get.reply.status != 'done' && !get.reply.sender)
                    api.tryNew(get.reply, 'timeout') }, config.streamingDisabled ? 9000 : 6000)

            // Get/show answer from ChatGPT
            xhr({
                method: apis[get.reply.api].method, url: 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 => { consoleErr(err.message)
                    if (!config.proxyAPIenabled) appAlert(!config.openAIkey ? 'login' : ['openAInotWorking', 'suggestProxy'])
                    else if (get.reply.status != 'done') api.tryNew(get.reply)
                }
            })

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

            update.footerContent()
        },

        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 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([getOpenAItoken(), new Promise(reject => setTimeout(reject, 3000))])

            // Try diff API after 7s of no response
            setTimeout(() => { if (get.related.status != 'done') api.tryNew(get.related, 'timeout') }, 7000)

            return new Promise(resolve => {
                const rqPrompt = 'Show a numbered list of queries related to this one:\n\n' + query
                   + '\n\nMake 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}`
                xhr({
                    method: apis[get.related.api].method, url: 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 => { consoleErr(err.message) ; if (get.related.status != 'done') api.tryNew(get.related) }
            })})
        }
    }

    // Define PROCESS functions

    const dataProcess = {

        text(caller, resp) {
            return new Promise(resolve => {
                let respText = ''
                const logPrefix = `get.${caller.name}() » dataProcess.text() » `
                if (caller == get.reply && config.proxyAPIenabled && !config.streamingDisabled || caller.status == 'done')
                    return
                if (resp.status != 200) {
                    consoleErr(logPrefix + 'Response status', resp.status)
                    consoleErr(logPrefix + 'Response', JSON.stringify(resp))
                    if (caller == get.reply && caller.api == 'OpenAI')
                        appAlert(resp.status == 401 ? 'login'
                               : resp.status == 403 ? 'checkCloudflare'
                               : resp.status == 429 ? ['tooManyRequests', 'suggestProxy']
                                                    : ['openAInotWorking', 'suggestProxy'] )
                    else if (caller.status != 'done')
                        api.tryNew(caller)
                } else if (caller.api == 'OpenAI') {
                    if (resp.response) {
                        try { // to show response or return related queries
                            respText = JSON.parse(resp.response).choices[0].message.content
                            caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                            if (caller == get.reply) show.reply(respText, footerContent) ; else resolve(arrayify(respText))
                        } catch (err) { // suggest proxy or try diff API
                            consoleInfo(logPrefix + 'Response text: ' + resp.response)
                            consoleErr(logPrefix + appAlerts.parseFailed, err)
                            if (caller == get.reply) appAlert('openAInotWorking, suggestProxy')
                            else if (caller.status != 'done') api.tryNew(caller)
                        }
                    } else { // suggest proxy or try diff API
                        if (caller == get.reply) appAlert('openAInotWorking, suggestProxy')
                        else if (caller.status != 'done') api.tryNew(caller)
                    }
                } else if (caller.api == 'AIchatOS') {
                    if (resp.responseText
                        && !new RegExp([apis.AIchatOS.expectedOrigin, ...apis.AIchatOS.failFlags]
                            .map(str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) // escape special chars
                            .join('|')).test(resp.responseText)) {
                                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)
                                        currentIdx += chunkSize ; respText += chunk
                                    }
                                    if (!respText) throw new Error()
                                    caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                                    if (caller == get.reply) show.reply(respText, footerContent) ; else resolve(arrayify(respText))
                                } catch (err) { // try diff API
                                    consoleInfo(logPrefix + 'Response text: ' + resp.responseText)
                                    consoleErr(logPrefix + appAlerts.parseFailed, err)
                                    if (caller.status != 'done') api.tryNew(caller)
                                }
                    } else if (caller.status != 'done') api.tryNew(caller)
                } else if (caller.api == 'GPTforLove') {
                    if (resp.responseText && !resp.responseText.includes('Fail')) {
                        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) apiIDs.gptForLove.parentID = lastObj.id
                            respText = lastObj.text
                            caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                            if (caller == get.reply) show.reply(respText, footerContent) ; else resolve(arrayify(respText))
                        } catch (err) { // try diff API
                            consoleInfo(logPrefix + 'Response text: ' + resp.responseText)
                            consoleErr(logPrefix + appAlerts.parseFailed, err)
                            if (caller.status != 'done') api.tryNew(caller)
                        }
                    } else if (caller.status != 'done') api.tryNew(caller)
                } else if (caller.api == 'MixerBox AI') {
                    if (resp.responseText) {
                        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('')
                            caller.status = 'done' ; api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                            if (caller == get.reply) show.reply(respText, footerContent) ; else resolve(arrayify(respText))
                        } catch (err) { // try diff API
                            consoleInfo(logPrefix + 'Response text: ' + resp.responseText)
                            consoleErr(logPrefix + appAlerts.parseFailed, err)
                            if (caller.status != 'done') api.tryNew(caller)
                        }
                    } else if (caller.status != 'done') api.tryNew(caller)
                }

                function arrayify(strList) { // for get.related() calls
                    return (strList.match(/\d+\.\s*(.*?)(?=\n|$)/g) || [])
                        .slice(0, 5) // limit to 1st 5
                        .map(match => match.replace(/^\d+\.\s*/, '')) // strip numbering
                }
            })
        },

        stream(caller, stream) {
            if (config.streamingDisabled || !config.proxyAPIenabled) return
            const reader = stream.response.getReader() ; let accumulatedChunks = ''
            reader.read().then(processStreamText).catch(err => consoleErr('Error processing stream', err.message))
            function processStreamText({ done, value }) {
                if (done) {
                    caller.status = 'done' ; caller.sender = null
                    api.clearTimedOut(caller.triedAPIs) ; caller.attemptCnt = null
                    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 (/['"]?status['"]?:\s*['"]Fail['"]/.test(accumulatedChunks)) { // GPTforLove fail
                    consoleErr('Response', accumulatedChunks)
                    if (caller.status != 'done' && !caller.sender) api.tryNew(caller)
                    return
                }
                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) apiIDs.gptForLove.parentID = nowResult.id // for contextual replies
                        textToShow = nowResult.text
                    } else textToShow = accumulatedChunks
                    if (textToShow && caller.status != 'done') { // text ready, app waiting or sending
                        if (!caller.sender) caller.sender = caller.api // app is waiting, become sender
                        if (caller.sender == caller.api) show.reply(textToShow, footerContent)
                    }
                } catch (err) { consoleErr('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 => consoleErr('Error reading stream', err.message))
            }
        }
    }

    // Define SHOW functions

    const show = {

        reply(answer) {

            // Hide font size slider if visible
            if (appDiv.querySelector('#font-size-slider-track')) fontSizeSlider.toggle('off')

            // Build answer interface up to reply section if missing
            if (!appDiv.querySelector('pre')) {
                while (appDiv.firstChild) appDiv.removeChild(appDiv.firstChild) // clear app content
                fillStarryBG(appDiv) // add stars      

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

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

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

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

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

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

                // Create/append Wider Sidebar button
                if (!isMobile) {                    
                    var wsbSpan = document.createElement('span'),
                        wsbSVG = icons.widescreen.create()
                    wsbSpan.id = 'wsb-btn' // for toggle.sidebar() + toggle.tooltip()
                    wsbSpan.className = 'corner-btn' ; wsbSpan.style.margin = '0.151em 11px 0 0'
                    wsbSpan.append(wsbSVG) ; appDiv.append(wsbSpan)
                }

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

                // Add corner button listeners
                aboutSVG.onclick = modals.about.show
                settingsSVG.onclick = modals.settings.show
                if (speakerSVG) speakerSVG.onclick = () => {
                    const dialectMap = [
                        { 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 replyDialect = dialectMap.find(entry => entry.regex.test(config.replyLanguage)) || dialectMap[0],
                          payload = { text: appDiv.querySelector('pre').textContent, curTime: Date.now(),
                                      spokenDialect: replyDialect.code, rate: replyDialect.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(answer, { voice: 2, pitch: 1, speed: 1.5 })
                            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
                        })}}
                    })
                }
                if (pinSVG) pinSVG.onclick = () => toggle.sidebar('sticky')
                if (fontSizeSVG) fontSizeSVG.onclick = () => fontSizeSlider.toggle()
                if (wsbSVG) wsbSVG.onclick = () => toggle.sidebar('wider')
                if (!isMobile) // add hover listeners for tooltips
                    [aboutSpan, settingsSpan, speakerSpan, pinSpan, fontSizeSpan, wsbSpan].forEach(span => {
                        if (span) span.onmouseover = span.onmouseout = toggle.tooltip })

                // 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(createAnchor('https://www.kudoai.com', 'KudoAI'))
                appDiv.querySelector('.app-name').insertAdjacentElement('afterend', kudoAIspan)
                update.tweaksStyle() // show/hide based on corner space available

                // Show standby state if prefix/suffix mode on
                if (answer == 'standby') {
                    const standbyBtn = document.createElement('button')
                    standbyBtn.className = 'standby-btn'
                    standbyBtn.textContent = msgs.buttonLabel_sendQueryToGPT || 'Send search query to GPT'
                    appDiv.append(standbyBtn)
                    standbyBtn.onclick = () => {
                        appAlert('waitingResponse')
                        msgChain.push({ role: 'user', content: augmentQuery(new URL(location.href).searchParams.get('q')) })
                        show.reply.submitSrc = 'click' ; show.reply.chatbarFocused = false
                        get.reply(msgChain)
                    }

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

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

                // Init/clear reply section content/classes
                const replySection = appDiv.querySelector('section') || document.createElement('section')
                while (replySection.firstChild) replySection.removeChild(replySection.firstChild)
                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' ? msgs.placeholder_askSomethingElse || 'Ask something else'
                                                                 : msgs.tooltip_sendReply || 'Send reply' ) + '...'
                continueChatDiv.append(chatTextarea)
                replyForm.append(continueChatDiv) ; replySection.append(replyForm)
                appDiv.insertBefore(replySection, appDiv.querySelector('footer'))

                // Create/append send button
                const sendBtn = document.createElement('button'),
                      sendSVG = icons.arrowUp.create()
                sendBtn.id = 'send-btn' ; sendBtn.className = 'chatbar-btn'
                sendBtn.style.right = '12px'
                sendBtn.append(sendSVG) ; continueChatDiv.append(sendBtn)

                // Create/append shuffle button
                const shuffleBtn = document.createElement('div')
                shuffleBtn.id = 'shuffle-btn' ; shuffleBtn.className = 'chatbar-btn'
                shuffleBtn.style.right = '20px'
                const shuffleSVG = icons.arrowsTwistedRight.create()
                shuffleBtn.append(shuffleSVG) ; continueChatDiv.append(shuffleBtn)

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

                // Add reply section listeners
                replyForm.onkeydown = handleEnter ; replyForm.onsubmit = handleSubmit
                chatTextarea.oninput = autosizeChatbar
                shuffleBtn.onclick = () => {
                    const randQAprompt = 'Generate a single random question on any topic then answer it.'
                                       + `${ !config.proxyAPIenabled ? 'Don\'t talk about Canberra, Tokyo, blue whales, photosynthesis,'
                                                                     + ' deserts, mindfulness meditation, the Fibonacci sequence,'
                                                                     + ' Jupiter, the Great Wall of China, Sheakespeare or da Vinci.' : '' }`
                                       + 'Try to give an answer that is 25-50 words.'
                                       + 'Do not type anything but the question and answer. Reply in markdown.'
                    chatTextarea.value = augmentQuery(randQAprompt)
                    show.reply.submitSrc = 'click' // for show.reply()'s mobile scroll-to-top if user interacted
                    chatTextarea.dispatchEvent(new KeyboardEvent('keydown', {
                        key: 'Enter', bubbles: true, cancelable: true }))
                }
                if (!isMobile) { // add hover listeners for tooltips
                    sendBtn.onmouseover = sendBtn.onmouseout = toggle.tooltip
                    shuffleBtn.onmouseover = shuffleBtn.onmouseout = toggle.tooltip
                }

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

            // Render/show answer if query sent
            if (answer != 'standby') {
                const answerPre = appDiv.querySelector('pre')
                answerPre.innerHTML = marked.parse(answer) // render markdown
                hljs.highlightAll() // highlight code

                // 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.tweaksStyle() // to reset answerPre height

                // Auto-scroll if active
                if (config.autoScroll && !isMobile && config.proxyAPIenabled && !config.streamingDisabled)
                    window.scrollBy({ top: appDiv.querySelector('footer').getBoundingClientRect().bottom - window.innerHeight + 13 })
            }

            // Focus chatbar conditionally
            if (!config.autoFocusChatbarDisabled && !show.reply.chatbarFocused // do only once if enabled
                && !isMobile // exclude mobile devices to not auto-popup OSD keyboard
                && ( appDiv.offsetHeight < window.innerHeight - appDiv.getBoundingClientRect().top )) { // app fully above fold
                    appDiv.querySelector('#app-chatbar').focus() ; show.reply.chatbarFocused = true }

            show.reply.submitSrc = 'none' // for reply section builder's mobile scroll-to-top if user interacted

            function handleEnter(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
                        autosizeChatbar()
                    } else if (!event.shiftKey) handleSubmit(event)
            }}

            function handleSubmit(event) {
                event.preventDefault()
                const chatTextarea = appDiv.querySelector('#app-chatbar')

                // No reply, change placeholder + focus chatbar
                if (chatTextarea.value.trim() == '') {
                    chatTextarea.placeholder = `${ msgs.placeholder_typeSomething || 'Type something' }...`
                    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 (!isMobile) tooltipDiv.style.opacity = 0 // hide 'Send reply' tooltip post-send btn click
                    const appFooter = appDiv.querySelector('footer')
                    while (appFooter.firstChild) appFooter.removeChild(appFooter.firstChild)

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

                    show.reply.chatbarFocused = false // for auto-focus routine
                }
            }

            // Autosize chatbar function
            const chatTextarea = appDiv.querySelector('#app-chatbar')
            let prevLength = chatTextarea.value.length
            function autosizeChatbar() {
                const newLength = chatTextarea.value.length
                if (newLength < prevLength) { // if deleting txt
                    chatTextarea.style.height = 'auto' // ...auto-fit height
                    if (parseInt(getComputedStyle(chatTextarea).height, 10) < 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
            }
        },

        related(queries) {
            if (!show.related.greenlit) { // wait for get.reply() to finish showing answer
                show.related.statusChecker = setInterval(() => {
                    if (get.reply.status != 'waiting') {
                        show.related.greenlit = true
                        show.related(queries)
                        clearInterval(show.related.statusChecker)
                }}, 500, queries)
            } else { // show queries from latest statusChecker call
                show.related.greenlit = false
                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 = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
                              relatedQuerySVGpath = document.createElementNS('http://www.w3.org/2000/svg','path')

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

                        // Create icon
                        for (const [attr, value] of [
                            ['viewBox', '0 0 24 24'], ['width', 18], ['height', 18], ['fill', 'currentColor']
                        ]) relatedQuerySVG.setAttribute(attr, value)
                        relatedQuerySVGpath.setAttribute('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')
                        relatedQuerySVG.style.transform = 'rotate(180deg)' // flip arrow upside down

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

                        // Add fade + listeners
                        setTimeout(() => {
                            relatedQueryDiv.classList.add('active')
                            relatedQueryDiv.onclick = relatedQueryDiv.onkeydown = handleRQevent
                        }, idx * 100)
                    })

                    update.tweaksStyle() // to shorten <pre> max-height
        }}}
    }

    // Run MAIN routine

    registerMenu() // create browser toolbar menu

    // Init ALERTS
    const appAlerts = {
        waitingResponse:  `${ msgs.alert_waitingFor || 'Waiting for' } ${config.appName} ${ msgs.alert_response || 'response' }...`,
        login:            `${ msgs.alert_login || 'Please login' } @ `,
        checkCloudflare:  `${ msgs.alert_checkCloudflare || 'Please pass Cloudflare security check' } @ `,
        tooManyRequests:  `${ msgs.alert_tooManyRequests || 'API is flooded with too many requests' }.`,
        parseFailed:      `${ msgs.alert_parseFailed || 'Failed to parse response JSON' }.`,
        proxyNotWorking:  `${ msgs.mode_proxy || 'Proxy Mode' } ${ msgs.alert_notWorking || 'is not working' }.`,
        openAInotWorking: `OpenAI API ${ msgs.alert_notWorking || 'is not working' }.`,
        suggestProxy:     `${ msgs.alert_try || 'Try' } ${ msgs.alert_switchingOn || 'switching on' } ${ msgs.mode_proxy || 'Proxy Mode' }`,
        suggestOpenAI:    `${ msgs.alert_try || 'Try' } ${ msgs.alert_switchingOff || 'switching off' } ${ msgs.mode_proxy || 'Proxy Mode' }`
    }

    // Init scheme var
    let scheme = config.scheme || ( isDarkMode() ? 'dark' : 'light' )

    // Create/ID/classify/listenerize BRAVEGPT container
    const appDiv = document.createElement('div') ; appDiv.id = 'bravegpt'
    appDiv.classList.add('fade-in', // BraveGPT class
                         'snippet') // Brave class
    appDiv.addEventListener(inputEvents.down, event => { // to dismiss visible font size slider
        let elem = event.target
        while (elem && !(elem.id?.includes('font-size'))) // find font size elem parent to exclude handling down event
            elem = elem.parentNode
        if (!elem && appDiv.querySelector('#font-size-slider-track')) fontSizeSlider.toggle('off')
    })

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

    // Stylize SITE elems
    const tweaksStyle = createStyle(),
          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
    update.tweaksStyle() ; document.head.append(tweaksStyle)

    // Create/stylize TOOLTIPs
    if (!isMobile) {
        var tooltipDiv = document.createElement('div') ; tooltipDiv.classList.add('btn-tooltip', 'no-user-select')
        document.head.append(createStyle('.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 ;' // font style
            + 'position: absolute ;' // for update.tooltip() calcs
            + 'box-shadow: 3px 5px 16px 0px rgb(0 0 0 / 21%) ;' // drop shadow
            + 'opacity: 0 ; transition: opacity 0.1s ; height: fit-content ; z-index: 9999 }' // visibility
        ))
    }

    // APPEND to Brave
    const hostContainer = document.querySelector(isMobile ? '#results' : '.sidebar')
    setTimeout(() => {
        hostContainer.prepend(appDiv)
        setTimeout(() => appDiv.classList.add('active'), 100) // fade in
    }, isMobile ? 500 : 100)

    // Remove non-visible OVERFLOW STYLES for boundless hover fx
    let appAncestor = hostContainer
    while (appAncestor) {
        if (getComputedStyle(appAncestor).overflow != 'visible') appAncestor.style.overflow = 'visible'
        appAncestor = appAncestor.parentElement
    }

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

    // 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(document.location) // prefix required but not present
        || config.suffixEnabled && !/.*q=.*(?:%3F|?|%EF%BC%9F)(?:&|$)/.test(document.location)) { // suffix required but not present
            show.reply('standby', footerContent)
            if (!config.rqDisabled) {
                const lastQuery = stripQueryAugments(msgChain)[msgChain.length - 1].content
                get.related(lastQuery).then(queries => show.related(queries))
                    .catch(err => { consoleErr(err.message)
                        if (get.related.status != 'done') api.tryNew(get.related) })
            }
    } else { appAlert('waitingResponse') ; get.reply(msgChain) }

    // Add key listener to dismiss modals
    document.onkeydown = 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 != scheme) update.scheme(newScheme)
    }

}, 1500)