Greasy Fork is available in English.
Modern light/dark/system theme for bitcointalk.org with separate customisable Main (background) and Accent presets.
// ==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();
});
}
}
})();