Bitcointalk Modern Theme

Modern light/dark/system theme for bitcointalk.org with separate customisable Main (background) and Accent presets.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bitcointalk Modern Theme
// @namespace    bitcointalk-modern-theme
// @version      2.6.8
// @description  Modern light/dark/system theme for bitcointalk.org with separate customisable Main (background) and Accent presets.
// @license      MIT
// @match        *://*.bitcointalk.org/*
// @match        *://bitcointalk.org/*
// @run-at       document-start
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  // ===========================================================================
  // Configuration
  // ===========================================================================

  // Main (background) presets — one set per mode. Surface variants are derived
  // from the chosen base hex by deriveMainPalette().
  const MAIN_PRESETS_DARK = {
    slate: { name: 'Slate (default)', base: '#1A1D21' },
    midnight: { name: 'Midnight', base: '#0F172A' },
    forest: { name: 'Forest', base: '#0F1F1A' },
    plum: { name: 'Plum', base: '#1A1022' },
    charcoal: { name: 'Charcoal', base: '#1A1A1A' },
  };
  const MAIN_PRESETS_LIGHT = {
    pearl: { name: 'Pearl (default)', base: '#F2F4F7' },
    sky: { name: 'Sky', base: '#EAF1FA' },
    cream: { name: 'Cream', base: '#F9F5EE' },
    mint: { name: 'Mint', base: '#ECF5EF' },
    rose: { name: 'Rose', base: '#F8F0F2' },
  };

  // Accent presets — one set per mode. Hover/active/on-text are derived from
  // the chosen base hex by deriveAccent(). Light variants are softer/desaturated
  // versions of the dark ones, except Bitcoin Orange which keeps its brand hue.
  const ACCENTS_DARK = {
    blue: { name: 'Forum Blue', base: '#4D6F89' },
    orange: { name: 'Bitcoin Orange', base: '#F7931A' },
    green: { name: 'Forest Green', base: '#10B981' },
    purple: { name: 'Royal Purple', base: '#8B5CF6' },
    red: { name: 'Crimson', base: '#EF4444' },
  };
  const ACCENTS_LIGHT = {
    blue: { name: 'Slate Blue', base: '#5A7B98' },
    orange: { name: 'Bitcoin Orange', base: '#F7931A' },
    green: { name: 'Sage', base: '#3FA47A' },
    purple: { name: 'Lavender', base: '#A78BFA' },
    red: { name: 'Coral', base: '#E47272' },
  };

  const DEFAULT_THEME = 'dark'; // 'light' | 'dark' | 'system'
  const DEFAULT_ACCENT_DARK_KEY = 'orange';
  const DEFAULT_ACCENT_LIGHT_KEY = 'blue';
  const DEFAULT_MAIN_DARK_KEY = 'slate';
  const DEFAULT_MAIN_LIGHT_KEY = 'pearl';
  const DEFAULT_CUSTOM_MAIN_DARK = '#1A1D21';
  const DEFAULT_CUSTOM_MAIN_LIGHT = '#F2F4F7';
  const DEFAULT_CUSTOM_ACCENT_DARK = '#F7931A';
  const DEFAULT_CUSTOM_ACCENT_LIGHT = '#5A7B98';

  // Storage keys (per-mode so Light and Dark customisations are independent)
  const KEY_THEME = 'bt-modern-theme';
  const KEY_MAIN_DARK = 'bt-modern-main-dark';
  const KEY_MAIN_LIGHT = 'bt-modern-main-light';
  const KEY_ACCENT_DARK = 'bt-modern-accent-dark';
  const KEY_ACCENT_LIGHT = 'bt-modern-accent-light';
  const KEY_MAIN_DARK_CUSTOM = 'bt-modern-main-dark-custom';
  const KEY_MAIN_LIGHT_CUSTOM = 'bt-modern-main-light-custom';
  const KEY_ACCENT_DARK_CUSTOM = 'bt-modern-accent-dark-custom';
  const KEY_ACCENT_LIGHT_CUSTOM = 'bt-modern-accent-light-custom';

  // ===========================================================================
  // Storage helpers (localStorage with safe fallback)
  // ===========================================================================

  function getPref(key, fallback) {
    try { return localStorage.getItem(key) || fallback; }
    catch (e) { return fallback; }
  }
  function setPref(key, value) {
    try { localStorage.setItem(key, value); } catch (e) {}
  }

  // ===========================================================================
  // Color helpers — derive hover/active/on-text and surface variants from a base
  // ===========================================================================

  function hexToRgb(hex) {
    const c = String(hex || '').replace('#', '');
    const full = c.length === 3 ? c.split('').map(x => x + x).join('') : c;
    const n = parseInt(full, 16);
    return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
  }
  function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
  function rgbToHex(r, g, b) {
    return '#' + [r, g, b]
      .map(x => clamp(Math.round(x), 0, 255).toString(16).padStart(2, '0'))
      .join('');
  }
  function lighten(hex, amt) {
    const { r, g, b } = hexToRgb(hex);
    return rgbToHex(r + (255 - r) * amt, g + (255 - g) * amt, b + (255 - b) * amt);
  }
  function darken(hex, amt) {
    const { r, g, b } = hexToRgb(hex);
    return rgbToHex(r * (1 - amt), g * (1 - amt), b * (1 - amt));
  }
  // RGB ↔ HSL conversion — needed to darken light surfaces while *preserving*
  // hue and boosting saturation, so a pale main like Mint (#ECF5EF, only ~14%
  // saturation) doesn't collapse into neutral grey when darkened in RGB space.
  function rgbToHsl(r, g, b) {
    r /= 255; g /= 255; b /= 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    let h, s; const l = (max + min) / 2;
    if (max === min) {
      h = 0; s = 0;
    } else {
      const d = max - min;
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
      if (max === r) h = ((g - b) / d + (g < b ? 6 : 0));
      else if (max === g) h = (b - r) / d + 2;
      else h = (r - g) / d + 4;
      h /= 6;
    }
    return { h, s, l };
  }
  function hslToRgb(h, s, l) {
    let r, g, b;
    if (s === 0) {
      r = g = b = l;
    } else {
      const hue2rgb = (p, q, t) => {
        if (t < 0) t += 1;
        if (t > 1) t -= 1;
        if (t < 1 / 6) return p + (q - p) * 6 * t;
        if (t < 1 / 2) return q;
        if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
        return p;
      };
      const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
      const p = 2 * l - q;
      r = hue2rgb(p, q, h + 1 / 3);
      g = hue2rgb(p, q, h);
      b = hue2rgb(p, q, h - 1 / 3);
    }
    return { r: r * 255, g: g * 255, b: b * 255 };
  }
  // Adjust a hex by deltaL (lightness, -1..1) and deltaS (saturation, -1..1)
  // in HSL space. Hue is preserved exactly.
  function adjustHsl(hex, deltaL, deltaS) {
    const { r, g, b } = hexToRgb(hex);
    const hsl = rgbToHsl(r, g, b);
    const newL = clamp(hsl.l + deltaL, 0, 1);
    const newS = clamp(hsl.s + deltaS, 0, 1);
    const rgb = hslToRgb(hsl.h, newS, newL);
    return rgbToHex(rgb.r, rgb.g, rgb.b);
  }

  // WCAG relative luminance — used to pick readable text color over the accent.
  function relativeLuminance(hex) {
    const { r, g, b } = hexToRgb(hex);
    const [R, G, B] = [r, g, b].map(v => {
      v /= 255;
      return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
    });
    return 0.2126 * R + 0.7152 * G + 0.0722 * B;
  }
  function deriveAccent(hex) {
    return {
      base: hex,
      hover: lighten(hex, 0.15),
      active: darken(hex, 0.10),
      onText: relativeLuminance(hex) > 0.45 ? '#1A1D21' : '#FFFFFF',
    };
  }

  // Derive a coherent surface/text palette from a single Main hex.
  function deriveMainPalette(hex, theme) {
    if (theme === 'dark') {
      return {
        bg: hex,
        bgEnd: darken(hex, 0.06),
        surface1: lighten(hex, 0.06),
        surface2: lighten(hex, 0.12),
        surface3: lighten(hex, 0.20),
        inputBg: darken(hex, 0.06),
        quoteBg: lighten(hex, 0.04),
        codeBg: darken(hex, 0.06),
        border: 'rgba(255,255,255,0.07)',
        border2: 'rgba(255,255,255,0.14)',
        text: '#E3E3E8',
        text2: '#A8B0BA',
        text3: '#6B7480',
        iconFilter: 'invert(1) opacity(0.75)',
        shadow: '0 1px 0 rgba(0,0,0,0.2)',
        shadowLg: '0 12px 32px rgba(0,0,0,0.35)',
      };
    }
    // Light theme: all surfaces derive from the chosen main hex with hue
    // preserved (HSL adjust). Each surface step lowers lightness AND boosts
    // saturation aggressively — without the saturation boost a pale main like
    // Mint (#ECF5EF, S≈32%) collapses into neutral grey when darkened. The
    // user explicitly asked surfaces to read as "dark green" when Mint is
    // selected, hence the strong saturation boost. Only text inputs/textareas
    // stay pure white via --bt-input-bg.
    return {
      bg: hex,
      bgEnd: adjustHsl(hex, -0.04, 0.10),
      surface1: adjustHsl(hex, -0.07, 0.20),
      surface2: adjustHsl(hex, -0.16, 0.32),
      surface3: adjustHsl(hex, -0.26, 0.42),
      inputBg: '#FFFFFF',
      quoteBg: adjustHsl(hex, -0.08, 0.18),
      codeBg: adjustHsl(hex, -0.12, 0.26),
      border: 'rgba(0,0,0,0.08)',
      border2: 'rgba(0,0,0,0.16)',
      text: '#1A1D21',
      text2: '#4A5058',
      text3: '#7A8088',
      iconFilter: 'opacity(0.7)',
      shadow: '0 1px 0 rgba(0,0,0,0.04)',
      shadowLg: '0 12px 32px rgba(0,0,0,0.12)',
    };
  }

  // camelCase → kebab-case for CSS variable names. Must insert a hyphen
  // before BOTH uppercase letters AND digit groups, otherwise palette keys
  // like "surface1" / "border2" / "text3" silently map to non-existent CSS
  // variables (--bt-surface1 vs the CSS expectation --bt-surface-1) and the
  // inline overrides never apply. This was the root cause of all the
  // "surfaces stay grey regardless of main colour" bug reports up to v2.5.0.
  function toCssVar(name) {
    return '--bt-' + name
      .replace(/([A-Z]|\d+)/g, '-$1')
      .toLowerCase()
      .replace(/^-/, '');
  }

  // ===========================================================================
  // Theme application
  // ===========================================================================

  function getEffectiveTheme(pref) {
    if (pref === 'system') {
      return window.matchMedia &&
        window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark' : 'light';
    }
    return pref === 'light' ? 'light' : 'dark';
  }

  function getMainKey(theme) {
    const def = theme === 'dark' ? DEFAULT_MAIN_DARK_KEY : DEFAULT_MAIN_LIGHT_KEY;
    return getPref(theme === 'dark' ? KEY_MAIN_DARK : KEY_MAIN_LIGHT, def);
  }
  function getAccentKey(theme) {
    const def = theme === 'dark' ? DEFAULT_ACCENT_DARK_KEY : DEFAULT_ACCENT_LIGHT_KEY;
    return getPref(theme === 'dark' ? KEY_ACCENT_DARK : KEY_ACCENT_LIGHT, def);
  }
  function getActiveMainHex(theme) {
    const presets = theme === 'dark' ? MAIN_PRESETS_DARK : MAIN_PRESETS_LIGHT;
    const key = getMainKey(theme);
    if (key === 'custom') {
      const def = theme === 'dark' ? DEFAULT_CUSTOM_MAIN_DARK : DEFAULT_CUSTOM_MAIN_LIGHT;
      return getPref(theme === 'dark' ? KEY_MAIN_DARK_CUSTOM : KEY_MAIN_LIGHT_CUSTOM, def);
    }
    const fallbackKey = theme === 'dark' ? DEFAULT_MAIN_DARK_KEY : DEFAULT_MAIN_LIGHT_KEY;
    return (presets[key] || presets[fallbackKey]).base;
  }
  function getActiveAccentHex(theme) {
    const presets = theme === 'dark' ? ACCENTS_DARK : ACCENTS_LIGHT;
    const key = getAccentKey(theme);
    if (key === 'custom') {
      const def = theme === 'dark' ? DEFAULT_CUSTOM_ACCENT_DARK : DEFAULT_CUSTOM_ACCENT_LIGHT;
      return getPref(theme === 'dark' ? KEY_ACCENT_DARK_CUSTOM : KEY_ACCENT_LIGHT_CUSTOM, def);
    }
    const fallbackKey = theme === 'dark' ? DEFAULT_ACCENT_DARK_KEY : DEFAULT_ACCENT_LIGHT_KEY;
    return (presets[key] || presets[fallbackKey]).base;
  }

  function applyTheme() {
    const themePref = getPref(KEY_THEME, DEFAULT_THEME);
    const effective = getEffectiveTheme(themePref);

    const mainKey = getMainKey(effective);
    const accentKey = getAccentKey(effective);
    const mainHex = getActiveMainHex(effective);
    const accentHex = getActiveAccentHex(effective);
    const palette = deriveMainPalette(mainHex, effective);
    const accent = deriveAccent(accentHex);

    const root = document.documentElement;
    root.setAttribute('data-bt-theme', effective);
    root.setAttribute('data-bt-theme-pref', themePref);
    root.setAttribute('data-bt-main', mainKey);
    root.setAttribute('data-bt-accent', accentKey);

    Object.entries(palette).forEach(([k, v]) => root.style.setProperty(toCssVar(k), v));
    root.style.setProperty('--bt-accent', accent.base);
    root.style.setProperty('--bt-accent-h', accent.hover);
    root.style.setProperty('--bt-accent-a', accent.active);
    root.style.setProperty('--bt-on-accent', accent.onText);

    // Reflect current selection in the switcher UI, if mounted.
    const sw = document.getElementById('bt-switcher');
    if (sw) {
      // Auto mode hides the customise sub-panel: in Auto the user is delegating
      // to the OS, so per-mode colour overrides would be confusing.
      const customizeEl = sw.querySelector('.bt-sw-customize');
      if (customizeEl) {
        customizeEl.style.display = themePref === 'system' ? 'none' : '';
      }
      const titleEl = sw.querySelector('#bt-sw-customize-title');
      if (titleEl) {
        titleEl.textContent = `Customise ${effective === 'dark' ? 'Dark' : 'Light'} Colours`;
      }
      rebuildSwatches(sw, 'main', effective, mainKey, mainHex);
      rebuildSwatches(sw, 'accent', effective, accentKey, accentHex);
      sw.querySelectorAll('[data-theme]').forEach(b => {
        b.classList.toggle('selected', b.dataset.theme === themePref);
      });
    }
  }

  function rebuildSwatches(sw, kind, effective, selectedKey, currentHex) {
    const row = sw.querySelector(`#bt-sw-${kind}-row`);
    if (!row) return;
    const presets = kind === 'main'
      ? (effective === 'dark' ? MAIN_PRESETS_DARK : MAIN_PRESETS_LIGHT)
      : (effective === 'dark' ? ACCENTS_DARK : ACCENTS_LIGHT);
    const customDef = kind === 'main'
      ? (effective === 'dark' ? DEFAULT_CUSTOM_MAIN_DARK : DEFAULT_CUSTOM_MAIN_LIGHT)
      : (effective === 'dark' ? DEFAULT_CUSTOM_ACCENT_DARK : DEFAULT_CUSTOM_ACCENT_LIGHT);
    const customStorageKey = kind === 'main'
      ? (effective === 'dark' ? KEY_MAIN_DARK_CUSTOM : KEY_MAIN_LIGHT_CUSTOM)
      : (effective === 'dark' ? KEY_ACCENT_DARK_CUSTOM : KEY_ACCENT_LIGHT_CUSTOM);
    const customHex = getPref(customStorageKey, customDef);
    const isCustomSelected = selectedKey === 'custom';

    const presetSwatches = Object.entries(presets).map(([key, p]) =>
      `<button type="button" class="bt-sw-swatch${key === selectedKey ? ' selected' : ''}" data-${kind}="${key}" title="${p.name}" style="background:${p.base}"></button>`
    ).join('');

    row.innerHTML = presetSwatches + `
      <span class="bt-sw-custom-wrap${isCustomSelected ? ' selected' : ''}" data-target="${kind}" title="Custom ${kind} colour">
        <span class="bt-sw-custom-display"${isCustomSelected ? ` style="background:${currentHex}"` : ''}></span>
        <input type="color" data-target="${kind}" value="${customHex}" />
      </span>
    `;
  }

  // ===========================================================================
  // Maintab fix — hide empty decorative caps that classic SMF leaves around the
  // active tab. CSS :empty fails when SMF inserts whitespace text nodes, so we
  // also do a JS pass that uses textContent.trim().
  // ===========================================================================

  function fixMaintabCaps() {
    document.querySelectorAll('[class*="maintab"]').forEach(el => {
      const text = (el.textContent || '').replace(/\s+/g, '');
      // cssText with !important — needed because the broad
      // [class*="maintab_"] CSS rule below uses !important and would
      // otherwise win over a plain inline el.style.display.
      if (!text) el.style.cssText = 'display: none !important;';
    });
  }

  // ===========================================================================
  // BBCode toolbar icon modernisation — replaces classic SMF .gif sprites with
  // inline Lucide-style SVG icons. The original <img> stays in the DOM (hidden)
  // so its onclick handlers keep firing; clicks on the new SVG span are
  // forwarded to it.
  // ===========================================================================

  const BBCODE_SVGS = {
    bold: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4h7a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/></svg>',
    italic: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>',
    underline: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3v8a6 6 0 0 0 12 0V3"/><line x1="4" y1="21" x2="20" y2="21"/></svg>',
    strike: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4H9a3 3 0 0 0-2.83 4M14 12a4 4 0 0 1 0 8H6"/><line x1="4" y1="12" x2="20" y2="12"/></svg>',
    pre: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>',
    code: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
    quote: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.75-2-2-2H4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h1v1c0 1-1 2-2 2v3c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.75-2-2-2h-4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h1v1c0 1-1 2-2 2v3c0 1 0 1 1 1z"/></svg>',
    img: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>',
    url: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
    email: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><polyline points="22,6 12,13 2,6"/></svg>',
    ftp: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>',
    flash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
    youtube: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z"/><polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02"/></svg>',
    list: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="3.5" cy="6" r="1"/><circle cx="3.5" cy="12" r="1"/><circle cx="3.5" cy="18" r="1"/></svg>',
    orderlist: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>',
    left: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg>',
    center: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="10" x2="6" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="18" y1="18" x2="6" y2="18"/></svg>',
    right: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" y1="10" x2="7" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="7" y2="18"/></svg>',
    hr: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"/></svg>',
    sub: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5l8 11"/><path d="M12 5L4 16"/><text x="14" y="22" font-size="9" fill="currentColor" stroke="none">2</text></svg>',
    sup: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8l8 11"/><path d="M12 8L4 19"/><text x="14" y="9" font-size="9" fill="currentColor" stroke="none">2</text></svg>',
    tt: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>',
    font: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>',
    size: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V4h18v3"/><path d="M8 20h8"/><path d="M12 4v16"/><path d="M18 14v6"/><path d="M15 14h6"/></svg>',
    color: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c.93 0 1.69-.76 1.69-1.69 0-.43-.16-.83-.43-1.13-.27-.31-.43-.71-.43-1.13a1.69 1.69 0 0 1 1.69-1.69h1.99c3.04 0 5.49-2.45 5.49-5.49C22 6.04 17.52 2 12 2z"/><circle cx="6.5" cy="11.5" r="1.5"/><circle cx="9.5" cy="7.5" r="1.5"/><circle cx="14.5" cy="7.5" r="1.5"/><circle cx="17.5" cy="11.5" r="1.5"/></svg>',
    table: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>',
    glow: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>',
    shadow: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 1 0 18z" fill="currentColor"/></svg>',
    spoiler: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
    abbr: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
    // bitcointalk-specific BBCode for the Bitcoin symbol
    bitcoin: '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M16.43 11.36c.5-.4.85-1 .85-1.74 0-1.6-1.27-2.66-3.06-2.84V5h-1.5v1.7H11V5H9.5v1.7H7v1.5h1.2V16H7v1.5h2.5V19H11v-1.5h1.72V19h1.5v-1.5c2.05 0 3.34-1.18 3.34-3 0-.93-.42-1.74-1.13-2.14zm-5.93-3.16h2.74c.74 0 1.3.42 1.3 1.2 0 .76-.56 1.2-1.3 1.2H10.5V8.2zm3.04 7.6H10.5v-2.6h3.04c.83 0 1.46.5 1.46 1.3 0 .8-.63 1.3-1.46 1.3z"/></svg>',
    // Insert table row — 3x3 grid with the middle row highlighted
    trow: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><rect x="3" y="9" width="18" height="6" fill="currentColor" stroke="none" opacity="0.35"/></svg>',
    // Insert table column — 3x3 grid with the middle column highlighted
    tcol: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/><rect x="9" y="3" width="6" height="18" fill="currentColor" stroke="none" opacity="0.35"/></svg>',
    // Delete / remove a post — trash can
    trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></svg>',
    // Modify / edit a post — pencil
    pencil: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>',
  };

  // SMF uses several alternative filenames for the same BBCode icon (e.g.
  // "italicize.gif" instead of "italic.gif", "teletype.gif" for tt, etc.).
  // Map every variant we know about back to the canonical SVG.
  BBCODE_SVGS.italicize = BBCODE_SVGS.italic;
  BBCODE_SVGS.bolden = BBCODE_SVGS.bold;
  BBCODE_SVGS.strikethrough = BBCODE_SVGS.strike;
  BBCODE_SVGS.linethrough = BBCODE_SVGS.strike;
  BBCODE_SVGS.teletype = BBCODE_SVGS.tt;
  BBCODE_SVGS.monospace = BBCODE_SVGS.tt;
  BBCODE_SVGS.fontface = BBCODE_SVGS.font;
  BBCODE_SVGS.fontname = BBCODE_SVGS.font;
  BBCODE_SVGS.colour = BBCODE_SVGS.color;
  BBCODE_SVGS.fontcolour = BBCODE_SVGS.color;
  BBCODE_SVGS.fontcolor = BBCODE_SVGS.color;
  BBCODE_SVGS.fontsize = BBCODE_SVGS.size;
  BBCODE_SVGS.alignleft = BBCODE_SVGS.left;
  BBCODE_SVGS.aligncenter = BBCODE_SVGS.center;
  BBCODE_SVGS.alignright = BBCODE_SVGS.right;
  BBCODE_SVGS.bullist = BBCODE_SVGS.list;
  BBCODE_SVGS.numlist = BBCODE_SVGS.orderlist;
  BBCODE_SVGS.numberedlist = BBCODE_SVGS.orderlist;
  BBCODE_SVGS.image = BBCODE_SVGS.img;
  BBCODE_SVGS.picture = BBCODE_SVGS.img;
  BBCODE_SVGS.link = BBCODE_SVGS.url;
  BBCODE_SVGS.hyperlink = BBCODE_SVGS.url;
  BBCODE_SVGS.mail = BBCODE_SVGS.email;
  BBCODE_SVGS.video = BBCODE_SVGS.youtube;
  BBCODE_SVGS.youtu = BBCODE_SVGS.youtube;
  BBCODE_SVGS.horizontalrule = BBCODE_SVGS.hr;
  BBCODE_SVGS.subscript = BBCODE_SVGS.sub;
  BBCODE_SVGS.superscript = BBCODE_SVGS.sup;
  // bitcointalk-custom filenames (verified from <img src="..."> on bitcointalk):
  BBCODE_SVGS.tele = BBCODE_SVGS.tt;            // tele.gif → Teletype
  BBCODE_SVGS.face = BBCODE_SVGS.font;          // face.gif → Font Face
  BBCODE_SVGS.btc = BBCODE_SVGS.bitcoin;        // BTC.gif  → Insert Bitcoin symbol
  BBCODE_SVGS.tr = BBCODE_SVGS.trow;            // tr.gif   → Insert Table Row
  BBCODE_SVGS.td = BBCODE_SVGS.tcol;            // td.gif   → Insert Table Column
  // Post-action button filenames (also under /frostee/ — prefix is stripped):
  BBCODE_SVGS.delete = BBCODE_SVGS.trash;       // delete.png  → Delete message
  BBCODE_SVGS.remove = BBCODE_SVGS.trash;       // remove.png  → Remove
  BBCODE_SVGS.del = BBCODE_SVGS.trash;
  BBCODE_SVGS.modify = BBCODE_SVGS.pencil;      // modify.png  → Edit / Modify
  BBCODE_SVGS.edit = BBCODE_SVGS.pencil;        // edit.png    → Edit
  BBCODE_SVGS.pen = BBCODE_SVGS.pencil;

  function modernizeBBCodeIcons() {
    const imgs = document.querySelectorAll(
      '#bbcBox_message img, .bbc_buttons img, [id^="bbc_"] img, ' +
      'img[src*="/bbc/"], img[src*="bbc_"], ' +
      // Post-action buttons (Quote / Reply / Modify / Remove) shipped by the
      // bitcointalk "frostee" theme — they're <img class="reply_button"> /
      // <img class="remove_button"> / <img class="modify_button"> with src
      // like /frostee/frostee_quote.png.
      'img[src*="/frostee/"], img[src*="frostee_"], ' +
      'img.reply_button, img.remove_button, img.modify_button, ' +
      'img[class$="_button"]'
    );
    imgs.forEach(img => {
      if (img.dataset.btReplaced === '1') return;
      const src = img.getAttribute('src') || '';
      const m = src.match(/\/(\w+)\.(?:gif|png)/i);
      // Strip common SMF/bitcointalk prefixes so the same SVG map covers
      // bbc_quote, frostee_quote, theme_quote, etc.
      const name = m ? m[1].toLowerCase().replace(/^(bbc|frostee|theme)_/, '') : '';

      // Post-action buttons (in /frostee/ or with class *_button) get a text
      // label instead of an icon — matches the look users expect for the
      // Quote / Reply / Modify / Delete actions next to each post.
      const isPostAction = /\/frostee\//i.test(src) ||
                           img.classList.contains('reply_button') ||
                           img.classList.contains('modify_button') ||
                           img.classList.contains('remove_button');
      if (isPostAction) {
        const POST_ACTION_LABELS = {
          quote: 'Quote',
          edit: 'Edit',
          modify: 'Edit',
          delete: 'Delete',
          remove: 'Delete',
        };
        const label = POST_ACTION_LABELS[name];
        if (label) {
          const span = document.createElement('span');
          span.className = 'bt-post-action';
          span.title = img.alt || img.title || label;
          span.textContent = label;
          span.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            img.click();
          });
          img.dataset.btReplaced = '1';
          img.style.cssText = 'display: none !important;';
          img.parentNode.insertBefore(span, img);
          return;
        }
      }

      const svg = BBCODE_SVGS[name];
      if (!svg) return;

      const span = document.createElement('span');
      span.className = 'bt-bbc-icon';
      span.title = img.alt || img.title || name;
      span.setAttribute('aria-label', span.title);
      span.innerHTML = svg;

      // Forward clicks back to the original <img> so its onclick / handlers fire.
      span.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        img.click();
      });

      img.dataset.btReplaced = '1';
      img.style.cssText = 'display: none !important;';
      img.parentNode.insertBefore(span, img);
    });
  }

  // ===========================================================================
  // Smiley modernisation — replaces classic SMF .gif smileys (e.g. smiley.gif,
  // wink.gif) with native Unicode emoji, which render with the OS's modern
  // emoji font and look infinitely better than 2003-era pixel art.
  // ===========================================================================

  const SMILEYS = {
    smiley: '🙂', happy: '🙂',
    wink: '😉',
    cheesy: '😄', grin: '😀',
    sad: '🙁', cry: '😢',
    shocked: '😮', surprised: '😮',
    cool: '😎', sunglasses: '😎',
    huh: '🤔', confused: '😕', undecided: '😕',
    tongue: '😛',
    embarrassed: '😳',
    lipsrsealed: '🤐',
    angry: '😠', mad: '😡',
    kiss: '😘',
    rolleyes: '🙄',
    evil: '😈',
    azn: '😏',
    afro: '🕺',
    laugh: '😆', lol: '😂',
    police: '👮',
    wassat: '🤨',
    chocked: '😱',
    skeptical: '🤨',
    facepalm: '🤦',
    thumbsup: '👍', thumbup: '👍',
    thumbsdown: '👎', thumbdown: '👎',
    heart: '❤️',
    star: '⭐',
    fire: '🔥',
    check: '✅', tick: '✅',
    cross: '❌',
  };

  // Add the .bt-post-action class to +Merit links and message-number anchors
  // (#1, #2, ...) so they pick up the same pill styling as the new Quote/Edit
  // /Delete buttons. Selectors must be strict to avoid false positives like
  // the "Earned Merit" profile stat label or the BPIP "#50" rank value.
  function stylePostMetaButtons() {
    document.querySelectorAll('a').forEach(a => {
      if (a.dataset.btPostMeta === '1') return;
      const text = (a.textContent || '').trim();
      const href = a.getAttribute('href') || '';

      // +Merit must literally start with "+"  (excludes "Earned Merit", etc.)
      const isMerit = /^\+\s*merit\b/i.test(text);

      // Message numbers must be exactly "#N" AND link to a topic / specific
      // post (excludes BPIP Rank "#50", external rank links, etc.).
      const isMsgNum = /^#\d+$/.test(text) &&
        /(topic=|#msg|action=display|action=profile)/i.test(href);

      if (isMerit || isMsgNum) {
        a.classList.add('bt-post-action');
        a.dataset.btPostMeta = '1';
      }
    });
  }

  function modernizeSmileys() {
    const imgs = document.querySelectorAll(
      'img[src*="Smileys/"], img[src*="/smileys/"], img.smiley, ' +
      'img[alt*=":"], img[alt^=";"], img[alt*="("], img[alt*=")"]'
    );
    imgs.forEach(img => {
      if (img.dataset.btSmiley === '1') return;
      const src = img.getAttribute('src') || '';
      // Only touch images that actually look like smileys (path or class)
      if (!/(Smileys|smileys|smiley)/i.test(src) && !img.classList.contains('smiley')) return;
      const m = src.match(/\/(\w+)\.(?:gif|png|jpg)/i);
      const name = m ? m[1].toLowerCase() : '';
      const emoji = SMILEYS[name];
      if (!emoji) return;

      const span = document.createElement('span');
      span.className = 'bt-smiley';
      span.textContent = emoji;
      span.title = img.alt || img.title || name;

      img.dataset.btSmiley = '1';
      img.style.cssText = 'display: none !important;';
      img.parentNode.insertBefore(span, img);
    });
  }

  // ===========================================================================
  // CSS
  // ===========================================================================

  const css = `
    /* ===== Theme tokens — DARK fallback (overridden inline by JS) ===== */
    :root[data-bt-theme="dark"] {
      --bt-bg: #1A1D21;
      --bt-surface-1: #22262B;
      --bt-surface-2: #2A2F35;
      --bt-surface-3: #343A41;
      --bt-border: rgba(255,255,255,0.07);
      --bt-border-2: rgba(255,255,255,0.14);
      --bt-text: #E3E3E8;
      --bt-text-2: #A8B0BA;
      --bt-text-3: #6B7480;
      --bt-input-bg: #14171B;
      --bt-quote-bg: #1F2328;
      --bt-code-bg: #14171B;
      --bt-bg-end: #17191D;
      --bt-icon-filter: invert(1) opacity(0.75);
      --bt-shadow: 0 1px 0 rgba(0,0,0,0.2);
      --bt-shadow-lg: 0 12px 32px rgba(0,0,0,0.35);
    }

    /* ===== Theme tokens — LIGHT fallback (overridden inline by JS) =====
       Surfaces are progressively darker tints of the main bg (so the user's
       chosen colour shows through every container). Only --bt-input-bg stays
       pure white, used by text inputs/textareas. */
    :root[data-bt-theme="light"] {
      --bt-bg: #F2F4F7;
      --bt-surface-1: #E3E6EA;
      --bt-surface-2: #D7DAE0;
      --bt-surface-3: #C5C8CD;
      --bt-border: rgba(0,0,0,0.08);
      --bt-border-2: rgba(0,0,0,0.16);
      --bt-text: #1A1D21;
      --bt-text-2: #4A5058;
      --bt-text-3: #7A8088;
      --bt-input-bg: #FFFFFF;
      --bt-quote-bg: #E1E4E8;
      --bt-code-bg: #DBDEE3;
      --bt-bg-end: #E9ECEF;
      --bt-icon-filter: opacity(0.7);
      --bt-shadow: 0 1px 0 rgba(0,0,0,0.04);
      --bt-shadow-lg: 0 12px 32px rgba(0,0,0,0.12);
    }

    /* ===== Shared shape tokens ===== */
    :root {
      --bt-radius: 10px;
      --bt-radius-sm: 6px;
      --bt-radius-lg: 14px;
      --bt-font: 'Source Sans 3', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      --bt-mono: 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;
      --bt-trans: 150ms ease;
      --bt-danger: #EF4444;

      /* Default accent values — overridden by JS based on saved preference. */
      --bt-accent: #F7931A;
      --bt-accent-h: #FFA733;
      --bt-accent-a: #E08410;
      --bt-on-accent: #1A1D21;
    }

    /* ===== Base ===== */
    html, body {
      background: var(--bt-bg) !important;
      color: var(--bt-text) !important;
      font-family: var(--bt-font) !important;
      font-size: 14px !important;
      -webkit-font-smoothing: antialiased;
      text-rendering: optimizeLegibility;
    }

    body {
      background:
        radial-gradient(140% 100% at 15% -10%, color-mix(in srgb, var(--bt-accent) 6%, transparent) 0%, transparent 55%),
        radial-gradient(120% 100% at 90% -5%,  color-mix(in srgb, var(--bt-accent) 3%, transparent) 0%, transparent 60%),
        linear-gradient(180deg, var(--bt-bg) 0%, var(--bt-bg-end) 100%) !important;
      background-attachment: fixed !important;
    }

    h1, h2, h3, h4, h5, h6 {
      color: var(--bt-text) !important;
      font-weight: 600 !important;
      letter-spacing: -0.005em;
    }

    a, a:link, a:visited {
      color: var(--bt-text) !important;
      text-decoration: none !important;
      transition: color var(--bt-trans) !important;
    }
    a:hover, a:active, a:focus {
      color: var(--bt-accent) !important;
      text-decoration: none !important;
    }

    #upper_section, #header, #wrapper, #content_section,
    #main_content_section, #footer_section, .frame,
    #bodyarea, .navigate_section, .roundframe, #content {
      background: transparent !important;
      color: var(--bt-text) !important;
    }

    /* ===== Tables ===== */
    table.bordercolor,
    table.table_list,
    table.table_grid,
    .tborder {
      background: var(--bt-surface-1) !important;
      border: 1px solid var(--bt-border-2) !important;
      border-radius: var(--bt-radius) !important;
      overflow: hidden !important;
      border-spacing: 0 !important;
      box-shadow: var(--bt-shadow);
    }

    [class*="windowbg"],
    td[class*="windowbg"],
    tr[class*="windowbg"] > td {
      background-color: var(--bt-surface-1) !important;
      background-image: none !important;
      color: var(--bt-text) !important;
      border-color: var(--bt-border) !important;
      border-bottom: 1px solid var(--bt-border) !important;
    }
    tr.windowbg:hover > td, tr.windowbg2:hover > td {
      background-color: var(--bt-surface-2) !important;
    }

    .poster, td.poster, .poster_info, td.poster_info,
    [id*="poster"] {
      background-color: var(--bt-surface-1) !important;
      background-image: none !important;
      color: var(--bt-text) !important;
    }

    table.bordercolor tr[style] > td,
    table.bordercolor td[style],
    .tborder tr[style] > td,
    .tborder td[style] {
      background-color: var(--bt-surface-1) !important;
      background-image: none !important;
    }

    /* ===== Category / title / topic-header bars =====
       bitcointalk applies background-image to TDs *inside* tr.catbg / tr.catbg3,
       so we need the descendant selector tr[class*="catbg"] td. */
    [class*="catbg"], [class*="titlebg"],
    td[class*="catbg"], td[class*="titlebg"],
    th[class*="catbg"], th[class*="titlebg"],
    tr[class*="catbg"] > td, tr[class*="catbg"] > th,
    tr[class*="catbg"] td, tr[class*="catbg"] th,
    tr[class*="titlebg"] > td, tr[class*="titlebg"] > th,
    tr[class*="titlebg"] td, tr[class*="titlebg"] th,
    h3[class*="catbg"], h4[class*="catbg"],
    h3[class*="titlebg"], h4[class*="titlebg"] {
      background-color: var(--bt-surface-2) !important;
      background-image: none !important;
      color: var(--bt-text) !important;
      border-bottom: 1px solid var(--bt-border-2) !important;
      font-weight: 600 !important;
      letter-spacing: 0.01em;
      text-shadow: none !important;
    }
    [class*="catbg"] a, [class*="titlebg"] a,
    tr[class*="catbg"] a, tr[class*="titlebg"] a {
      color: var(--bt-text) !important;
    }
    [class*="catbg"] a:hover, [class*="titlebg"] a:hover,
    tr[class*="catbg"] a:hover, tr[class*="titlebg"] a:hover {
      color: var(--bt-accent) !important;
    }

    th {
      background-color: var(--bt-surface-2) !important;
      background-image: none !important;
      color: var(--bt-text) !important;
    }

    /* ===== Posts =====
       bitcointalk has a rule '.windowbg, #preview_body { background: #ECEDF3 }'.
       The wildcard above only catches the .windowbg side; the post-body cell
       uses class="post" id="preview_body", so we override it explicitly here.
       Same for personal-message bodies and the preview-section container. */
    .post,
    td.post,
    #preview_body,
    #preview_section,
    #preview_section td,
    #preview_section table,
    .personalmessage,
    td.personalmessage {
      background-color: var(--bt-surface-1) !important;
      background-image: none !important;
      color: var(--bt-text) !important;
    }
    .post {
      line-height: 1.6 !important;
      font-size: 14px !important;
    }
    .signature {
      color: var(--bt-text-3) !important;
      border-top: 1px solid var(--bt-border) !important;
      padding-top: 8px !important;
      margin-top: 12px !important;
      font-size: 12px !important;
    }
    .smalltext { color: var(--bt-text-3) !important; }

    .quoteheader, .codeheader {
      background: var(--bt-surface-3) !important;
      color: var(--bt-text-2) !important;
      border: 1px solid var(--bt-border) !important;
      border-bottom: none !important;
      border-radius: var(--bt-radius-sm) var(--bt-radius-sm) 0 0 !important;
      padding: 6px 10px !important;
      font-size: 12px !important;
      font-weight: 500 !important;
    }
    .quote {
      background: var(--bt-quote-bg) !important;
      color: var(--bt-text-2) !important;
      border: 1px solid var(--bt-border) !important;
      border-left: 3px solid var(--bt-accent) !important;
      border-radius: 0 var(--bt-radius-sm) var(--bt-radius-sm) var(--bt-radius-sm) !important;
      padding: 10px 14px !important;
      margin: 6px 0 12px !important;
    }
    .code {
      background: var(--bt-code-bg) !important;
      color: var(--bt-text) !important;
      font-family: var(--bt-mono) !important;
      border: 1px solid var(--bt-border) !important;
      border-radius: 0 0 var(--bt-radius-sm) var(--bt-radius-sm) !important;
      padding: 10px 14px !important;
    }

    /* ===== Buttons (excluding the switcher) ===== */
    .button_submit, .button_reset,
    input[type="submit"], input[type="button"], input[type="reset"],
    button:not([id^="bt-"]):not([class^="bt-sw-"]),
    .button:not([id^="bt-"]) {
      background: var(--bt-accent) !important;
      color: var(--bt-on-accent) !important;
      border: 1px solid var(--bt-accent) !important;
      border-radius: var(--bt-radius-sm) !important;
      padding: 8px 14px !important;
      font-family: var(--bt-font) !important;
      font-weight: 600 !important;
      font-size: 13px !important;
      cursor: pointer !important;
      transition: background var(--bt-trans), transform var(--bt-trans), box-shadow var(--bt-trans) !important;
      box-shadow: none !important;
      text-shadow: none !important;
    }
    .button_submit:hover, .button_reset:hover,
    input[type="submit"]:hover, input[type="button"]:hover, input[type="reset"]:hover,
    button:not([id^="bt-"]):not([class^="bt-sw-"]):hover,
    .button:not([id^="bt-"]):hover {
      background: var(--bt-accent-h) !important;
      border-color: var(--bt-accent-h) !important;
      box-shadow: 0 2px 12px color-mix(in srgb, var(--bt-accent) 30%, transparent) !important;
      transform: translateY(-1px);
      color: var(--bt-on-accent) !important;
    }
    .button_submit:active, input[type="submit"]:active {
      background: var(--bt-accent-a) !important;
      transform: translateY(0) !important;
    }

    a.button {
      background: transparent !important;
      color: var(--bt-text) !important;
      border: 1px solid var(--bt-border-2) !important;
    }
    a.button:hover {
      border-color: var(--bt-accent) !important;
      color: var(--bt-accent) !important;
      background: color-mix(in srgb, var(--bt-accent) 10%, transparent) !important;
      box-shadow: none !important;
      transform: none !important;
    }

    /* Post-action buttons — Quote / Reply / Modify / Remove / Report.
       Classic SMF renders them as small <a> with explicit classes. We DO NOT
       use a broad ".button_strip a" / ".postbuttons a" selector here because
       those wrappers may contain anchors that bitcointalk hides on purpose
       (e.g. a "Copy" link), and forcing display: inline-block on them would
       reveal something the original site doesn't show. */
    a.quotebutton, a.replybutton, a.modifybutton, a.removebutton,
    a.reportbutton, a.splitbutton, a.notifybutton, a.markreadbutton,
    a.unwatchbutton, a.printbutton, a.archivebutton {
      display: inline-block !important;
      background: transparent !important;
      color: var(--bt-text-2) !important;
      border: 1px solid var(--bt-border-2) !important;
      border-radius: var(--bt-radius-sm) !important;
      padding: 4px 10px !important;
      margin: 0 4px 0 0 !important;
      font-family: var(--bt-font) !important;
      font-weight: 500 !important;
      font-size: 11px !important;
      text-transform: uppercase !important;
      letter-spacing: 0.04em !important;
      text-decoration: none !important;
      box-shadow: none !important;
      text-shadow: none !important;
      transition: background var(--bt-trans), color var(--bt-trans), border-color var(--bt-trans) !important;
      cursor: pointer !important;
    }
    a.quotebutton:hover, a.replybutton:hover, a.modifybutton:hover,
    a.removebutton:hover, a.reportbutton:hover, a.splitbutton:hover,
    a.notifybutton:hover, a.markreadbutton:hover, a.unwatchbutton:hover,
    a.printbutton:hover, a.archivebutton:hover {
      background: var(--bt-accent) !important;
      color: var(--bt-on-accent) !important;
      border-color: var(--bt-accent) !important;
      transform: none !important;
      box-shadow: none !important;
    }

    /* .bt-post-action — text-label replacement for the frostee post-action
       buttons (Quote / Edit / Delete). Matches the .quotebutton style above
       so they look consistent next to each other. */
    .bt-post-action {
      display: inline-block !important;
      padding: 4px 10px !important;
      margin: 0 4px 0 0 !important;
      background: transparent !important;
      color: var(--bt-text-2) !important;
      border: 1px solid var(--bt-border-2) !important;
      border-radius: var(--bt-radius-sm) !important;
      font-family: var(--bt-font) !important;
      font-weight: 500 !important;
      font-size: 11px !important;
      text-transform: uppercase !important;
      letter-spacing: 0.04em !important;
      cursor: pointer !important;
      vertical-align: middle !important;
      transition: background var(--bt-trans), color var(--bt-trans), border-color var(--bt-trans) !important;
    }
    .bt-post-action:hover {
      background: var(--bt-accent) !important;
      color: var(--bt-on-accent) !important;
      border-color: var(--bt-accent) !important;
      text-decoration: none !important;
    }
    a.bt-post-action, a.bt-post-action:link, a.bt-post-action:visited {
      color: var(--bt-text-2) !important;
    }
    a.bt-post-action:hover { color: var(--bt-on-accent) !important; }

    /* "Copy" / "Quote to clipboard" — added by some bitcointalk userscripts
       or theme variants but not part of the original forum UI. Hide it. */
    span[title="Quote to clipboard"] {
      display: none !important;
    }

    /* ===== Inputs ===== */
    input[type="text"], input[type="password"], input[type="email"],
    input[type="search"], input[type="url"], input[type="number"],
    textarea, select {
      background: var(--bt-input-bg) !important;
      color: var(--bt-text) !important;
      border: 1px solid var(--bt-border-2) !important;
      border-radius: var(--bt-radius-sm) !important;
      padding: 8px 10px !important;
      font-family: var(--bt-font) !important;
      font-size: 13px !important;
      transition: border-color var(--bt-trans), box-shadow var(--bt-trans) !important;
      outline: none !important;
    }
    input[type="text"]:focus, input[type="password"]:focus,
    input[type="email"]:focus, input[type="search"]:focus,
    textarea:focus, select:focus {
      border-color: var(--bt-accent) !important;
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--bt-accent) 20%, transparent) !important;
    }
    textarea {
      min-height: 120px !important;
      line-height: 1.5 !important;
      font-family: var(--bt-mono) !important;
    }

    #bbcBox_message, .bbc_buttons, #post_modify, .post_buttons {
      background: var(--bt-surface-2) !important;
      border: 1px solid var(--bt-border) !important;
      border-radius: var(--bt-radius-sm) !important;
      padding: 6px !important;
    }
    #bbcBox_message img, .bbc_buttons img {
      filter: var(--bt-icon-filter) !important;
      transition: filter var(--bt-trans) !important;
    }
    #bbcBox_message img:hover, .bbc_buttons img:hover { filter: none !important; }

    /* Modern Lucide-style BBCode icons (injected by modernizeBBCodeIcons()) */
    .bt-bbc-icon {
      display: inline-flex !important;
      align-items: center !important;
      justify-content: center !important;
      width: 26px !important;
      height: 26px !important;
      margin: 1px !important;
      padding: 4px !important;
      border-radius: var(--bt-radius-sm) !important;
      color: var(--bt-text-2) !important;
      background: transparent !important;
      cursor: pointer !important;
      vertical-align: middle !important;
      transition: background var(--bt-trans), color var(--bt-trans) !important;
      box-sizing: border-box !important;
    }
    .bt-bbc-icon svg {
      width: 100% !important;
      height: 100% !important;
      display: block !important;
    }
    .bt-bbc-icon:hover {
      background: color-mix(in srgb, var(--bt-accent) 18%, transparent) !important;
      color: var(--bt-accent) !important;
    }

    /* Native-emoji smiley replacements (injected by modernizeSmileys()) */
    .bt-smiley {
      display: inline-block !important;
      font-size: 1.15em !important;
      line-height: 1 !important;
      vertical-align: -0.12em !important;
      font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
                   "Twemoji Mozilla", sans-serif !important;
    }

    /* ===== Top nav (HOME / HELP / SEARCH / ...) ===== */

    /* Tabs sit edge-to-edge: zero margin, no border-radius on inner tabs,
       only the outer-left of the first visible tab and outer-right of the
       last visible tab keep rounded corners (handled below). The right border
       of each tab doubles as the divider with the next one. */
    [class*="maintab_"] {
      display: inline-block !important;
      margin: 0 !important;
      padding: 0 !important;
      border-radius: 0 !important;
      background-color: var(--bt-surface-2) !important;
      background-image: none !important;
      border: 1px solid var(--bt-border) !important;
      border-left: none !important;
      box-shadow: none !important;
      text-shadow: none !important;
      transition: background-color var(--bt-trans), border-color var(--bt-trans) !important;
      vertical-align: middle !important;
      line-height: 1 !important;
      width: auto !important;
      min-width: 0 !important;
      height: auto !important;
    }
    /* Restore left border + left rounding on the first visible tab.
       Both the inactive .maintab_first and the active .maintab_active_back
       can be the leftmost depending on which tab is selected. */
    .maintab_first,
    .maintab_actfirst + .maintab_active_back {
      border-left: 1px solid var(--bt-border) !important;
      border-top-left-radius: var(--bt-radius-sm) !important;
      border-bottom-left-radius: var(--bt-radius-sm) !important;
    }
    .maintab_last {
      border-top-right-radius: var(--bt-radius-sm) !important;
      border-bottom-right-radius: var(--bt-radius-sm) !important;
    }
    [class*="maintab_"] a,
    [class*="maintab_"] a:link,
    [class*="maintab_"] a:visited {
      display: inline-block !important;
      padding: 6px 14px !important;
      margin: 0 !important;
      color: var(--bt-text-2) !important;
      font-family: var(--bt-font) !important;
      font-weight: 500 !important;
      font-size: 12px !important;
      text-transform: uppercase !important;
      letter-spacing: 0.05em !important;
      transition: color var(--bt-trans) !important;
      background: transparent !important;
      background-image: none !important;
      border: none !important;
      box-shadow: none !important;
      text-shadow: none !important;
      line-height: 1 !important;
    }
    .maintab_first:hover, .maintab_back:hover, .maintab_last:hover {
      background-color: var(--bt-surface-3) !important;
      border-color: var(--bt-border-2) !important;
    }
    .maintab_first:hover a, .maintab_back:hover a, .maintab_last:hover a {
      color: var(--bt-text) !important;
    }
    /* Active (current page) — only .maintab_active_back has the text. */
    .maintab_active_back {
      background-color: var(--bt-accent) !important;
      border-color: var(--bt-accent) !important;
    }
    .maintab_active_back a,
    .maintab_active_back a:hover {
      color: var(--bt-on-accent) !important;
      font-weight: 600 !important;
    }

    /* Hide the empty decorative caps used by classic SMF. Placed AFTER the
       broad [class*="maintab_"] rule above so that with same specificity (both
       single-class), declaration order makes display:none win. The doubled-class
       selector also bumps specificity to (0,2,0) for safety against any other
       maintab rule that might reapply display:inline-block. */
    .maintab_actfirst.maintab_actfirst,
    .maintab_actlast.maintab_actlast {
      display: none !important;
    }

    #main_menu, .dropmenu {
      background: transparent !important;
      border: none !important;
    }
    #main_menu li, .dropmenu li {
      background: var(--bt-surface-2) !important;
      border: 1px solid var(--bt-border) !important;
      border-radius: var(--bt-radius-sm) !important;
      margin-right: 4px !important;
    }
    #main_menu li.chosen, .dropmenu li.chosen,
    #main_menu li.active, .dropmenu li.active {
      background: var(--bt-accent) !important;
      border-color: var(--bt-accent) !important;
    }
    #main_menu li a, .dropmenu li a {
      color: var(--bt-text-2) !important;
      padding: 6px 14px !important;
      font-size: 12px !important;
      text-transform: uppercase !important;
      letter-spacing: 0.05em !important;
    }
    #main_menu li.chosen a, .dropmenu li.chosen a,
    #main_menu li.active a, .dropmenu li.active a {
      color: var(--bt-on-accent) !important;
      font-weight: 600 !important;
    }

    #upper_section {
      border-bottom: 1px solid var(--bt-border) !important;
      position: relative !important;
    }
    #top_section, #upshrink_header,
    .navigate_section ul, .navigate_section {
      background: transparent !important;
      color: var(--bt-text-2) !important;
    }
    .navigate_section a, .navigate_section a:link, .navigate_section a:visited {
      color: var(--bt-text-2) !important;
    }
    .navigate_section a:hover { color: var(--bt-accent) !important; }
    .navigate_section .last a, .navigate_section li:last-child a {
      color: var(--bt-text) !important;
      font-weight: 600 !important;
    }
    #header img, #logo { filter: brightness(1.05) !important; }

    hr {
      border: none !important;
      border-top: 1px solid var(--bt-border) !important;
      margin: 16px 0 !important;
    }

    #footer_section, .copywrite {
      background: transparent !important;
      color: var(--bt-text-3) !important;
      border-top: 1px solid var(--bt-border) !important;
    }
    #footer_section a, .copywrite a { color: var(--bt-text-2) !important; }
    #footer_section a:hover, .copywrite a:hover { color: var(--bt-accent) !important; }

    .information, .descbox, .infobox {
      background: var(--bt-surface-2) !important;
      color: var(--bt-text-2) !important;
      border: 1px solid var(--bt-border) !important;
      border-radius: var(--bt-radius-sm) !important;
      padding: 10px 12px !important;
    }
    .errorbox, .noticebox {
      background: color-mix(in srgb, var(--bt-danger) 12%, transparent) !important;
      border: 1px solid color-mix(in srgb, var(--bt-danger) 40%, transparent) !important;
      color: var(--bt-danger) !important;
      border-radius: var(--bt-radius-sm) !important;
      padding: 10px 12px !important;
    }

    .pagelinks { color: var(--bt-text-2) !important; }
    .pagelinks a, .pagelinks a:link, .pagelinks a:visited {
      color: var(--bt-text-2) !important;
      padding: 4px 8px !important;
      border-radius: var(--bt-radius-sm) !important;
      transition: background var(--bt-trans), color var(--bt-trans) !important;
    }
    .pagelinks a:hover {
      background: color-mix(in srgb, var(--bt-accent) 12%, transparent) !important;
      color: var(--bt-accent) !important;
    }
    .pagelinks strong {
      background: var(--bt-accent) !important;
      color: var(--bt-on-accent) !important;
      padding: 4px 8px !important;
      border-radius: var(--bt-radius-sm) !important;
    }

    .poster h4 a, .poster h4 a:link, .poster h4 a:visited {
      color: var(--bt-text) !important;
      font-weight: 600 !important;
    }
    .poster h4 a:hover { color: var(--bt-accent) !important; }
    .poster ul, .poster li {
      color: var(--bt-text-3) !important;
      font-size: 12px !important;
    }
    .avatar, img.avatar {
      border-radius: var(--bt-radius-sm) !important;
      border: 1px solid var(--bt-border) !important;
    }

    * {
      scrollbar-width: thin;
      scrollbar-color: var(--bt-surface-3) var(--bt-bg);
    }
    *::-webkit-scrollbar { width: 10px; height: 10px; }
    *::-webkit-scrollbar-track { background: var(--bt-bg); }
    *::-webkit-scrollbar-thumb {
      background: var(--bt-surface-3);
      border-radius: 5px;
      border: 2px solid var(--bt-bg);
    }
    *::-webkit-scrollbar-thumb:hover { background: var(--bt-accent); }

    ::selection {
      background: color-mix(in srgb, var(--bt-accent) 35%, transparent);
      color: var(--bt-text);
    }

    img[src*="bullet"], img[src*="off"], img[src*="on"] { opacity: 0.85; }

    select {
      appearance: none !important;
      -webkit-appearance: none !important;
      background-image:
        linear-gradient(45deg, transparent 50%, var(--bt-text-2) 50%),
        linear-gradient(135deg, var(--bt-text-2) 50%, transparent 50%) !important;
      background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50% !important;
      background-size: 5px 5px, 5px 5px !important;
      background-repeat: no-repeat !important;
      padding-right: 28px !important;
    }

    /* =========================================================================
       Theme switcher — injected into the forum top bar
       ========================================================================= */
    #bt-switcher {
      position: absolute !important;
      top: 8px !important;
      right: 8px !important;
      z-index: 10000 !important;
      font-family: var(--bt-font) !important;
      font-size: 13px !important;
      color: var(--bt-text) !important;
      line-height: 1 !important;
    }

    #bt-switcher-button {
      display: flex !important;
      align-items: center !important;
      justify-content: center !important;
      width: 32px !important;
      height: 32px !important;
      padding: 0 !important;
      margin: 0 !important;
      border-radius: 50% !important;
      background: var(--bt-surface-2) !important;
      color: var(--bt-text-2) !important;
      border: 1px solid var(--bt-border-2) !important;
      cursor: pointer !important;
      transition: background var(--bt-trans), color var(--bt-trans), transform 200ms ease !important;
      box-shadow: var(--bt-shadow) !important;
      text-shadow: none !important;
      font: inherit !important;
      text-transform: none !important;
      letter-spacing: 0 !important;
    }
    #bt-switcher-button:hover {
      background: var(--bt-surface-3) !important;
      color: var(--bt-text) !important;
      transform: rotate(20deg);
      box-shadow: var(--bt-shadow) !important;
    }
    #bt-switcher-button svg { width: 18px; height: 18px; }

    #bt-switcher-panel {
      position: absolute !important;
      top: 40px !important;
      right: 0 !important;
      min-width: 280px !important;
      background: var(--bt-surface-1) !important;
      border: 1px solid var(--bt-border-2) !important;
      border-radius: var(--bt-radius) !important;
      padding: 14px !important;
      box-shadow: var(--bt-shadow-lg) !important;
      display: none !important;
      color: var(--bt-text) !important;
    }
    #bt-switcher.open #bt-switcher-panel { display: block !important; }

    .bt-sw-section { margin-bottom: 14px; }
    .bt-sw-section:last-child { margin-bottom: 0; }
    .bt-sw-label {
      display: block !important;
      font-size: 10px !important;
      font-weight: 600 !important;
      text-transform: uppercase !important;
      letter-spacing: 0.08em !important;
      color: var(--bt-text-3) !important;
      margin-bottom: 8px !important;
    }
    .bt-sw-row {
      display: flex !important;
      gap: 6px !important;
      align-items: center !important;
      flex-wrap: wrap !important;
    }

    .bt-sw-theme-btn {
      flex: 1 !important;
      min-width: 0 !important;
      display: inline-flex !important;
      align-items: center !important;
      justify-content: center !important;
      gap: 5px !important;
      padding: 8px 6px !important;
      margin: 0 !important;
      background: var(--bt-surface-2) !important;
      color: var(--bt-text-2) !important;
      border: 1px solid var(--bt-border) !important;
      border-radius: var(--bt-radius-sm) !important;
      cursor: pointer !important;
      font-family: var(--bt-font) !important;
      font-size: 11px !important;
      font-weight: 500 !important;
      text-transform: none !important;
      letter-spacing: 0 !important;
      text-shadow: none !important;
      box-shadow: none !important;
      transition: background var(--bt-trans), color var(--bt-trans), border-color var(--bt-trans) !important;
      line-height: 1 !important;
    }
    .bt-sw-theme-btn:hover {
      background: var(--bt-surface-3) !important;
      color: var(--bt-text) !important;
      transform: none !important;
    }
    .bt-sw-theme-btn.selected {
      background: var(--bt-accent) !important;
      color: var(--bt-on-accent) !important;
      border-color: var(--bt-accent) !important;
    }
    .bt-sw-theme-btn svg { width: 14px; height: 14px; }

    /* "Customise [Light|Dark] Colours" sub-panel */
    .bt-sw-customize {
      margin-top: 14px;
      padding-top: 14px;
      border-top: 1px solid var(--bt-border);
    }
    .bt-sw-customize-title {
      display: block;
      text-align: center;
      font-size: 11px;
      font-weight: 600;
      letter-spacing: 0.05em;
      color: var(--bt-text);
      margin-bottom: 12px;
    }

    .bt-sw-swatch {
      width: 26px !important;
      height: 26px !important;
      padding: 0 !important;
      margin: 0 !important;
      border-radius: 50% !important;
      border: 2px solid transparent !important;
      cursor: pointer !important;
      transition: transform var(--bt-trans), border-color var(--bt-trans) !important;
      box-shadow: inset 0 0 0 1px rgba(0,0,0,0.15) !important;
      font-size: 0 !important;
      text-shadow: none !important;
    }
    .bt-sw-swatch:hover { transform: scale(1.12) !important; }
    .bt-sw-swatch.selected { border-color: var(--bt-text) !important; }

    /* Inline custom-colour swatch (multicolour gradient → opens a colour picker) */
    .bt-sw-custom-wrap {
      position: relative;
      display: inline-block;
      width: 26px;
      height: 26px;
    }
    .bt-sw-custom-wrap input[type="color"] {
      position: absolute !important;
      inset: 0 !important;
      width: 100% !important;
      height: 100% !important;
      padding: 0 !important;
      margin: 0 !important;
      border: 0 !important;
      border-radius: 50% !important;
      background: transparent !important;
      cursor: pointer !important;
      opacity: 0 !important;
      box-shadow: none !important;
    }
    .bt-sw-custom-display {
      width: 26px;
      height: 26px;
      border-radius: 50%;
      border: 2px solid transparent;
      pointer-events: none;
      box-shadow: inset 0 0 0 1px rgba(0,0,0,0.15);
      transition: border-color var(--bt-trans);
      background: conic-gradient(#ef4444, #f59e0b, #10b981, #3b82f6, #8b5cf6, #ef4444);
      display: flex;
      align-items: center;
      justify-content: center;
      color: #FFFFFF;
      font-weight: 700;
      font-size: 14px;
      line-height: 1;
      text-shadow: 0 1px 2px rgba(0,0,0,0.5);
    }
    .bt-sw-custom-display::before {
      content: '+';
    }
    .bt-sw-custom-wrap.selected .bt-sw-custom-display::before {
      content: '';
    }
    .bt-sw-custom-wrap.selected .bt-sw-custom-display {
      border-color: var(--bt-text);
    }
  `;

  // ===========================================================================
  // CSS injection
  // ===========================================================================

  function injectCSS() {
    if (typeof GM_addStyle === 'function') {
      GM_addStyle(css);
    } else {
      const style = document.createElement('style');
      style.id = 'bt-modern-theme-css';
      style.textContent = css;
      (document.head || document.documentElement).appendChild(style);
    }

    if (!document.getElementById('bt-fonts')) {
      const link = document.createElement('link');
      link.id = 'bt-fonts';
      link.rel = 'stylesheet';
      link.href =
        'https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700' +
        '&family=JetBrains+Mono:wght@400;500&display=swap';
      (document.head || document.documentElement).appendChild(link);
    }
  }

  // ===========================================================================
  // Switcher UI
  // ===========================================================================

  const ICONS = {
    cog: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
    sun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>',
    moon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>',
    monitor: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>',
  };

  function buildSwitcher(host) {
    if (document.getElementById('bt-switcher')) return;

    const container = document.createElement('div');
    container.id = 'bt-switcher';
    container.innerHTML = `
      <button id="bt-switcher-button" type="button" title="Theme settings" aria-label="Theme settings">
        ${ICONS.cog}
      </button>
      <div id="bt-switcher-panel" role="dialog" aria-label="Theme settings">
        <div class="bt-sw-section">
          <span class="bt-sw-label">Mode</span>
          <div class="bt-sw-row">
            <button type="button" class="bt-sw-theme-btn" data-theme="light" title="Light">${ICONS.sun}<span>Light</span></button>
            <button type="button" class="bt-sw-theme-btn" data-theme="dark" title="Dark">${ICONS.moon}<span>Dark</span></button>
            <button type="button" class="bt-sw-theme-btn" data-theme="system" title="Follow system">${ICONS.monitor}<span>Auto</span></button>
          </div>
        </div>

        <div class="bt-sw-customize">
          <span class="bt-sw-customize-title" id="bt-sw-customize-title">Customise Colours</span>

          <div class="bt-sw-section">
            <span class="bt-sw-label">Main</span>
            <div class="bt-sw-row" id="bt-sw-main-row"></div>
          </div>

          <div class="bt-sw-section">
            <span class="bt-sw-label">Accent</span>
            <div class="bt-sw-row" id="bt-sw-accent-row"></div>
          </div>
        </div>
      </div>
    `;
    host.appendChild(container);

    const btn = container.querySelector('#bt-switcher-button');

    // Open/close panel
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      container.classList.toggle('open');
    });
    document.addEventListener('click', (e) => {
      if (!container.contains(e.target)) container.classList.remove('open');
    });
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') container.classList.remove('open');
    });

    // Theme buttons
    container.querySelectorAll('.bt-sw-theme-btn').forEach(b => {
      b.addEventListener('click', () => {
        setPref(KEY_THEME, b.dataset.theme);
        applyTheme();
      });
    });

    // Delegated swatch clicks (rows are rebuilt on theme change)
    function handleSwatchClick(kind) {
      return (e) => {
        const swatch = e.target.closest(`[data-${kind}]`);
        if (!swatch || swatch.tagName === 'INPUT') return;
        const themeNow = getEffectiveTheme(getPref(KEY_THEME, DEFAULT_THEME));
        const storageKey = kind === 'main'
          ? (themeNow === 'dark' ? KEY_MAIN_DARK : KEY_MAIN_LIGHT)
          : (themeNow === 'dark' ? KEY_ACCENT_DARK : KEY_ACCENT_LIGHT);
        setPref(storageKey, swatch.dataset[kind]);
        applyTheme();
      };
    }
    container.querySelector('#bt-sw-main-row').addEventListener('click', handleSwatchClick('main'));
    container.querySelector('#bt-sw-accent-row').addEventListener('click', handleSwatchClick('accent'));

    // Delegated colour-picker input (rows are rebuilt on theme change)
    function handleColorInput(kind) {
      return (e) => {
        if (!e.target.matches(`input[type="color"][data-target="${kind}"]`)) return;
        const themeNow = getEffectiveTheme(getPref(KEY_THEME, DEFAULT_THEME));
        const storageKey = kind === 'main'
          ? (themeNow === 'dark' ? KEY_MAIN_DARK : KEY_MAIN_LIGHT)
          : (themeNow === 'dark' ? KEY_ACCENT_DARK : KEY_ACCENT_LIGHT);
        const customStorageKey = kind === 'main'
          ? (themeNow === 'dark' ? KEY_MAIN_DARK_CUSTOM : KEY_MAIN_LIGHT_CUSTOM)
          : (themeNow === 'dark' ? KEY_ACCENT_DARK_CUSTOM : KEY_ACCENT_LIGHT_CUSTOM);
        setPref(customStorageKey, e.target.value);
        setPref(storageKey, 'custom');
        applyTheme();
      };
    }
    container.querySelector('#bt-sw-main-row').addEventListener('input', handleColorInput('main'));
    container.querySelector('#bt-sw-accent-row').addEventListener('input', handleColorInput('accent'));

    applyTheme();
  }

  function injectSwitcher() {
    const host =
      document.getElementById('upper_section') ||
      document.getElementById('header') ||
      document.body;
    if (host) buildSwitcher(host);
  }

  // ===========================================================================
  // Init
  // ===========================================================================

  injectCSS();
  applyTheme();

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
      applyTheme();
      injectSwitcher();
      fixMaintabCaps();
      modernizeBBCodeIcons();
      modernizeSmileys();
      stylePostMetaButtons();
    }, { once: true });
  } else {
    injectSwitcher();
    fixMaintabCaps();
    modernizeBBCodeIcons();
    modernizeSmileys();
  }
  // Re-run icon and smiley modernisation on later DOM additions (SMF rebuilds
  // the BBCode toolbar when the editor preview opens, and posts may load via
  // ajax in some flows).
  if (typeof MutationObserver !== 'undefined') {
    const obs = new MutationObserver(() => {
      modernizeBBCodeIcons();
      modernizeSmileys();
      stylePostMetaButtons();
    });
    if (document.body) obs.observe(document.body, { childList: true, subtree: true });
    else document.addEventListener('DOMContentLoaded', () => {
      obs.observe(document.body, { childList: true, subtree: true });
    }, { once: true });
  }

  if (window.matchMedia) {
    const mq = window.matchMedia('(prefers-color-scheme: dark)');
    if (mq.addEventListener) {
      mq.addEventListener('change', () => {
        if (getPref(KEY_THEME, DEFAULT_THEME) === 'system') applyTheme();
      });
    } else if (mq.addListener) {
      mq.addListener(() => {
        if (getPref(KEY_THEME, DEFAULT_THEME) === 'system') applyTheme();
      });
    }
  }
})();