Torn Color Customizer

Customizes Torn sidebar notification colors and Ranked War weapon/armor rarity backgrounds.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Torn Color Customizer
// @namespace    https://www.torn.com/
// @version      1.0.0
// @description  Customizes Torn sidebar notification colors and Ranked War weapon/armor rarity backgrounds.
// @author       Exiled [3527247]
// @match        https://www.torn.com/*
// @match        https://torn.com/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(() => {
    'use strict';

    const ID = 'tncr', VERSION = '1.0.0';
    const KEYS = {
        enabled: `${ID}-enabled`,
        custom: `${ID}-custom`,
        color: `${ID}-color`,
        rwEnabled: `${ID}-rw-enabled`,
        dot: `${ID}-dot`,
        accountLinks: `${ID}-account-links`,
        eventsLegacy: `${ID}-events`
    };
    const RW = {
        yellow: {label: 'Yellow', default: '#FFFF00'},
        orange: {label: 'Orange', default: '#D08000'},
        red: {label: 'Red', default: '#E64D1A'}
    };
    const get = (key, fallback) => localStorage.getItem(key) ?? fallback;
    const enabled = () => get(KEYS.enabled, '1') !== '0';
    const custom = () => get(KEYS.custom, '0') === '1';
    const savedColor = () => normalize(get(KEYS.color, '#829E2E')) || '#829E2E';
    const rwEnabled = () => get(KEYS.rwEnabled, '0') === '1';
    const dotEnabled = () => get(KEYS.dot, '1') !== '0';
    const accountLinksEnabled = () => get(KEYS.accountLinks, get(KEYS.eventsLegacy, '0')) === '1';
    const rwTierEnabled = tier => get(`${ID}-rw-${tier}-enabled`, '0') === '1';
    const rwSavedColor = tier => normalize(get(`${ID}-rw-${tier}-color`, RW[tier].default)) || RW[tier].default;

    const style = document.createElement('style');
    style.textContent = `
html.${ID},html.${ID} body,html.${ID} #sidebarroot{
 --sidebar-status-attention-dot-color:linear-gradient(180deg,#A3C248,#799427)!important;
 --sidebar-status-attention-color:#829e2e!important;
 --sidebar-area-bg-attention:rgba(121,148,39,.18)!important;
 --sidebar-area-bg-attention-active:rgba(121,148,39,.28)!important
}
html.${ID} #sidebarroot [class*="area-mobile___"][class*="attention___"]{
 --sidebar-status-attention-dot-color:linear-gradient(180deg,#AFC372,#798D3C)!important;
 --sidebar-status-attention-color:#829e2e!important;
 --sidebar-area-bg-attention:rgba(121,141,60,.14)!important;
 --sidebar-area-bg-attention-active:rgba(133,166,35,.24)!important
}
html.${ID} body.dark-mode #sidebarroot [class*="area-mobile___"][class*="attention___"]{
 --sidebar-status-attention-color:#a8bc6b!important;
 --sidebar-area-bg-attention:rgba(175,195,114,.1)!important;
 --sidebar-area-bg-attention-active:rgba(187,220,89,.2)!important
}
html.${ID}.${ID}-custom,html.${ID}.${ID}-custom body,html.${ID}.${ID}-custom #sidebarroot,
html.${ID}.${ID}-custom #sidebarroot [class*="area-mobile___"][class*="attention___"],
html.${ID}.${ID}-custom body.dark-mode #sidebarroot [class*="area-mobile___"][class*="attention___"]{
 --sidebar-status-attention-dot-color:linear-gradient(180deg,var(--${ID}-light),var(--${ID}-dark))!important;
 --sidebar-status-attention-color:var(--${ID}-color)!important;
 --sidebar-area-bg-attention:var(--${ID}-bg)!important;
 --sidebar-area-bg-attention-active:var(--${ID}-bg-active)!important
}
html.${ID} #sidebarroot [class*="area-desktop___"][class*="attention___"] [class*="linkName___"]{
 color:var(--${ID}-desktop-label-color,inherit)!important
}
/* PDA labels use a direct text color instead of the shared attention variable. */
html.${ID} #sidebarroot [class*="area-mobile___"][class*="attention___"] a[class*="mobileLink___"] > span:last-child,
html.${ID} #sidebarroot [class*="area-mobile___"][class*="attention___"] a.sidebarMobileLink > span:last-child{
 color:var(--sidebar-status-attention-color)!important
}
/* PDA draws the notification dot with a pseudo-element. */
html.${ID} #sidebarroot [class*="area-mobile___"][class*="attention___"]::after,
html.${ID} #sidebarroot [class*="area-mobile___"][class*="attention___"] > [class*="area-row___"]::after,
html.${ID} #sidebarroot [class*="area-mobile___"][class*="attention___"] a[class*="mobileLink___"]::after,
html.${ID} #sidebarroot [class*="area-mobile___"][class*="attention___"] a.sidebarMobileLink::after{
 background:var(--sidebar-status-attention-dot-color)!important;
 background-image:var(--sidebar-status-attention-dot-color)!important
}
/* Remove Torn's dark inset rim from recolored dots. */
html.${ID} #sidebarroot [class*="attention___"]::before,
html.${ID} #sidebarroot [class*="attention___"]::after,
html.${ID} #sidebarroot [class*="attention___"] > [class*="area-row___"]::before,
html.${ID} #sidebarroot [class*="attention___"] > [class*="area-row___"]::after{
 box-shadow:none!important;
 filter:none!important
}

html.${ID}.${ID}-no-dot,html.${ID}.${ID}-no-dot body,html.${ID}.${ID}-no-dot #sidebarroot,
html.${ID}.${ID}-no-dot #sidebarroot [class*="attention___"]{
 --sidebar-status-attention-dot-color:transparent!important
}
/* Hide the PDA dot when disabled. */
html.${ID}.${ID}-no-dot #sidebarroot [class*="area-mobile___"][class*="attention___"]::after,
html.${ID}.${ID}-no-dot #sidebarroot [class*="area-mobile___"][class*="attention___"] > [class*="area-row___"]::after,
html.${ID}.${ID}-no-dot #sidebarroot [class*="area-mobile___"][class*="attention___"] a[class*="mobileLink___"]::after,
html.${ID}.${ID}-no-dot #sidebarroot [class*="area-mobile___"][class*="attention___"] a.sidebarMobileLink::after{
 content:none!important;
 display:none!important;
 background:none!important;
 box-shadow:none!important
}
html.${ID}:not(.${ID}-custom) #sidebarroot [class*="area-desktop___"][class*="attention___"] [class*="defaultIcon___"] svg :is(path,polygon,rect,circle,ellipse){fill:url(#sidebar_svg_gradient_regular_desktop_green)!important}
html.${ID}:not(.${ID}-custom) #sidebarroot [class*="area-mobile___"][class*="attention___"] [class*="defaultIcon___"] svg :is(path,polygon,rect,circle,ellipse){fill:url(#sidebar_svg_gradient_regular_green_mobile)!important}
html.${ID}:not(.${ID}-custom) #sidebarroot [class*="area-mobile___"][class*="attention___"]:is(:hover,:active) [class*="defaultIcon___"] svg :is(path,polygon,rect,circle,ellipse){fill:url(#sidebar_svg_gradient_regular_green_mobile_hover)!important}
html.${ID}.${ID}-custom #sidebarroot [class*="area-desktop___"][class*="attention___"] [class*="defaultIcon___"] svg :is(path,polygon,rect,circle,ellipse){fill:url(#${ID}-desktop)!important}
html.${ID}.${ID}-custom #sidebarroot [class*="area-mobile___"][class*="attention___"] [class*="defaultIcon___"] svg :is(path,polygon,rect,circle,ellipse){fill:url(#${ID}-mobile)!important}
html.${ID}.${ID}-custom #sidebarroot [class*="area-mobile___"][class*="attention___"]:is(:hover,:active) [class*="defaultIcon___"] svg :is(path,polygon,rect,circle,ellipse){fill:url(#${ID}-mobile-hover)!important}

/* Messages, Events, and Awards use different notification markup on PC and PDA. */
html.${ID}.${ID}-account-links :is(#nav-messages,#nav-events,#nav-awards)[class*="positive___"]{
 color:var(--sidebar-status-attention-color)!important
}
html.${ID}.${ID}-account-links :is(#nav-messages,#nav-events,#nav-awards)[class*="positive___"] svg,
html.${ID}.${ID}-account-links :is(#nav-messages,#nav-events,#nav-awards)[class*="positive___"] svg :is(path,polygon,rect,circle,ellipse){
 fill:var(--sidebar-status-attention-color)!important;
 color:var(--sidebar-status-attention-color)!important
}
html.${ID}.${ID}-account-links :is(#nav-messages,#nav-events,#nav-awards)[class*="positive___"] [class*="amount___"]{
 color:var(--sidebar-status-attention-color)!important;
 -webkit-text-fill-color:var(--sidebar-status-attention-color)!important;
 background:transparent!important;
 border:0!important;
 box-shadow:none!important;
 filter:none!important
}

/* PDA: the nav ID is on the outer mobile area. A live notification has available___ and mobileAmount___. */
html.${ID}.${ID}-account-links #sidebarroot :is(#nav-messages,#nav-events,#nav-awards)[class*="area-mobile___"][class*="available___"]:has([class*="mobileAmount___"]){
 --${ID}-account-mobile-color:var(--sidebar-status-attention-color)
}
html.${ID}.${ID}-account-links body.dark-mode #sidebarroot :is(#nav-messages,#nav-events,#nav-awards)[class*="area-mobile___"][class*="available___"]:has([class*="mobileAmount___"]){
 --${ID}-account-mobile-color:#a8bc6b
}
html.${ID}.${ID}-account-links.${ID}-custom #sidebarroot :is(#nav-messages,#nav-events,#nav-awards)[class*="area-mobile___"][class*="available___"]:has([class*="mobileAmount___"]),
html.${ID}.${ID}-account-links.${ID}-custom body.dark-mode #sidebarroot :is(#nav-messages,#nav-events,#nav-awards)[class*="area-mobile___"][class*="available___"]:has([class*="mobileAmount___"]){
 --${ID}-account-mobile-color:var(--${ID}-color)
}
html.${ID}.${ID}-account-links #sidebarroot :is(#nav-messages,#nav-events,#nav-awards)[class*="area-mobile___"][class*="available___"]:has([class*="mobileAmount___"]) [class*="defaultIcon___"] svg,
html.${ID}.${ID}-account-links #sidebarroot :is(#nav-messages,#nav-events,#nav-awards)[class*="area-mobile___"][class*="available___"]:has([class*="mobileAmount___"]) [class*="defaultIcon___"] svg :is(path,polygon,rect,circle,ellipse){
 fill:var(--${ID}-account-mobile-color)!important;
 color:var(--${ID}-account-mobile-color)!important
}
html.${ID}.${ID}-account-links #sidebarroot :is(#nav-messages,#nav-events,#nav-awards)[class*="area-mobile___"][class*="available___"]:has([class*="mobileAmount___"]) [class*="mobileAmount___"],
html.${ID}.${ID}-account-links #sidebarroot :is(#nav-messages,#nav-events,#nav-awards)[class*="area-mobile___"][class*="available___"]:has([class*="mobileAmount___"]) a[class*="mobileLink___"] > span:last-child{
 color:var(--${ID}-account-mobile-color)!important;
 -webkit-text-fill-color:var(--${ID}-account-mobile-color)!important
}
html.${ID}.${ID}-account-links #sidebarroot :is(#nav-messages,#nav-events,#nav-awards)[class*="area-mobile___"][class*="available___"]:has([class*="mobileAmount___"]) [class*="mobileAmount___"]{
 background:transparent!important;
 border:0!important;
 box-shadow:none!important;
 filter:none!important
}

html.${ID}-rw.${ID}-rw-yellow .display-bonus--yellow{
 --db-bgc:var(--${ID}-rw-yellow-row)!important;
 --db-outline:var(--${ID}-rw-yellow-color)!important
}
html.${ID}-rw.${ID}-rw-orange .display-bonus--orange{
 --db-bgc:var(--${ID}-rw-orange-row)!important;
 --db-outline:var(--${ID}-rw-orange-color)!important
}
html.${ID}-rw.${ID}-rw-red .display-bonus--red{
 --db-bgc:var(--${ID}-rw-red-row)!important;
 --db-outline:var(--${ID}-rw-red-color)!important
}
html.${ID}-rw.${ID}-rw-yellow :is(.glow-yellow,.glow-yellow-background){
 background-color:var(--${ID}-rw-yellow-soft)!important;
 background-image:radial-gradient(circle at 50% 45%,var(--${ID}-rw-yellow-glow) 0%,var(--${ID}-rw-yellow-soft) 48%,transparent 78%)!important;
 box-shadow:inset 0 0 30px var(--${ID}-rw-yellow-mid),0 0 10px var(--${ID}-rw-yellow-soft)!important
}
html.${ID}-rw.${ID}-rw-orange :is(.glow-orange,.glow-orange-background){
 background-color:var(--${ID}-rw-orange-soft)!important;
 background-image:radial-gradient(circle at 50% 45%,var(--${ID}-rw-orange-glow) 0%,var(--${ID}-rw-orange-soft) 48%,transparent 78%)!important;
 box-shadow:inset 0 0 30px var(--${ID}-rw-orange-mid),0 0 10px var(--${ID}-rw-orange-soft)!important
}
html.${ID}-rw.${ID}-rw-red :is(.glow-red,.glow-red-background){
 background-color:var(--${ID}-rw-red-soft)!important;
 background-image:radial-gradient(circle at 50% 45%,var(--${ID}-rw-red-glow) 0%,var(--${ID}-rw-red-soft) 48%,transparent 78%)!important;
 box-shadow:inset 0 0 30px var(--${ID}-rw-red-mid),0 0 10px var(--${ID}-rw-red-soft)!important
}
html.${ID}-rw.${ID}-rw-yellow :is(.glow-yellow,.glow-yellow-background)::before,html.${ID}-rw.${ID}-rw-yellow :is(.glow-yellow,.glow-yellow-background)::after{background-color:var(--${ID}-rw-yellow-soft)!important;background-image:radial-gradient(circle at 50% 45%,var(--${ID}-rw-yellow-glow) 0%,var(--${ID}-rw-yellow-soft) 48%,transparent 78%)!important;box-shadow:inset 0 0 30px var(--${ID}-rw-yellow-mid)!important}
html.${ID}-rw.${ID}-rw-orange :is(.glow-orange,.glow-orange-background)::before,html.${ID}-rw.${ID}-rw-orange :is(.glow-orange,.glow-orange-background)::after{background-color:var(--${ID}-rw-orange-soft)!important;background-image:radial-gradient(circle at 50% 45%,var(--${ID}-rw-orange-glow) 0%,var(--${ID}-rw-orange-soft) 48%,transparent 78%)!important;box-shadow:inset 0 0 30px var(--${ID}-rw-orange-mid)!important}
html.${ID}-rw.${ID}-rw-red :is(.glow-red,.glow-red-background)::before,html.${ID}-rw.${ID}-rw-red :is(.glow-red,.glow-red-background)::after{background-color:var(--${ID}-rw-red-soft)!important;background-image:radial-gradient(circle at 50% 45%,var(--${ID}-rw-red-glow) 0%,var(--${ID}-rw-red-soft) 48%,transparent 78%)!important;box-shadow:inset 0 0 30px var(--${ID}-rw-red-mid)!important}

html.${ID}-rw.${ID}-rw-yellow li.item-weapon:has(.glow-yellow),html.${ID}-rw.${ID}-rw-yellow li.item-armor:has(.glow-yellow){background-color:var(--${ID}-rw-yellow-soft)!important;background-image:radial-gradient(circle at 50% 45%,var(--${ID}-rw-yellow-glow) 0%,var(--${ID}-rw-yellow-soft) 52%,transparent 82%)!important;box-shadow:inset 0 0 24px var(--${ID}-rw-yellow-mid)!important}
html.${ID}-rw.${ID}-rw-orange li.item-weapon:has(.glow-orange),html.${ID}-rw.${ID}-rw-orange li.item-armor:has(.glow-orange){background-color:var(--${ID}-rw-orange-soft)!important;background-image:radial-gradient(circle at 50% 45%,var(--${ID}-rw-orange-glow) 0%,var(--${ID}-rw-orange-soft) 52%,transparent 82%)!important;box-shadow:inset 0 0 24px var(--${ID}-rw-orange-mid)!important}
html.${ID}-rw.${ID}-rw-red li.item-weapon:has(.glow-red),html.${ID}-rw.${ID}-rw-red li.item-armor:has(.glow-red){background-color:var(--${ID}-rw-red-soft)!important;background-image:radial-gradient(circle at 50% 45%,var(--${ID}-rw-red-glow) 0%,var(--${ID}-rw-red-soft) 52%,transparent 82%)!important;box-shadow:inset 0 0 24px var(--${ID}-rw-red-mid)!important}

#${ID}-divider{background:rgba(0,0,0,.18)!important;display:block!important;flex:0 0 1px!important;height:1px!important;margin:12px 0!important;min-height:1px!important;padding:0!important;width:100%!important}
body.dark-mode #${ID}-divider{background:rgba(255,255,255,.16)!important}
#${ID}-entry button{width:100%}
#${ID}-panel[hidden],#${ID}-rw-options[hidden],.${ID}-rw-color[hidden]{display:none!important}
#${ID}-panel{background:#f2f2f2;border:1px solid #aaa;border-radius:7px;box-shadow:0 5px 22px #0008;color:#222;left:50%;max-height:calc(100vh - 100px);max-width:calc(100vw - 24px);overflow:auto;padding:14px;position:fixed;top:72px;transform:translateX(-50%);width:350px;z-index:999999}
body.dark-mode #${ID}-panel{background:#2d2d2d;border-color:#555;color:#ddd}
#${ID}-panel header{align-items:center;display:flex;font-size:15px;font-weight:700;justify-content:space-between;margin-bottom:14px}
#${ID}-close{background:none;border:0;color:inherit;cursor:pointer;font-size:22px;line-height:1;padding:0 2px}
#${ID}-panel label{align-items:center;display:flex;gap:9px;margin:9px 0}
#${ID}-modes{border:0;margin:12px 0 0;padding:0}
#${ID}-modes legend,.${ID}-rw-tier legend{font-size:12px;font-weight:700;margin-bottom:5px}
#${ID}-modes[aria-disabled="true"]{opacity:.45;pointer-events:none}
#${ID}-colors,.${ID}-rw-color{align-items:center;display:flex;gap:8px;margin:10px 0 0 25px}
#${ID}-colors[aria-disabled="true"]{opacity:.45;pointer-events:none}
#${ID}-picker,.${ID}-rw-picker{background:none;border:0;cursor:pointer;height:34px;padding:0;width:42px}
#${ID}-hex,.${ID}-rw-hex{background:transparent;border:1px solid #888;border-radius:4px;color:inherit;font:13px monospace;padding:7px;width:88px}
.${ID}-invalid{border-color:#d33!important;box-shadow:0 0 0 1px #d33}
#${ID}-panel p{font-size:12px;line-height:1.4;margin:12px 0 0;opacity:.75}
#${ID}-panel-divider{border:0;border-top:1px solid rgba(127,127,127,.35);height:0;margin:16px 0}
#${ID}-rw-title{font-size:13px;font-weight:700;margin:0 0 8px}
#${ID}-rw-options{margin-top:8px}
.${ID}-rw-tier{border:1px solid rgba(127,127,127,.35);border-radius:6px;margin:10px 0;padding:8px 10px 10px}
.${ID}-rw-tier legend{padding:0 5px}
.${ID}-actions{display:flex;justify-content:flex-end;margin-top:12px}
.${ID}-reset{background:rgba(127,127,127,.12);border:1px solid rgba(127,127,127,.5);border-radius:5px;color:inherit;cursor:pointer;font:inherit;padding:6px 10px}
.${ID}-reset:hover{background:rgba(127,127,127,.22)}
#${ID}-panel footer{border-top:1px solid rgba(127,127,127,.3);font-size:11px;margin-top:14px;padding-top:9px;opacity:.7}
@media (max-width:600px){#${ID}-panel{box-sizing:border-box;max-height:calc(100vh - 24px);top:12px;width:calc(100vw - 24px)}#${ID}-hex,.${ID}-rw-hex{font-size:16px}}
`;
    (document.head || document.documentElement).append(style);

    function normalize(value) {
        const match = String(value).trim().match(/^#?([\da-f]{3}|[\da-f]{6})$/i);
        if (!match) return '';
        let hex = match[1];
        if (hex.length === 3) hex = [...hex].map(x => x + x).join('');
        return `#${hex.toUpperCase()}`;
    }

    const rgb = hex => hex.slice(1).match(/../g).map(x => parseInt(x, 16));
    const mix = (hex, target, amount) => `#${rgb(hex).map((v, i) => Math.round(v + (target[i] - v) * amount).toString(16).padStart(2, '0')).join('').toUpperCase()}`;
    const rgba = (hex, alpha) => `rgba(${rgb(hex).join(',')},${alpha})`;

    const ensureGradients = () => {
        if (document.getElementById(`${ID}-gradients`) || !document.body) return;
        document.body.insertAdjacentHTML('afterbegin', `<svg id="${ID}-gradients" width="0" height="0" style="position:absolute"><defs>
<linearGradient id="${ID}-desktop" x1=".5" y1="1" x2=".5" y2="0"><stop offset="0"/><stop offset="1"/></linearGradient>
<linearGradient id="${ID}-mobile" x1=".5" y1="0" x2=".5" y2="1"><stop offset="0"/><stop offset="1"/></linearGradient>
<linearGradient id="${ID}-mobile-hover" x1=".5" y1="0" x2=".5" y2="1"><stop offset="0"/><stop offset="1"/></linearGradient>
</defs></svg>`);
    };

    const applyColor = color => {
        ensureGradients();
        const root = document.documentElement;
        const dark = mix(color, [0,0,0], .22), light = mix(color, [255,255,255], .28);
        const hoverLight = mix(color, [255,255,255], .42), hoverDark = mix(color, [0,0,0], .06);
        root.style.setProperty(`--${ID}-color`, color);
        root.style.setProperty(`--${ID}-dark`, dark);
        root.style.setProperty(`--${ID}-light`, light);
        root.style.setProperty(`--${ID}-bg`, rgba(color, .18));
        root.style.setProperty(`--${ID}-bg-active`, rgba(color, .28));
        const setStops = (gradient, a, b) => {
            const stops = document.getElementById(gradient)?.querySelectorAll('stop');
            if (stops?.length === 2) {
                stops[0].setAttribute('stop-color', a);
                stops[1].setAttribute('stop-color', b);
            }
        };
        setStops(`${ID}-desktop`, dark, light);
        setStops(`${ID}-mobile`, light, dark);
        setStops(`${ID}-mobile-hover`, hoverLight, hoverDark);
    };

    const rwValues = color => ({
        row: rgba(color, .20),
        soft: rgba(color, .18),
        mid: rgba(color, .38),
        glow: rgba(mix(color, [255,255,255], .35), .62)
    });

    const applyRwColor = (tier, color) => {
        const root = document.documentElement, values = rwValues(color);
        root.style.setProperty(`--${ID}-rw-${tier}-color`, color);
        root.style.setProperty(`--${ID}-rw-${tier}-row`, values.row);
        root.style.setProperty(`--${ID}-rw-${tier}-soft`, values.soft);
        root.style.setProperty(`--${ID}-rw-${tier}-mid`, values.mid);
        root.style.setProperty(`--${ID}-rw-${tier}-glow`, values.glow);
    };

    const matchesIn = (scope, selector) => {
        const found = [];
        if (scope?.nodeType === 1 && scope.matches(selector)) found.push(scope);
        scope?.querySelectorAll?.(selector).forEach(el => found.push(el));
        return found;
    };

    const applyRwDom = (scope = document) => {
        if (!document.body) return;
        for (const tier of Object.keys(RW)) {
            const active = rwEnabled() && rwTierEnabled(tier);
            const color = rwSavedColor(tier), values = rwValues(color);
            for (const row of matchesIn(scope, `.display-bonus--${tier}`)) {
                if (active) {
                    row.dataset.tncrRwRow = tier;
                    row.style.setProperty('--db-bgc', values.row, 'important');
                    row.style.setProperty('--db-outline', color, 'important');
                    row.style.setProperty('background-color', values.row, 'important');
                    row.style.setProperty('outline-color', color, 'important');
                } else if (row.dataset.tncrRwRow === tier) {
                    delete row.dataset.tncrRwRow;
                    row.style.removeProperty('--db-bgc');
                    row.style.removeProperty('--db-outline');
                    row.style.removeProperty('background-color');
                    row.style.removeProperty('outline-color');
                }
            }
            // Item components use either a single glow class or separate background and border classes.
            const previewSelector = `.glow-${tier},.glow-${tier}-background`;
            for (const preview of matchesIn(scope, previewSelector)) {
                if (active) {
                    preview.dataset.tncrRwPreview = tier;
                    preview.style.setProperty('background-color', values.soft, 'important');
                    preview.style.setProperty('background-image', `radial-gradient(circle at 50% 45%,${values.glow} 0%,${values.soft} 48%,transparent 78%)`, 'important');
                    preview.style.setProperty('box-shadow', `inset 0 0 30px ${values.mid},0 0 10px ${values.soft}`, 'important');
                } else if (preview.dataset.tncrRwPreview === tier) {
                    delete preview.dataset.tncrRwPreview;
                    preview.style.removeProperty('background-color');
                    preview.style.removeProperty('background-image');
                    preview.style.removeProperty('box-shadow');
                }
            }

            for (const border of matchesIn(scope, `.glow-${tier}-border`)) {
                if (active) {
                    border.dataset.tncrRwBorder = tier;
                    border.style.setProperty('border-color', color, 'important');
                    border.style.setProperty('outline-color', color, 'important');
                    border.style.setProperty('box-shadow', `0 0 10px ${values.mid},inset 0 0 8px ${values.soft}`, 'important');
                } else if (border.dataset.tncrRwBorder === tier) {
                    delete border.dataset.tncrRwBorder;
                    border.style.removeProperty('border-color');
                    border.style.removeProperty('outline-color');
                    border.style.removeProperty('box-shadow');
                }
            }
        }
    };

    const syncDesktopLabelColor = () => {
        const label = document.querySelector('#sidebarroot [class*="area-desktop___"]:not([class*="attention___"]) [class*="linkName___"]');
        if (label) document.documentElement.style.setProperty(`--${ID}-desktop-label-color`, getComputedStyle(label).color);
    };

    const sync = () => {
        const root = document.documentElement;
        syncDesktopLabelColor();
        root.classList.toggle(ID, enabled());
        root.classList.toggle(`${ID}-custom`, enabled() && custom());
        root.classList.toggle(`${ID}-no-dot`, enabled() && !dotEnabled());
        root.classList.toggle(`${ID}-account-links`, enabled() && accountLinksEnabled());
        root.classList.toggle(`${ID}-rw`, rwEnabled());
        applyColor(savedColor());

        const enabledInput = document.getElementById(`${ID}-enabled`);
        const customInput = document.getElementById(`${ID}-custom`);
        const picker = document.getElementById(`${ID}-picker`);
        const hex = document.getElementById(`${ID}-hex`);
        const colors = document.getElementById(`${ID}-colors`);
        const greenInput = document.getElementById(`${ID}-green`);
        const dotInput = document.getElementById(`${ID}-dot`);
        const accountLinksInput = document.getElementById(`${ID}-account-links`);
        if (enabledInput) enabledInput.checked = enabled();
        if (customInput) customInput.checked = custom();
        if (greenInput) greenInput.checked = !custom();
        if (dotInput) dotInput.checked = dotEnabled();
        if (accountLinksInput) accountLinksInput.checked = accountLinksEnabled();
        if (picker) picker.value = savedColor();
        if (hex && document.activeElement !== hex) hex.value = savedColor();
        const modeDisabled = !enabled();
        [greenInput, customInput, dotInput, accountLinksInput].forEach(input => { if (input) input.disabled = modeDisabled; });
        if (picker) picker.disabled = modeDisabled || !custom();
        if (hex) hex.disabled = modeDisabled || !custom();
        const modes = document.getElementById(`${ID}-modes`);
        if (modes) modes.setAttribute('aria-disabled', String(modeDisabled));
        if (colors) colors.setAttribute('aria-disabled', String(modeDisabled || !custom()));

        const rwMaster = document.getElementById(`${ID}-rw-enabled`);
        const rwOptions = document.getElementById(`${ID}-rw-options`);
        if (rwMaster) rwMaster.checked = rwEnabled();
        if (rwOptions) rwOptions.hidden = !rwEnabled();
        for (const tier of Object.keys(RW)) {
            const tierOn = rwTierEnabled(tier);
            const color = rwSavedColor(tier);
            applyRwColor(tier, color);
            root.classList.toggle(`${ID}-rw-${tier}`, rwEnabled() && tierOn);
            const toggle = document.getElementById(`${ID}-rw-${tier}-enabled`);
            const row = document.getElementById(`${ID}-rw-${tier}-color`);
            const tierPicker = document.getElementById(`${ID}-rw-${tier}-picker`);
            const tierHex = document.getElementById(`${ID}-rw-${tier}-hex`);
            if (toggle) toggle.checked = tierOn;
            if (row) row.hidden = !tierOn;
            if (tierPicker) tierPicker.value = color;
            if (tierHex && document.activeElement !== tierHex) tierHex.value = color;
        }
        applyRwDom();
    };
    sync();

    const saveToggle = (key, value) => {
        localStorage.setItem(key, value ? '1' : '0');
        sync();
    };

    const saveColor = value => {
        const color = normalize(value), hex = document.getElementById(`${ID}-hex`);
        hex?.classList.toggle(`${ID}-invalid`, !color);
        if (!color) return;
        localStorage.setItem(KEYS.color, color);
        applyColor(color);
        const picker = document.getElementById(`${ID}-picker`);
        if (picker) picker.value = color;
        if (hex && document.activeElement !== hex) hex.value = color;
    };

    const saveRwColor = (tier, value) => {
        const color = normalize(value);
        const hex = document.getElementById(`${ID}-rw-${tier}-hex`);
        hex?.classList.toggle(`${ID}-invalid`, !color);
        if (!color) return;
        localStorage.setItem(`${ID}-rw-${tier}-color`, color);
        applyRwColor(tier, color);
        applyRwDom();
        const picker = document.getElementById(`${ID}-rw-${tier}-picker`);
        if (picker) picker.value = color;
        if (hex && document.activeElement !== hex) hex.value = color;
    };

    const resetNotifications = () => {
        localStorage.setItem(KEYS.enabled, '1');
        localStorage.setItem(KEYS.custom, '0');
        localStorage.setItem(KEYS.color, '#829E2E');
        localStorage.setItem(KEYS.dot, '1');
        localStorage.setItem(KEYS.accountLinks, '0');
        localStorage.removeItem(KEYS.eventsLegacy);
        sync();
    };

    const resetRw = () => {
        localStorage.setItem(KEYS.rwEnabled, '0');
        for (const tier of Object.keys(RW)) {
            localStorage.setItem(`${ID}-rw-${tier}-enabled`, '0');
            localStorage.setItem(`${ID}-rw-${tier}-color`, RW[tier].default);
        }
        sync();
    };

    const rwTierMarkup = tier => {
        const {label} = RW[tier];
        return `<fieldset class="${ID}-rw-tier"><legend>${label} RW class</legend>
<label><input id="${ID}-rw-${tier}-enabled" type="checkbox"> Customize ${label.toLowerCase()} class</label>
<div id="${ID}-rw-${tier}-color" class="${ID}-rw-color" hidden>
<input id="${ID}-rw-${tier}-picker" class="${ID}-rw-picker" type="color" aria-label="Choose ${label.toLowerCase()} RW color">
<input id="${ID}-rw-${tier}-hex" class="${ID}-rw-hex" type="text" maxlength="7" spellcheck="false" aria-label="${label} RW color hex code">
</div></fieldset>`;
    };

    const ensurePanel = () => {
        let panel = document.getElementById(`${ID}-panel`);
        if (panel) return panel;
        panel = document.createElement('section');
        panel.id = `${ID}-panel`;
        panel.hidden = true;
        panel.setAttribute('role', 'dialog');
        panel.setAttribute('aria-label', 'Torn color settings');
        panel.innerHTML = `<header><span>Torn Color Customizer</span><button type="button" id="${ID}-close" aria-label="Close">X</button></header>
<label><input id="${ID}-enabled" type="checkbox"> Enable notification color changes</label>
<fieldset id="${ID}-modes"><legend>Color mode</legend>
<label><input id="${ID}-green" name="${ID}-mode" type="radio" value="green"> Original Torn green</label>
<label><input id="${ID}-custom" name="${ID}-mode" type="radio" value="custom"> Custom color</label>
</fieldset>
<div id="${ID}-colors"><input id="${ID}-picker" type="color" aria-label="Choose notification color"><input id="${ID}-hex" type="text" maxlength="7" spellcheck="false" aria-label="Notification color hex code"></div>
<label><input id="${ID}-dot" type="checkbox"> Show notification dot</label>
<label><input id="${ID}-account-links" type="checkbox"> Include Messages, Events, and Awards</label>
<p>Choose Torn's original green or a custom color.</p>
<div class="${ID}-actions"><button type="button" id="${ID}-reset-notifications" class="${ID}-reset">Reset notification colors</button></div>
<hr id="${ID}-panel-divider">
<div id="${ID}-rw-title">RW weapon / armor colors</div>
<label><input id="${ID}-rw-enabled" type="checkbox"> Enable RW color changer</label>
<div id="${ID}-rw-options" hidden>
${Object.keys(RW).map(rwTierMarkup).join('')}
<p>Each enabled class changes auction rows plus matching small item images and expanded large previews across Torn.</p>
<div class="${ID}-actions"><button type="button" id="${ID}-reset-rw" class="${ID}-reset">Reset RW colors</button></div>
</div>
<footer>Version ${VERSION}<br>Developed by Exiled [3527247]</footer>`;
        document.body.append(panel);
        panel.querySelector(`#${ID}-enabled`).onchange = e => saveToggle(KEYS.enabled, e.target.checked);
        panel.querySelector(`#${ID}-green`).onchange = e => { if (e.target.checked) saveToggle(KEYS.custom, false); };
        panel.querySelector(`#${ID}-custom`).onchange = e => { if (e.target.checked) saveToggle(KEYS.custom, true); };
        panel.querySelector(`#${ID}-dot`).onchange = e => saveToggle(KEYS.dot, e.target.checked);
        panel.querySelector(`#${ID}-account-links`).onchange = e => saveToggle(KEYS.accountLinks, e.target.checked);
        panel.querySelector(`#${ID}-reset-notifications`).onclick = resetNotifications;
        panel.querySelector(`#${ID}-reset-rw`).onclick = resetRw;
        panel.querySelector(`#${ID}-picker`).oninput = e => saveColor(e.target.value);
        panel.querySelector(`#${ID}-hex`).oninput = e => saveColor(e.target.value);
        panel.querySelector(`#${ID}-hex`).onblur = e => {
            e.target.value = savedColor();
            e.target.classList.remove(`${ID}-invalid`);
        };
        panel.querySelector(`#${ID}-rw-enabled`).onchange = e => saveToggle(KEYS.rwEnabled, e.target.checked);
        for (const tier of Object.keys(RW)) {
            panel.querySelector(`#${ID}-rw-${tier}-enabled`).onchange = e => saveToggle(`${ID}-rw-${tier}-enabled`, e.target.checked);
            panel.querySelector(`#${ID}-rw-${tier}-picker`).oninput = e => saveRwColor(tier, e.target.value);
            panel.querySelector(`#${ID}-rw-${tier}-hex`).oninput = e => saveRwColor(tier, e.target.value);
            panel.querySelector(`#${ID}-rw-${tier}-hex`).onblur = e => {
                e.target.value = rwSavedColor(tier);
                e.target.classList.remove(`${ID}-invalid`);
            };
        }
        panel.querySelector(`#${ID}-close`).onclick = () => panel.hidden = true;
        sync();
        return panel;
    };

    const addUI = () => {
        const settings = document.getElementById('settings_panel');
        if (!settings || document.getElementById(`${ID}-entry`)) return;
        const content = [...settings.children].find(el => el.tagName === 'DIV');
        if (!content) return;
        const sections = [...content.children].filter(el => el.nodeType === 1);
        const version = sections.find(el => /version/i.test(el.textContent) && /copyright/i.test(el.textContent));
        if (!version) return;
        const nativeButton = content.querySelector('button[class]');
        const nativeTitle = nativeButton?.querySelector('span[class]');
        const divider = document.createElement('div');
        divider.id = `${ID}-divider`;
        divider.setAttribute('aria-hidden', 'true');
        const entry = document.createElement('div');
        entry.id = `${ID}-entry`;
        entry.className = version.className;
        const button = document.createElement('button');
        button.type = 'button';
        button.id = `${ID}-open`;
        button.setAttribute('aria-label', 'Open Torn Color Customizer settings');
        if (nativeButton) button.className = nativeButton.className;
        else button.style.cssText = 'background:none;border:1px solid #777;border-radius:4px;color:inherit;cursor:pointer;padding:8px 10px';
        const label = document.createElement('span');
        if (nativeTitle) label.className = nativeTitle.className;
        label.textContent = 'Torn Color Customizer';
        button.append(label);
        button.onclick = () => ensurePanel().hidden = false;
        entry.append(button);
        version.insertAdjacentElement('afterend', divider);
        divider.insertAdjacentElement('afterend', entry);
    };

    const start = () => {
        ensureGradients();
        ensurePanel();
        addUI();
        syncDesktopLabelColor();
        applyRwDom();
        new MutationObserver(records => {
            addUI();
            syncDesktopLabelColor();
                for (const record of records) {
                if (record.type === 'attributes') applyRwDom(record.target);
                else record.addedNodes.forEach(node => applyRwDom(node));
            }
        }).observe(document.body, {childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class', 'aria-label']});
    };
    document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', start, {once: true}) : start();
})();