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();
      });
    }
  }
})();