Greasy Fork is available in English.
Modern light/dark/system theme for bitcointalk.org with separate customisable Main and Accent presets, plus an OFF mode.
// ==UserScript==
// @name Bitcointalk Modern Theme
// @namespace bitcointalk-modern-theme
// @version 4.0.4
// @description Modern light/dark/system theme for bitcointalk.org with separate customisable Main and Accent presets, plus an OFF mode.
// @license MIT
// @match *://*.bitcointalk.org/*
// @match *://bitcointalk.org/*
// @run-at document-start
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function () {
'use strict';
// ===========================================================================
// Configuration
// ===========================================================================
const MAIN_PRESETS_DARK = {
slate: { name: 'Slate (default)', base: '#15171D' },
midnight: { name: 'Midnight', base: '#0F1424' },
forest: { name: 'Forest', base: '#0E1A18' },
plum: { name: 'Plum', base: '#1A0F1F' },
charcoal: { name: 'Charcoal', base: '#141414' },
};
const MAIN_PRESETS_LIGHT = {
// v2.10.30: "Classic" preset rebuilt against the actual classic SMF
// Bitcointalk theme. Body wash is now a near-neutral cool grey
// (#EEF0F3) so the page reads as a desaturated grey-white rather than
// an azure tint. The visible blue in the original lives only in two
// places: the catbg/header band (#5B7FA0 navy) and the link colour
// (#225588). Both are kept in their own rules below.
sky: { name: 'Classic (default)', base: '#EEF0F3' },
// v2.10.32: "Pearl" given a clear sky-blue identity (the visible azure
// tint that the old "Classic" preset used to have, before v2.10.30
// moved Classic to a neutral grey). New base #E5ECF3 + a clean blue
// accent so the preset reads as "blue & white" rather than the
// previous neutral-with-indigo-tint that came across as violet.
pearl: { name: 'Sky Blue', base: '#E5ECF3' },
cream: { name: 'Soft Cream', base: '#F4EFE6' },
mint: { name: 'Soft Mint', base: '#EEF3EF' },
rose: { name: 'Soft Rose', base: '#F5EEEE' },
};
const AUTO_ACCENT_MAP = {
light: {
// v2.10.28: classic SMF Bitcointalk uses the legendary #225588 blue
// for links throughout. Switching the auto-accent for the Classic
// preset to that exact hex pulls links, hover states and the active
// nav tab much closer to the original look.
sky: '#225588', // classic SMF Bitcointalk link blue
// v2.10.32: pearl auto-accent moved from indigo (#3A3DAA, which read
// as violet on the cool body wash) to a clean medium blue that pairs
// naturally with the pearl base #E5ECF3.
pearl: '#1E5FA8', // clean medium blue
cream: '#9A6842', // muted rust
mint: '#4C735D', // muted pine
rose: '#7B5662', // muted plum
},
dark: {
slate: '#7F84D8', // muted periwinkle
midnight: '#8E84CF', // muted lavender
forest: '#55B7A9', // muted aqua
plum: '#B779C2', // muted orchid
charcoal: '#CC8E45', // muted amber
},
};
const ACCENTS_DARK = {
orange: { name: 'Bitcoin Orange', base: '#F7931A' },
};
const ACCENTS_LIGHT = {
orange: { name: 'Bitcoin Orange', base: '#D97706' },
};
const DEFAULT_THEME = 'dark';
const SCRIPT_VERSION = '4.0.4'; // build-injected from package.json (see build.mjs)
const DEFAULT_ACCENT_DARK_KEY = 'orange';
const DEFAULT_ACCENT_LIGHT_KEY = 'auto';
const DEFAULT_MAIN_DARK_KEY = 'slate';
const DEFAULT_MAIN_LIGHT_KEY = 'sky';
const DEFAULT_CUSTOM_MAIN_DARK = '#15171D';
const DEFAULT_CUSTOM_MAIN_LIGHT = '#F4F5F8';
const DEFAULT_CUSTOM_ACCENT_DARK = '#F7931A';
const DEFAULT_CUSTOM_ACCENT_LIGHT = '#5B7FA0';
const DEFAULT_MOBILE_VERSION = 'auto';
const DEFAULT_TOPIC_READ_SHADING = 'on';
const KEY_THEME = 'bt-modern-theme';
const KEY_THEME_PRIOR = 'bt-modern-theme-prior';
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';
const KEY_MOBILE_VERSION = 'bt-modern-mobile-version';
const KEY_TOPIC_READ_SHADING = 'bt-modern-topic-read-shading';
const KEY_TOPIC_READ_STATE = 'bt-modern-topic-read-state';
const TOPIC_READ_STATE_MAX_ENTRIES = 800;
const ALL_KEYS = [
KEY_THEME,
KEY_THEME_PRIOR,
KEY_MAIN_DARK, KEY_MAIN_LIGHT,
KEY_ACCENT_DARK, KEY_ACCENT_LIGHT,
KEY_MAIN_DARK_CUSTOM, KEY_MAIN_LIGHT_CUSTOM,
KEY_ACCENT_DARK_CUSTOM, KEY_ACCENT_LIGHT_CUSTOM,
KEY_MOBILE_VERSION,
KEY_TOPIC_READ_SHADING,
KEY_TOPIC_READ_STATE,
];
// Safari flash guard constants (integrated extreme-fast variant)
const NAV_SELECTOR = 'a[href]';
const INCOMING_HOLD_STYLE_ID = 'bt-paint-hold';
const OUTGOING_HOLD_STYLE_ID = 'bt-pre-nav-hold';
const HOLD_REVEALED_ATTR = 'data-bt-revealed';
const NAV_UNTIL_SESSION_KEY = 'bt-modern-nav-hold-until';
const NAV_BG_SESSION_KEY = 'bt-modern-nav-hold-bg';
const OUTGOING_UNLOAD_FAILSAFE_MS = 1500;
const NAV_HOLD_SESSION_MS = 800;
// ===========================================================================
// Storage layer
// ===========================================================================
const hasGM = typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
function lsGet(key) {
try { return localStorage.getItem(key); } catch (e) { return null; }
}
function lsSet(key, value) {
try { localStorage.setItem(key, value); return true; } catch (e) { return false; }
}
function gmGet(key) {
if (!hasGM) return null;
try {
const v = GM_getValue(key, null);
return v == null ? null : String(v);
} catch (e) { return null; }
}
function gmSet(key, value) {
if (!hasGM) return false;
try { GM_setValue(key, String(value)); return true; } catch (e) { return false; }
}
function ssGet(key) {
try {
const v = sessionStorage.getItem(key);
return v == null || v === '' ? null : v;
} catch (e) { return null; }
}
function ssSet(key, value) {
try { sessionStorage.setItem(key, String(value)); } catch (e) {}
}
function ssRemove(key) {
try { sessionStorage.removeItem(key); } catch (e) {}
}
function getPref(key, fallback) {
let v = gmGet(key);
if (v == null || v === '') v = lsGet(key);
if (v == null || v === '') return fallback;
return v;
}
function setPref(key, value) {
const lsOk = lsSet(key, value);
const gmOk = gmSet(key, value);
return lsOk || gmOk;
}
function syncStores() {
ALL_KEYS.forEach(k => {
const ls = lsGet(k);
const gm = gmGet(k);
if (gm && (!ls || ls === '')) lsSet(k, gm);
else if (ls && (!gm || gm === '') && hasGM) gmSet(k, ls);
});
}
function getMobileVersionPreference() {
const preference = getPref(KEY_MOBILE_VERSION, DEFAULT_MOBILE_VERSION);
return preference === 'on' || preference === 'off' || preference === 'balanced'
? preference
: 'auto';
}
function shouldUseAutomaticMobilePerformance() {
const compactViewport = !!(
window.matchMedia && window.matchMedia('(max-width: 820px)').matches
);
const compactScreen = !!(
window.screen && Math.min(window.screen.width || Infinity, window.screen.height || Infinity) <= 820
);
const coarsePointer = !!(
(window.matchMedia && window.matchMedia('(any-pointer: coarse)').matches) ||
(navigator && navigator.maxTouchPoints > 0)
);
return coarsePointer && (compactViewport || compactScreen);
}
function getEffectiveMobileMode(preference = getMobileVersionPreference()) {
if (preference === 'on') return 'lite';
if (preference === 'balanced') return 'balanced';
if (preference === 'off') return 'full';
return shouldUseAutomaticMobilePerformance() ? 'balanced' : 'full';
}
function isMobileLite() {
return getEffectiveMobileMode() === 'lite';
}
function isMobileBalanced() {
return getEffectiveMobileMode() === 'balanced';
}
function isMobilePerformanceMode() {
return getEffectiveMobileMode() !== 'full';
}
function isTopicReadShadingEnabled() {
return getPref(KEY_TOPIC_READ_SHADING, DEFAULT_TOPIC_READ_SHADING) === 'on';
}
function updateMobileLiteAttribute() {
const root = document.documentElement;
const mode = getEffectiveMobileMode();
const optimized = mode === 'full' ? 'false' : 'true';
if (root.getAttribute('data-bt-mobile-mode') !== mode) {
root.setAttribute('data-bt-mobile-mode', mode);
}
// Retain the original attribute as a CSS compatibility flag for both
// performance-oriented modes.
if (root.getAttribute('data-bt-mobile-lite') !== optimized) {
root.setAttribute('data-bt-mobile-lite', optimized);
}
}
function updatePageAttribute() {
let page = '';
try {
if (isMeritPage()) page = 'merit';
} catch (e) {}
if (page) {
if (document.documentElement.getAttribute('data-bt-page') !== page) {
document.documentElement.setAttribute('data-bt-page', page);
}
} else if (document.documentElement.hasAttribute('data-bt-page')) {
document.documentElement.removeAttribute('data-bt-page');
}
}
function isMeritPage() {
try {
const search = window.location.search || '';
const params = new URLSearchParams(search);
const action = (params.get('action') || '').toLowerCase();
return action === 'merit' || action.indexOf('merit;') === 0 || /[?;&]action=merit(?:[;&]|$)/i.test(search);
} catch (e) {
return /[?;&]action=merit(?:[;&]|$)/i.test(window.location.search || '');
}
}
// ===========================================================================
// Color helpers
// ===========================================================================
function sanitizeHex(hex, fallback = '#000000') {
const v = String(hex || '').trim();
const fallbackValue = String(fallback || '').trim();
const fallbackMatch = fallbackValue.match(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/);
const fb = fallbackMatch
? '#' + (
fallbackMatch[1].length === 3
? fallbackMatch[1].split('').map(ch => ch + ch).join('')
: fallbackMatch[1]
).toUpperCase()
: '#000000';
const match = v.match(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/);
if (!match) return fb;
const body = match[1];
return '#' + (
body.length === 3
? body.split('').map(ch => ch + ch).join('')
: body
).toUpperCase();
}
function hexToRgb(hex) {
hex = sanitizeHex(hex, '#000000');
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));
}
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 };
}
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 sanitizeHex(rgbToHex(rgb.r, rgb.g, rgb.b), sanitizeHex(hex, '#000000'));
}
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',
};
}
function deriveMainPalette(hex, theme, mainKey) {
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),
postBg: lighten(hex, 0.055),
postAltBg: lighten(hex, 0.085),
posterBg: lighten(hex, 0.050),
posterAltBg: lighten(hex, 0.075),
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). v2.8.3 also derives post alternation colours
// from the selected light theme, rather than hardcoding Classic/Sky.
//
// v2.10.28: when the Classic (sky) preset is active, we override the
// surface tones with hand-picked SMF Bitcointalk values instead of the
// generic adjustHsl derivation. This gives us:
// surface1 → windowbg-equivalent (the post background)
// surface2 → titlebg-equivalent (sub-category headers)
// surface3 → catbg-equivalent (top category headers, deeper blue)
// Other presets continue to use the algorithmic derivation so their
// chromatic identity is preserved.
//
// v2.10.30: Classic surface/post/border/quote/code values further
// desaturated toward neutral cool grey to match the original SMF look,
// which reads much more grey than blue against a white browser canvas.
// The catbg-equivalent (surface3) is the only place that retains a
// visible blue tint, since classic SMF Bitcointalk uses a steel-blue
// for category headers.
const isClassic = mainKey === 'sky';
const navActiveBg = adjustHsl(hex, -0.38, 0.36);
return {
bg: hex,
bgEnd: adjustHsl(hex, -0.025, 0.04),
// v2.10.30: classic SMF surfaces. Posts on near-white (the original's
// windowbg is essentially #F8F9FA), titlebg as a soft grey-blue band,
// catbg as a saturated navy (the iconic SMF header colour).
surface1: isClassic ? '#FAFBFC' : adjustHsl(hex, -0.055, 0.12),
surface2: isClassic ? '#D8DDE5' : adjustHsl(hex, -0.12, 0.20),
surface3: isClassic ? '#5B7FA0' : adjustHsl(hex, -0.20, 0.28),
// v2.10.30: classic alternation in cool neutral greys. The originals
// were #F0F4F7 / #F8F9FA — both very light, very desaturated. We use
// pure white for the primary windowbg and a faintest grey for the
// alternate row so the stripe is visible without becoming blue.
postBg: isClassic ? '#FFFFFF' : adjustHsl(hex, -0.010, 0.015),
postAltBg: isClassic ? '#F2F4F7' : adjustHsl(hex, -0.045, 0.060),
posterBg: isClassic ? '#F0F2F5' : adjustHsl(hex, -0.035, 0.045),
posterAltBg: isClassic ? '#E6E9EE' : adjustHsl(hex, -0.070, 0.090),
navActiveBg: isClassic ? '#5B7FA0' : navActiveBg,
navActiveText: isClassic
? '#FFFFFF'
: (relativeLuminance(navActiveBg) > 0.45 ? '#1A1D21' : '#FFFFFF'),
inputBg: '#FFFFFF',
quoteBg: isClassic ? '#EFF2F6' : adjustHsl(hex, -0.02, 0.04),
codeBg: isClassic ? '#E0E4EB' : adjustHsl(hex, -0.09, 0.18),
// v2.10.30: classic borders as solid neutral greys, not rgba —
// semi-transparent borders pick up the underlying tint and read as
// bluish on a cool-grey body. Solid #C0C5CC / #99A0AB give the
// crisp grey separators of the classic SMF theme.
border: isClassic ? '#C0C5CC' : 'rgba(0,0,0,0.08)',
border2: isClassic ? '#99A0AB' : 'rgba(0,0,0,0.16)',
text: isClassic ? '#000000' : '#1A1D21',
text2: isClassic ? '#3F4754' : '#4A5058',
text3: isClassic ? '#6E7785' : '#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)',
};
}
function deriveAutoAccent(mainHex, theme, mainKey) {
mainHex = sanitizeHex(mainHex, theme === 'dark' ? DEFAULT_CUSTOM_MAIN_DARK : DEFAULT_CUSTOM_MAIN_LIGHT);
if (mainKey && AUTO_ACCENT_MAP[theme] && AUTO_ACCENT_MAP[theme][mainKey]) {
return sanitizeHex(AUTO_ACCENT_MAP[theme][mainKey], theme === 'dark' ? DEFAULT_CUSTOM_ACCENT_DARK : DEFAULT_CUSTOM_ACCENT_LIGHT);
}
const { r, g, b } = hexToRgb(mainHex);
const hsl = rgbToHsl(r, g, b);
if (hsl.s < 0.05) {
return sanitizeHex(theme === 'dark' ? '#E8A04A' : '#B5552B', theme === 'dark' ? DEFAULT_CUSTOM_ACCENT_DARK : DEFAULT_CUSTOM_ACCENT_LIGHT);
}
const newH = hsl.h;
const newS = Math.min(0.65, Math.max(0.50, hsl.s + 0.25));
const newL = theme === 'dark' ? 0.62 : 0.38;
const rgb = hslToRgb(newH, newS, newL);
return sanitizeHex(rgbToHex(rgb.r, rgb.g, rgb.b), theme === 'dark' ? DEFAULT_CUSTOM_ACCENT_DARK : DEFAULT_CUSTOM_ACCENT_LIGHT);
}
function toCssVar(name) {
return '--bt-' + name
.replace(/([A-Z]|\d+)/g, '-$1')
.toLowerCase()
.replace(/^-/, '');
}
// ===========================================================================
// Theme application
// ===========================================================================
function getEffectiveTheme(pref) {
if (pref === 'off') return 'off';
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 sanitizeHex(getPref(theme === 'dark' ? KEY_MAIN_DARK_CUSTOM : KEY_MAIN_LIGHT_CUSTOM, def), def);
}
const fallbackKey = theme === 'dark' ? DEFAULT_MAIN_DARK_KEY : DEFAULT_MAIN_LIGHT_KEY;
return sanitizeHex((presets[key] || presets[fallbackKey]).base, presets[fallbackKey].base);
}
function getActiveAccentHex(theme) {
const presets = theme === 'dark' ? ACCENTS_DARK : ACCENTS_LIGHT;
const key = getAccentKey(theme);
if (key === 'auto') {
return deriveAutoAccent(getActiveMainHex(theme), theme, getMainKey(theme));
}
if (key === 'custom') {
const def = theme === 'dark' ? DEFAULT_CUSTOM_ACCENT_DARK : DEFAULT_CUSTOM_ACCENT_LIGHT;
return sanitizeHex(getPref(theme === 'dark' ? KEY_ACCENT_DARK_CUSTOM : KEY_ACCENT_LIGHT_CUSTOM, def), def);
}
if (presets[key]) {
return sanitizeHex(presets[key].base, presets.orange ? presets.orange.base : '#F7931A');
}
return deriveAutoAccent(getActiveMainHex(theme), theme, getMainKey(theme));
}
// ===========================================================================
// Replacement visibility / OFF-mode restoration
// ===========================================================================
//
// We replace classic SMF image buttons with modern spans/SVGs. Earlier builds
// hid the original <img> nodes with inline `display:none !important`.
// That is stronger than stylesheet rules, so when OFF mode disabled the theme
// stylesheet the original BBCode/Quote buttons stayed hidden. This helper is
// the OFF-mode fix: it explicitly restores original images and hides only our
// replacement nodes.
function setModernizedControlsActive(active, scope = document) {
const showOriginals = !active;
queryAllWithin(scope, 'img[data-bt-replaced="1"], img[data-bt-smiley="1"]').forEach(img => {
// v2.10.15: inline-icon IMGs (lock + new) are handled via src swap.
// Skip the display:none codepath here and route to src restore/apply.
if (img.dataset.btInlineIconActive === '1' || Object.prototype.hasOwnProperty.call(img.dataset, 'btOriginalSrc')) {
if (active) {
// Re-apply the modern src in case OFF mode reverted it. Pick the
// right data-URL based on the recorded type.
if (img.dataset.btTopicType === 'locked') {
applyInlineIconSrc(img, INLINE_LOCK_DATAURL);
} else if (img.dataset.btTopicType === 'newPost') {
applyInlineIconSrc(img, INLINE_NEW_DATAURL);
} else if (img.dataset.btTopicType === 'sticky') {
applyInlineIconSrc(img, INLINE_STICKY_DATAURL);
}
} else {
restoreInlineIconSrc(img);
}
return;
}
if (active) {
if (!Object.prototype.hasOwnProperty.call(img.dataset, 'btOriginalStyle')) {
img.dataset.btOriginalStyle = img.getAttribute('style') || '';
}
img.style.setProperty('display', 'none', 'important');
} else {
if (Object.prototype.hasOwnProperty.call(img.dataset, 'btOriginalStyle')) {
const originalStyle = img.dataset.btOriginalStyle || '';
if (originalStyle) img.setAttribute('style', originalStyle);
else img.removeAttribute('style');
} else {
img.style.removeProperty('display');
if (!img.getAttribute('style')) img.removeAttribute('style');
}
}
});
// Hide/show our modern replacement UI. Important: do NOT hide every
// .bt-post-action in OFF mode, because +Merit and #post anchors are real
// site links that merely receive this class for styling. Only replacement
// spans created from hidden image buttons carry data-bt-replacement="1".
queryAllWithin(scope, '.bt-bbc-icon, .bt-smiley, .bt-topic-icon[data-bt-replacement="1"], .bt-post-action[data-bt-replacement="1"]').forEach(el => {
if (showOriginals) {
el.style.setProperty('display', 'none', 'important');
} else {
el.style.removeProperty('display');
if (!el.getAttribute('style')) el.removeAttribute('style');
}
});
// Re-apply / clear special topic tints when toggling the theme. Original
// inline backgrounds are preserved for a faithful OFF-mode restoration.
queryAllWithin(scope, 'tr.bt-row-sticky, tr.bt-row-locked').forEach(tr => {
if (active) {
applySpecialTopicRowTintInline(tr);
} else {
clearSpecialTopicRowTintInline(tr);
}
});
}
function isThemeActiveNow() {
return getEffectiveTheme(getPref(KEY_THEME, DEFAULT_THEME)) !== 'off';
}
let lastModernizedControlsActive = null;
function setRootAttributeIfChanged(root, name, value) {
if (root.getAttribute(name) !== value) root.setAttribute(name, value);
}
function setRootStyleIfChanged(root, name, value) {
if (root.style.getPropertyValue(name) !== value) root.style.setProperty(name, value);
}
function setModernizedControlsState(active) {
if (lastModernizedControlsActive === active) return;
lastModernizedControlsActive = active;
setModernizedControlsActive(active);
}
function applyTheme() {
const themePref = getPref(KEY_THEME, DEFAULT_THEME);
const effective = getEffectiveTheme(themePref);
const root = document.documentElement;
updateMobileLiteAttribute();
updatePageAttribute();
if (effective === 'off') {
cancelPendingFontLoad();
setRootAttributeIfChanged(root, 'data-bt-active', 'false');
setRootAttributeIfChanged(root, 'data-bt-theme-pref', themePref);
if (themeStyleEl && !themeStyleEl.disabled) themeStyleEl.disabled = true;
if (root.style.colorScheme) root.style.colorScheme = '';
setModernizedControlsState(false);
updateSwitcherUI(themePref, effective, null, null, null, null);
return;
}
const mainKey = getMainKey(effective);
const accentKey = getAccentKey(effective);
const mainHex = getActiveMainHex(effective);
const accentHex = getActiveAccentHex(effective);
const palette = deriveMainPalette(mainHex, effective, mainKey);
const accent = deriveAccent(accentHex);
setRootAttributeIfChanged(root, 'data-bt-active', 'true');
setRootAttributeIfChanged(root, 'data-bt-theme', effective);
setRootAttributeIfChanged(root, 'data-bt-theme-pref', themePref);
setRootAttributeIfChanged(root, 'data-bt-main', mainKey);
setRootAttributeIfChanged(root, 'data-bt-accent', accentKey);
if (root.style.colorScheme !== effective) root.style.colorScheme = effective;
Object.entries(palette).forEach(([k, v]) => setRootStyleIfChanged(root, toCssVar(k), v));
setRootStyleIfChanged(root, '--bt-accent', accent.base);
setRootStyleIfChanged(root, '--bt-accent-h', accent.hover);
setRootStyleIfChanged(root, '--bt-accent-a', accent.active);
setRootStyleIfChanged(root, '--bt-on-accent', accent.onText);
if (themeStyleEl && themeStyleEl.disabled) themeStyleEl.disabled = false;
setModernizedControlsState(!isMobileLite());
ensureFonts();
updateSwitcherUI(themePref, effective, mainKey, accentKey, mainHex, accentHex);
}
function updateSwitcherFromPreferences() {
const themePref = getPref(KEY_THEME, DEFAULT_THEME);
const effective = getEffectiveTheme(themePref);
if (effective === 'off') {
updateSwitcherUI(themePref, effective, null, null, null, null);
return;
}
updateSwitcherUI(
themePref,
effective,
getMainKey(effective),
getAccentKey(effective),
getActiveMainHex(effective),
getActiveAccentHex(effective)
);
}
function updateSwitcherUI(themePref, effective, mainKey, accentKey, mainHex, accentHex) {
const sw = document.getElementById('bt-switcher');
if (!sw) return;
ensureSettingsToggleControls(sw);
const mainBtn = sw.querySelector('#bt-switcher-button');
if (mainBtn) {
if (themePref === 'off') {
mainBtn.innerHTML = '<span class="bt-sw-on-text">ON</span>';
mainBtn.title = 'Turn the theme back on';
mainBtn.setAttribute('aria-label', 'Turn the theme back on');
} else {
mainBtn.innerHTML = ICONS.cog;
mainBtn.title = 'Theme settings';
mainBtn.setAttribute('aria-label', 'Theme settings');
}
}
const customizeEl = sw.querySelector('.bt-sw-customize');
if (customizeEl) {
const hide = themePref === 'system' || themePref === 'off';
customizeEl.style.display = hide ? 'none' : '';
}
const titleEl = sw.querySelector('#bt-sw-customize-title');
if (titleEl && effective !== 'off') {
titleEl.textContent = `Customise ${effective === 'dark' ? 'Dark' : 'Light'} Colours`;
}
sw.querySelectorAll('[data-theme]').forEach(b => {
b.classList.toggle('selected', b.dataset.theme === themePref);
});
const offBtn = sw.querySelector('[data-theme="off"]');
if (offBtn) {
const lbl = offBtn.querySelector('.bt-sw-theme-label');
if (lbl) lbl.textContent = themePref === 'off' ? 'ON' : 'OFF';
offBtn.title = themePref === 'off' ? 'Turn the theme back on' : 'Turn the theme off';
}
if (effective !== 'off') {
rebuildSwatches(sw, 'main', effective, mainKey, mainHex);
rebuildSwatches(sw, 'accent', effective, accentKey, accentHex);
}
const mobileBtn = sw.querySelector('#bt-sw-mobile-version');
if (mobileBtn) {
const mobilePreference = getMobileVersionPreference();
const mobileMode = getEffectiveMobileMode(mobilePreference);
const mobileOptimized = mobileMode !== 'full';
const modeName = mobileMode === 'balanced' ? 'Balanced' : mobileMode === 'lite' ? 'Lite' : 'Full';
mobileBtn.classList.toggle('selected', mobileOptimized);
mobileBtn.classList.toggle('bt-sw-mobile-on', mobileOptimized);
mobileBtn.classList.toggle('bt-sw-mobile-auto', mobilePreference === 'auto');
const nextModeName = mobilePreference === 'auto'
? 'Full'
: mobilePreference === 'off' ? 'Balanced' : mobilePreference === 'balanced' ? 'Lite' : 'Auto';
mobileBtn.title = mobilePreference === 'auto'
? `Automatic mobile performance (${modeName} now). Click for ${nextModeName}.`
: `Mobile performance: ${modeName}. Click for ${nextModeName}.`;
mobileBtn.setAttribute(
'aria-pressed',
mobilePreference === 'auto' ? 'mixed' : mobileOptimized ? 'true' : 'false'
);
const label = mobileBtn.querySelector('.bt-sw-theme-label');
if (label) {
label.textContent = mobilePreference === 'auto'
? `Mobile: Auto (${modeName})`
: `Mobile: ${modeName}`;
}
const state = mobileBtn.querySelector('.bt-sw-toggle-state');
if (state) {
state.textContent = mobilePreference === 'auto'
? 'AUTO'
: mobileMode === 'balanced' ? 'BAL' : mobileMode === 'lite' ? 'LITE' : 'FULL';
}
}
const readBtn = sw.querySelector('#bt-sw-read-shading');
if (readBtn) {
const readOn = isTopicReadShadingEnabled();
readBtn.classList.toggle('selected', readOn);
readBtn.classList.toggle('bt-sw-read-on', readOn);
readBtn.title = readOn ? 'Turn read-post shading off' : 'Shade posts you have already read';
readBtn.setAttribute('aria-pressed', readOn ? 'true' : 'false');
const state = readBtn.querySelector('.bt-sw-toggle-state');
if (state) state.textContent = readOn ? 'ON' : 'OFF';
}
}
function ensureSettingsToggleControls(sw) {
if (!sw) return null;
const panel = sw.querySelector('#bt-switcher-panel');
if (!panel) return null;
let section = sw.querySelector('.bt-sw-performance-section');
if (!section) {
section = document.createElement('div');
section.className = 'bt-sw-section bt-sw-performance-section';
const customize = panel.querySelector('.bt-sw-customize');
if (customize) panel.insertBefore(section, customize);
else panel.appendChild(section);
}
function ensureToggle(id, extraClass, label, title, pressed) {
let btn = sw.querySelector(`#${id}`);
if (!btn) {
section.insertAdjacentHTML('beforeend', `
<button type="button" class="bt-sw-toggle ${extraClass}" id="${id}" title="${title}" aria-pressed="${pressed}">
<span class="bt-sw-theme-label">${label}</span>
<span class="bt-sw-toggle-spacer"></span>
<span class="bt-sw-toggle-track" aria-hidden="true"><span class="bt-sw-toggle-knob"></span></span>
<span class="bt-sw-toggle-state">${pressed === 'true' ? 'ON' : 'OFF'}</span>
</button>
`);
btn = sw.querySelector(`#${id}`);
}
return btn;
}
const mobileBtn = ensureToggle(
'bt-sw-mobile-version',
'bt-sw-mobile-toggle',
'Mobile mode',
'Cycle Auto, Full, Balanced and Lite mobile performance',
'false'
);
const readBtn = ensureToggle(
'bt-sw-read-shading',
'bt-sw-read-toggle',
'Read shading',
'Shade posts you have already read',
'true'
);
if (mobileBtn && mobileBtn.dataset.btMobileBound !== '1') {
mobileBtn.dataset.btMobileBound = '1';
mobileBtn.addEventListener('click', () => {
const previousMode = getEffectiveMobileMode();
const current = getMobileVersionPreference();
const next = current === 'off'
? 'balanced'
: current === 'balanced' ? 'on' : current === 'on' ? 'auto' : 'off';
setPref(KEY_MOBILE_VERSION, next);
applyTheme();
reconcileMobileRuntime(previousMode, getEffectiveMobileMode(), true);
});
}
if (readBtn && readBtn.dataset.btReadBound !== '1') {
readBtn.dataset.btReadBound = '1';
readBtn.addEventListener('click', () => {
const next = isTopicReadShadingEnabled() ? 'off' : 'on';
setPref(KEY_TOPIC_READ_SHADING, next);
if (next === 'off') clearReadPostMarks();
else markReadPostsForCurrentTopic();
applyTheme();
});
}
return { mobileBtn, readBtn };
}
function ensureMobileToggleControl(sw) {
const controls = ensureSettingsToggleControls(sw);
return controls && controls.mobileBtn;
}
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 = sanitizeHex(getPref(customStorageKey, customDef), customDef);
const safeCurrentHex = sanitizeHex(currentHex, customHex);
const isCustomSelected = selectedKey === 'custom';
const isAutoSelected = selectedKey === 'auto';
const signature = [kind, effective, selectedKey, safeCurrentHex, customHex].join('|');
if (row.dataset.btSwatchSignature === signature && row.dataset.btPreviewing !== '1') return;
row.dataset.btSwatchSignature = signature;
delete row.dataset.btPreviewing;
const presetSwatches = Object.entries(presets).map(([key, p]) => {
const safePresetHex = sanitizeHex(p.base, customDef);
return `<button type="button" class="bt-sw-swatch${key === selectedKey ? ' selected' : ''}" data-${kind}="${key}" title="${p.name}" style="background:${safePresetHex}"></button>`;
}).join('');
const autoSwatch = kind === 'accent'
? `<button type="button" class="bt-sw-swatch bt-sw-auto${isAutoSelected ? ' selected' : ''}" data-${kind}="auto" title="Auto (matches your Main)"></button>`
: '';
row.innerHTML = autoSwatch + 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:${safeCurrentHex}"` : ''}></span>
<input type="color" data-target="${kind}" value="${customHex}" />
</span>
`;
}
// ===========================================================================
// Maintab fix
// ===========================================================================
function fixMaintabCaps(scope = document) {
queryAllWithin(scope, '[class*="maintab"], [class*="mirrortab"]').forEach(el => {
const text = (el.textContent || '').replace(/\s+/g, '');
if (!text) el.style.cssText = 'display: none !important;';
});
}
const TOPIC_ACTION_TEXTS = new Set([
'reply',
'unwatch',
'notify',
'mark unread',
'add poll',
'print',
'send this topic',
'view topic archive',
]);
function normaliseTopicActionText(text) {
return String(text || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function markTopicActionBars(scope = document) {
const cells = queryAllWithin(scope, 'td[class*="maintab_"], td[class*="mirrortab_"]');
if (scope !== document && scope.closest) {
const parentCell = scope.closest('td[class*="maintab_"], td[class*="mirrortab_"]');
if (parentCell && !cells.includes(parentCell)) cells.push(parentCell);
}
cells.forEach(cell => {
const table = cell.closest('table');
if (!table) return;
const links = Array.from(table.querySelectorAll('a'));
if (!links.some(a => TOPIC_ACTION_TEXTS.has(normaliseTopicActionText(a.textContent)))) return;
table.classList.add('bt-topic-action-bar');
table.querySelectorAll('td[class*="maintab_"], td[class*="mirrortab_"]').forEach(td => {
td.classList.add('bt-topic-action-cell');
});
});
}
// ===========================================================================
// BBCode icons
// ===========================================================================
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>',
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>',
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>',
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>',
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>',
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>',
};
const BBCODE_ALIASES = {
italic: ['italicize'],
bold: ['bolden'],
strike: ['strikethrough', 'linethrough'],
tt: ['teletype', 'monospace', 'tele'],
font: ['fontface', 'fontname', 'face'],
color: ['colour', 'fontcolour', 'fontcolor'],
size: ['fontsize'],
left: ['alignleft'],
center: ['aligncenter'],
right: ['alignright'],
list: ['bullist'],
orderlist: ['numlist', 'numberedlist'],
img: ['image', 'picture'],
url: ['link', 'hyperlink'],
email: ['mail'],
youtube: ['video', 'youtu'],
hr: ['horizontalrule'],
sub: ['subscript'],
sup: ['superscript'],
bitcoin: ['btc'],
trow: ['tr'],
tcol: ['td'],
trash: ['delete', 'remove', 'del'],
pencil: ['modify', 'edit', 'pen'],
};
Object.entries(BBCODE_ALIASES).forEach(([canon, aliases]) => {
aliases.forEach(a => { BBCODE_SVGS[a] = BBCODE_SVGS[canon]; });
});
// ===========================================================================
// Topic-list / board-list / message-status icon modernisation
// ===========================================================================
//
// Bitcointalk thread-list and thread-header pages still use classic SMF GIFs:
// topic state icons, locked/sticky/poll icons, and message icons such as
// exclamation/question/lamp/thumbs. These are not BBCode compose toolbar
// icons, so they need a separate pass. The originals stay in the DOM and are
// restored in OFF mode via setModernizedControlsActive().
const TOPIC_ICON_SVGS = {
topic: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v8A2.5 2.5 0 0 1 17.5 17H9l-5 3v-13.5z"/><line x1="8" y1="9" x2="16" y2="9"/><line x1="8" y1="12" x2="14" y2="12"/></svg>',
hot: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A3.5 3.5 0 0 0 12 21a6 6 0 0 0 6-6c0-4-3-6-4.5-9.5-.4 2.4-1.6 3.9-3.5 5.2C8.5 11.7 8 12.8 8.5 14.5z"/><path d="M12 21c-1.2-1-1.6-2.4-.9-4 .5-1.1 1.4-1.8 2-3 .7 1.2 1.4 2.2 1.4 3.6A3.2 3.2 0 0 1 12 21z"/></svg>',
veryhot: '<svg viewBox="0 0 24 24" fill="rgba(217,48,37,0.18)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 14.5A4.5 4.5 0 0 0 12 21a7 7 0 0 0 7-7c0-4.4-3.2-6.7-4.8-10.5-.5 2.7-1.9 4.4-4 5.9-1.8 1.3-2.9 2.8-2.7 5.1z"/><path d="M12 21c-1.5-1.2-2-2.8-1.1-4.7.6-1.3 1.7-2.1 2.4-3.5.9 1.4 1.7 2.6 1.7 4.2A3.8 3.8 0 0 1 12 21z" fill="rgba(217,48,37,0.35)"/><path d="M5 9c-1 .9-1.5 2-1.3 3.3" fill="none"/></svg>',
locked: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg>',
sticky: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4l5 5-4 1-4.5 4.5.5 4.5-2 2-3.5-6.5L1 11l2-2 4.5.5L12 5z"/><line x1="14" y1="6" x2="18" y2="10"/></svg>',
poll: '<svg viewBox="0 0 24 24" fill="rgba(139,92,246,0.25)" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="20" x2="20" y2="20" fill="none"/><rect x="6" y="10" width="3" height="7" rx="1"/><rect x="11" y="6" width="3" height="11" rx="1"/><rect x="16" y="13" width="3" height="4" rx="1"/></svg>',
moved: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M13 6l6 6-6 6"/><path d="M5 5v14"/></svg>',
newPost: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="18" height="12" rx="4"/><path d="M7 15V9l4 6V9"/><path d="M13 9h4"/><path d="M13 12h3"/><path d="M13 15h4"/></svg>',
important: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><line x1="12" y1="7" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
question: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.4 9a3 3 0 1 1 4.9 2.3c-1.3.9-2.3 1.5-2.3 3.2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>',
idea: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M8 14a6 6 0 1 1 8 0c-1.2.9-1.5 1.8-1.5 3h-5c0-1.2-.3-2.1-1.5-3z"/></svg>',
thumbsUp: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/><path d="M7 11l4-8a3 3 0 0 1 3 3v4h5a2 2 0 0 1 2 2l-1 7a3 3 0 0 1-3 3H7z"/></svg>',
thumbsDown: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/><path d="M17 13l-4 8a3 3 0 0 1-3-3v-4H5a2 2 0 0 1-2-2l1-7a3 3 0 0 1 3-3h10z"/></svg>',
smile: '<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="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>',
angry: '<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="M8 8l3 2"/><path d="M16 8l-3 2"/><path d="M8 16s1.5-1.5 4-1.5S16 16 16 16"/></svg>',
standard: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h7l5 5v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><path d="M14 4v5h5"/></svg>',
};
// ===========================================================================
// v2.10.15: Inline status icons via src replacement
// ===========================================================================
//
// Classic SMF places certain status icons as <img> elements that the browser
// already positions correctly:
// • quick_lock.gif lives on the lock'd thread row with align="right" — the
// browser floats it to the right edge of the subject cell automatically.
// • the "new" indicator is an <img> inside <a href="...new">, which sits
// naturally after the topic title.
//
// Earlier versions tried to hide these <img>s and inject synthetic spans in
// their place. That fights the SMF layout and led to the wrapper-position
// and visibility bugs in 2.10.13/2.10.14. The cleaner approach is to keep
// every <img> exactly where SMF rendered it and only swap its `src` to a
// modern SVG data-URL. OFF mode restoration becomes trivial: put the
// original src back.
//
// Colors are baked into the SVGs because data-URL SVGs run in their own
// document context and cannot read the host page's CSS variables. Gold for
// the lock matches the classic Bitcointalk yellow padlock; the dark-blue
// pill for "new" matches the original SMF "new" badge tint.
const INLINE_LOCK_SVG = (
// v2.10.17: bumped from 14×14 to 17×17 to match the upsized sticky pin.
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="17" height="17">' +
'<path d="M4.2 6V4.2C4.2 2.65 5.45 1.4 7 1.4C8.55 1.4 9.8 2.65 9.8 4.2V6" ' +
'fill="none" stroke="#a17710" stroke-width="1.3" stroke-linecap="round"/>' +
'<rect x="2.6" y="6" width="8.8" height="6.6" rx="1.1" fill="#e8b923" stroke="#a17710" stroke-width="0.6"/>' +
'<circle cx="7" cy="9" r="0.85" fill="#7a5808"/>' +
'<rect x="6.55" y="9" width="0.9" height="2.2" rx="0.3" fill="#7a5808"/>' +
'</svg>'
);
const INLINE_NEW_SVG = (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 13" width="26" height="13">' +
'<rect x="0" y="0" width="26" height="13" rx="6.5" fill="#4d6c8c"/>' +
'<text x="13" y="9.6" text-anchor="middle" ' +
'font-family="-apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif" ' +
'font-size="8" font-weight="600" fill="#ffffff" letter-spacing="0.2">new</text>' +
'</svg>'
);
// v2.10.17: bumped from 14×14 to 17×17 for better visual presence next to
// the topic title — the previous size felt cramped especially compared to
// the title's 13px font.
const INLINE_STICKY_SVG = (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="17" height="17">' +
'<line x1="8.5" y1="8.5" x2="12.5" y2="12.5" stroke="#4a4a4a" stroke-width="1" stroke-linecap="round"/>' +
'<circle cx="6" cy="6" r="4" fill="#cc3838" stroke="#7a2020" stroke-width="0.6"/>' +
'<ellipse cx="4.6" cy="4.8" rx="1.1" ry="0.8" fill="#ee9595" opacity="0.75"/>' +
'</svg>'
);
function svgToDataUrl(svg) {
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg);
}
const INLINE_LOCK_DATAURL = svgToDataUrl(INLINE_LOCK_SVG);
const INLINE_NEW_DATAURL = svgToDataUrl(INLINE_NEW_SVG);
const INLINE_STICKY_DATAURL = svgToDataUrl(INLINE_STICKY_SVG);
// True when the IMG is the standalone lock indicator that SMF places to the
// right of a topic title. Compound state icons like sticky_locked.gif live
// in the left state column and are handled separately.
function isInlineLockImage(img) {
const src = (img.getAttribute('src') || '').toLowerCase();
if (!/lock/.test(src)) return false;
if (/sticky[_-]?locked|locked[_-]?(post|sticky|topic|poll)/.test(src)) return false;
// align="right" is the SMF tell — only inline lock indicators use it.
if ((img.getAttribute('align') || '').toLowerCase() === 'right') return true;
// Fallback: filenames like quick_lock.gif / lock.gif that aren't compound.
return /(^|[/_-])(quick[_-]?lock|lock)\.(gif|png)/.test(src);
}
// v2.10.16: standalone sticky-pin indicator that SMF places to the right of
// a sticky topic title. Same parsing rules as the lock detector — exclude
// compound left-column icons (sticky_post / sticky_locked / sticky_poll),
// trigger on align="right", fall back to filename pattern.
function isInlineStickyImage(img) {
const src = (img.getAttribute('src') || '').toLowerCase();
if (!/sticky|pin/.test(src)) return false;
if (/sticky[_-]?(locked|post|poll|topic)|locked[_-]?sticky/.test(src)) return false;
if ((img.getAttribute('align') || '').toLowerCase() === 'right') return true;
return /(^|[/_-])(quick[_-]?sticky|sticky|pin|pinned)\.(gif|png)/.test(src);
}
// True when the IMG is the "new" indicator wrapped in <a href="...new">.
function isInlineNewImage(img) {
const src = (img.getAttribute('src') || '').toLowerCase();
if (!/(^|[/_-])(new|unread)/.test(src)) return false;
const link = img.closest('a[href*="topic="]');
if (!link) return false;
const href = (link.getAttribute('href') || '').toLowerCase();
return /[#;]new\b|;topicseen|msg\d+/.test(href);
}
// Swap the IMG's src to a modern SVG data-URL while preserving the original
// for OFF-mode restoration. Returns true if the swap was applied (or already
// in place), false otherwise.
function applyInlineIconSrc(img, dataUrl) {
if (!img || !dataUrl) return false;
if (img.dataset.btInlineIconActive === '1' && img.getAttribute('src') === dataUrl) return true;
if (!Object.prototype.hasOwnProperty.call(img.dataset, 'btOriginalSrc')) {
img.dataset.btOriginalSrc = img.getAttribute('src') || '';
}
img.setAttribute('src', dataUrl);
img.dataset.btInlineIconActive = '1';
img.dataset.btReplaced = '1';
return true;
}
function restoreInlineIconSrc(img) {
if (!img) return;
if (img.dataset.btInlineIconActive !== '1') return;
const originalSrc = img.dataset.btOriginalSrc;
if (typeof originalSrc === 'string') img.setAttribute('src', originalSrc);
img.dataset.btInlineIconActive = '0';
}
// Mark and tint the topic subject cell for both standalone inline markers
// and compound left-column status images. The inline declaration is only a
// compatibility fallback; the stylesheet owns the final colour and hover
// state through palette-derived variables.
function findSpecialTopicSubjectCell(statusImg) {
if (!statusImg) return null;
const titleLink = findTopicTitleLinkNear(statusImg);
return (titleLink && titleLink.closest('td')) || statusImg.closest('td');
}
function markSpecialTopicRow(statusImg, type) {
if (!statusImg || (type !== 'sticky' && type !== 'locked')) return;
const tr = statusImg.closest('tr');
const subjectCell = findSpecialTopicSubjectCell(statusImg);
if (!tr || !subjectCell) return;
tr.classList.add(`bt-row-${type}`);
subjectCell.classList.add('bt-cell-special-subject', `bt-cell-${type}-subject`);
applySpecialTopicRowTintInline(tr);
}
function applySpecialTopicRowTintInline(tr) {
if (!tr) return;
const root = document.documentElement;
const themeActive = root.getAttribute('data-bt-active') === 'true';
if (!themeActive) return;
const subjectCell = tr.querySelector(':scope > td.bt-cell-special-subject');
if (!subjectCell) return;
if (!Object.prototype.hasOwnProperty.call(subjectCell.dataset, 'btOriginalSpecialBgColor')) {
subjectCell.dataset.btOriginalSpecialBgColor = subjectCell.style.getPropertyValue('background-color');
subjectCell.dataset.btOriginalSpecialBgColorPriority = subjectCell.style.getPropertyPriority('background-color');
subjectCell.dataset.btOriginalSpecialBgImage = subjectCell.style.getPropertyValue('background-image');
subjectCell.dataset.btOriginalSpecialBgImagePriority = subjectCell.style.getPropertyPriority('background-image');
}
const type = tr.classList.contains('bt-row-sticky') ? 'sticky' : 'locked';
subjectCell.style.setProperty('background-color', `var(--bt-${type}-row-bg)`);
subjectCell.style.setProperty('background-image', 'none');
tr.dataset.btSpecialTintApplied = '1';
}
function clearSpecialTopicRowTintInline(tr) {
if (!tr) return;
const subjectCell = tr.querySelector(':scope > td.bt-cell-special-subject');
if (!subjectCell) return;
if (Object.prototype.hasOwnProperty.call(subjectCell.dataset, 'btOriginalSpecialBgColor')) {
const restoreProperty = (name, value, priority) => {
subjectCell.style.removeProperty(name);
if (value) subjectCell.style.setProperty(name, value, priority || '');
};
restoreProperty(
'background-color',
subjectCell.dataset.btOriginalSpecialBgColor,
subjectCell.dataset.btOriginalSpecialBgColorPriority
);
restoreProperty(
'background-image',
subjectCell.dataset.btOriginalSpecialBgImage,
subjectCell.dataset.btOriginalSpecialBgImagePriority
);
}
tr.dataset.btSpecialTintApplied = '0';
}
// Typography is now handled entirely by static CSS. This migration runs
// once at startup to remove inline markers left by an older userscript
// instance without putting document-wide scans back into the live paths.
function clearLegacyInlineSizing() {
const selector = [
'.bt-breadcrumb-fixed',
'.bt-header-sized',
'.bt-header-self-sized',
'.bt-merit-sized',
'.bt-compose-sized',
].join(', ');
document.querySelectorAll(selector).forEach(el => {
if (el.classList.contains('bt-breadcrumb-fixed')) {
el.style.removeProperty('font-size');
el.style.removeProperty('line-height');
if (Object.prototype.hasOwnProperty.call(el.dataset, 'btOriginalFs')) {
if (el.dataset.btOriginalFs) el.style.fontSize = el.dataset.btOriginalFs;
delete el.dataset.btOriginalFs;
}
el.querySelectorAll('*').forEach(child => {
child.style.removeProperty('font-size');
child.style.removeProperty('line-height');
});
el.classList.remove('bt-breadcrumb-fixed');
}
if (el.classList.contains('bt-header-sized')) {
el.style.removeProperty('font-size');
el.style.removeProperty('line-height');
if (Object.prototype.hasOwnProperty.call(el.dataset, 'btOriginalHdrFs')) {
if (el.dataset.btOriginalHdrFs) el.style.fontSize = el.dataset.btOriginalHdrFs;
delete el.dataset.btOriginalHdrFs;
}
el.querySelectorAll('*').forEach(child => {
child.style.removeProperty('font-size');
child.style.removeProperty('line-height');
});
el.classList.remove('bt-header-sized');
}
if (el.classList.contains('bt-header-self-sized')) {
el.style.removeProperty('font-size');
el.style.removeProperty('line-height');
el.style.removeProperty('font-weight');
if (Object.prototype.hasOwnProperty.call(el.dataset, 'btOriginalHdrSelfFs')) {
if (el.dataset.btOriginalHdrSelfFs) el.style.fontSize = el.dataset.btOriginalHdrSelfFs;
if (el.dataset.btOriginalHdrSelfLh) el.style.lineHeight = el.dataset.btOriginalHdrSelfLh;
if (el.dataset.btOriginalHdrSelfFw) el.style.fontWeight = el.dataset.btOriginalHdrSelfFw;
delete el.dataset.btOriginalHdrSelfFs;
delete el.dataset.btOriginalHdrSelfLh;
delete el.dataset.btOriginalHdrSelfFw;
}
el.classList.remove('bt-header-self-sized');
}
if (el.classList.contains('bt-merit-sized')) {
el.style.removeProperty('font-size');
el.style.removeProperty('line-height');
el.style.removeProperty('font-family');
el.style.removeProperty('letter-spacing');
if (Object.prototype.hasOwnProperty.call(el.dataset, 'btOriginalMeritFs')) {
if (el.dataset.btOriginalMeritFs) el.style.fontSize = el.dataset.btOriginalMeritFs;
if (el.dataset.btOriginalMeritLh) el.style.lineHeight = el.dataset.btOriginalMeritLh;
if (el.dataset.btOriginalMeritFamily) el.style.fontFamily = el.dataset.btOriginalMeritFamily;
delete el.dataset.btOriginalMeritFs;
delete el.dataset.btOriginalMeritLh;
delete el.dataset.btOriginalMeritFamily;
}
el.classList.remove('bt-merit-sized');
}
if (el.classList.contains('bt-compose-sized')) {
el.style.removeProperty('font-size');
el.style.removeProperty('line-height');
if (Object.prototype.hasOwnProperty.call(el.dataset, 'btOriginalComposeFs')) {
if (el.dataset.btOriginalComposeFs) el.style.fontSize = el.dataset.btOriginalComposeFs;
if (el.dataset.btOriginalComposeLh) el.style.lineHeight = el.dataset.btOriginalComposeLh;
delete el.dataset.btOriginalComposeFs;
delete el.dataset.btOriginalComposeLh;
}
el.classList.remove('bt-compose-sized');
}
});
removeMeritLockStyle();
}
function removeMeritLockStyle() {
const style = document.getElementById('bt-merit-lock-css');
if (style) style.remove();
}
function stopMeritSizingObserver() {
// Kept as a no-op compatibility hook for mobile/runtime reconciliation.
removeMeritLockStyle();
}
function getImageBaseName(img) {
const src = img.getAttribute('src') || '';
const clean = src.split(/[?#]/)[0];
const file = clean.slice(clean.lastIndexOf('/') + 1);
return file
.replace(/\.(?:gif|png|jpe?g|webp)$/i, '')
.toLowerCase()
.replace(/^(?:topic|theme|smf|icon)_/, '');
}
function topicIconHaystack(img) {
const src = (img.getAttribute('src') || '').toLowerCase();
const name = getImageBaseName(img);
const label = ((img.getAttribute('alt') || '') + ' ' + (img.getAttribute('title') || '')).toLowerCase();
return { src, name, label, hay: `${src} ${name} ${label}` };
}
function isCompoundStickyTopicIconName(name, hay) {
// sticky_post / sticky_poll / sticky_locked are the LEFT topic-state icon.
// They are not the inline PIN marker. The inline pin is usually a small
// standalone sticky/pin/pinned icon near the title.
return /(?:^|[_-])sticky(?:[_-](?:post|poll|locked)|locked)(?:[_-]|$)/.test(name) ||
/sticky_(?:post|poll|locked)/.test(name) ||
/stickylocked|stickied_post|stickied_poll/.test(name) ||
/sticky[_-]?(?:topic|thread|post|poll|locked)|(?:^|[_-])sticky(?:[_-]|$).*(?:post|poll|locked)/.test(hay);
}
function isCompoundLockedTopicIconName(name, hay) {
return /locked?[_-](?:post|poll|topic|thread)/.test(name) ||
/(?:^|[_-])locked(?:[_-](?:post|poll|topic|thread)|post|poll)(?:[_-]|$)/.test(name) ||
/sticky[_-]?locked/.test(name) ||
/lock(?:ed)?[_-]?(?:topic|thread|post|poll)|(?:^|[_-])lock(?:ed)?(?:[_-]|$).*(?:post|poll|topic|thread)/.test(hay);
}
function topicIconTypeForImage(img) {
const { src, name, label, hay } = topicIconHaystack(img);
// Avoid touching avatars, rank images, logos, the compose toolbar, and
// actual inline post images. Thread/message icons can live in wrappers
// such as .td_headerandpost, but should not live inside the .post body.
if (
img.closest('#bbcBox_message, .bbc_buttons, [id^="bbc_"], td.post, .post, .personalmessage, .signature, .poster_info') ||
/avatar|rank|star|user|flag|logo/i.test(src)
) {
return null;
}
const looksLikeTopicIcon =
/\/topic\//i.test(src) ||
/(?:^|[_-])(normal|hot|veryhot|very_hot|locked|lock|sticky|pin|pinned|sticky_locked|poll|moved|redirect|new|unread)(?:[_-]|$)/i.test(name) &&
/(post|topic|poll|locked|lock|sticky|pin|pinned|hot|veryhot|normal|moved|redirect|new|unread)/i.test(name + ' ' + label);
const looksLikeMessageIcon =
/\/post\//i.test(src) ||
/\/message/i.test(src) ||
/(?:^|[_-])(standard|xx|exclam|exclamation|question|lamp|idea|thumbup|thumbdown|thumb_up|thumb_down|smiley|happy|angry|sad|cheesy|grin|wink|new|unread)(?:[_-]|$)/i.test(name);
if (!looksLikeTopicIcon && !looksLikeMessageIcon) return null;
if (/(^|[_\s./-])(new|unread)([_\s./-]|$)|\bnew\b|\bunread\b/.test(hay)) return 'newPost';
if (/moved|redirect/.test(hay)) return 'moved';
const compoundSticky = isCompoundStickyTopicIconName(name, hay);
const compoundLocked = isCompoundLockedTopicIconName(name, hay);
// Compound SMF files such as sticky_locked.gif, locked_post.gif and
// sticky_post.gif are LEFT-column folder/topic-state images. In the
// classic visual layout, the PIN/LOCK markers belong beside the title.
// Therefore this function returns only the neutral left-state icon; the
// actual status marker is added by maybeAddInlineStatusIcon().
if (compoundSticky || compoundLocked) {
if (/poll/.test(hay)) return 'poll';
if (/veryhot|very_hot|very hot/.test(hay)) return 'veryhot';
if (/(^|[_\s/-])hot([_\s./-]|$)/.test(hay)) return 'hot';
return 'topic';
}
if (/sticky|pinned|\bpin\b/.test(hay)) return 'sticky';
if (/locked|\block\b/.test(hay)) return 'locked';
if (/poll/.test(hay)) return 'poll';
if (/veryhot|very_hot|very hot/.test(hay)) return 'veryhot';
if (/(^|[_\s/-])hot([_\s./-]|$)/.test(hay)) return 'hot';
if (/exclam|exclamation|important|warning|alert|\!/.test(hay)) return 'important';
if (/question|\?/.test(hay)) return 'question';
if (/lamp|idea|lightbulb|bulb/.test(hay)) return 'idea';
if (/thumbup|thumb_up|thumbs? up|\+1/.test(hay)) return 'thumbsUp';
if (/thumbdown|thumb_down|thumbs? down|-1/.test(hay)) return 'thumbsDown';
if (/angry|mad/.test(hay)) return 'angry';
if (/smiley|happy|cheesy|grin|wink|sad/.test(hay)) return 'smile';
if (/standard|xx/.test(hay)) return 'standard';
return 'topic';
}
function isInlineTopicStatusIcon(img, type) {
if (type !== 'sticky' && type !== 'locked' && type !== 'newPost') return false;
const { name, hay } = topicIconHaystack(img);
if (type === 'newPost') return true;
if (type === 'sticky') {
return /(?:^|[_\s./-])(sticky|pin|pinned)([_\s./-]|$)|\bsticky\b|\bpinned\b|\bpin\b/.test(hay) &&
!isCompoundStickyTopicIconName(name, hay);
}
if (type === 'locked') {
return /(?:^|[_\s./-])(lock|locked)([_\s./-]|$)|\blocked\b/.test(hay) &&
!isCompoundLockedTopicIconName(name, hay);
}
return false;
}
function findTopicTitleLinkNear(img) {
const row = img.closest('tr');
const scope = row || img.closest('td') || document;
const links = Array.from(scope.querySelectorAll('a[href*="topic="]'));
return links.find(a => {
const text = (a.textContent || '').trim();
const href = a.getAttribute('href') || '';
if (!text) return false;
if (/#[a-z0-9_-]+$/i.test(href) && !/topic=/i.test(href)) return false;
if (/new|last post|last edit|notify/i.test(text) && text.length < 12) return false;
return true;
}) || null;
}
function insertStatusIconAfterTitle(titleLink, span) {
const parent = titleLink && titleLink.parentNode;
if (!parent) return false;
// Keep status markers in the title area, but never add raw text nodes.
// OFF mode can then hide/remove modern elements without leaving visible
// whitespace artefacts in the classic Bitcointalk layout.
const children = Array.from(parent.childNodes);
const titleIndex = children.indexOf(titleLink);
let anchor = titleLink;
for (let i = titleIndex + 1; i < children.length; i += 1) {
const node = children[i];
if (node.nodeType === Node.ELEMENT_NODE && node.matches && node.matches('.bt-topic-icon--inline-status[data-bt-replacement="1"]')) {
anchor = node;
continue;
}
if (node.nodeType === Node.TEXT_NODE && !String(node.nodeValue || '').trim()) continue;
break;
}
anchor.insertAdjacentElement('afterend', span);
return true;
}
function makeTopicStatusSpan(type, sourceImg) {
if (!TOPIC_ICON_SVGS[type]) return null;
const span = document.createElement('span');
span.className = `bt-topic-icon bt-topic-icon--${type} bt-topic-icon--inline-status`;
span.dataset.btReplacement = '1';
span.dataset.btSynthetic = '1';
span.dataset.btTopicType = type;
span.title = (sourceImg && (sourceImg.alt || sourceImg.title)) ||
(type === 'sticky' ? 'Pinned topic' : type === 'locked' ? 'Locked topic' : type);
span.setAttribute('aria-label', span.title);
span.innerHTML = TOPIC_ICON_SVGS[type];
return span;
}
function maybeAddInlineStatusIcon(img, type) {
const titleLink = findTopicTitleLinkNear(img);
if (!titleLink || !titleLink.parentNode) return false;
const parent = titleLink.parentNode;
const existing = parent.querySelector(`.bt-topic-icon--inline-status[data-bt-topic-type="${type}"]`);
if (existing) return true;
const span = makeTopicStatusSpan(type, img);
if (!span) return false;
return insertStatusIconAfterTitle(titleLink, span);
}
function modernizeTopicListIcons(scope = document) {
// v2.10.17: clean up any leftover bt-topic-icon spans that previous
// versions of this script accidentally created inside compose-panel
// smiley links. Without this, users upgrading from 2.10.15/16 keep
// seeing the duplicate outline glyphs until they hard-reload.
queryAllWithin(scope,
'a[onclick*="replaceText"] > span.bt-topic-icon[data-bt-replacement="1"]'
).forEach(span => span.remove());
const imgs = queryAllWithin(scope,
'img[src*="/topic/"], img[src*="/post/"], img[src*="/Post/"], ' +
'img[src*="normal_post"], img[src*="hot_post"], img[src*="veryhot_post"], ' +
'img[src*="locked_post"], img[src*="lock_post"], img[src*="sticky_post"], img[src*="sticky_locked"], ' +
'img[src*="lock"], img[src*="locked"], img[src*="sticky"], img[src*="pin"], img[src*="pinned"], ' +
'img[src*="new"], img[src*="unread"], img[alt*="New"], img[title*="New"], img[alt*="new"], img[title*="new"], ' +
'img[alt*="Sticky"], img[title*="Sticky"], img[alt*="Pinned"], img[title*="Pinned"], ' +
'img[alt*="Locked"], img[title*="Locked"], img[alt*="Lock"], img[title*="Lock"], ' +
'img[src*="normal_poll"], img[src*="hot_poll"], img[src*="veryhot_poll"], ' +
'img[src*="locked_poll"], img[src*="sticky_poll"], ' +
'img[src*="moved"], img[src*="redirect"], ' +
'img[src*="exclam"], img[src*="exclamation"], img[src*="question"], ' +
'img[src*="lamp"], img[src*="idea"], img[src*="thumbup"], img[src*="thumbdown"], ' +
'img[src*="thumb_up"], img[src*="thumb_down"], img[src*="standard"], img[src*="xx.gif"], ' +
'img[src*="smiley"], img[src*="happy"], img[src*="angry"]'
);
imgs.forEach(img => {
if (img.dataset.btTopicIcon === '1' || img.dataset.btReplaced === '1') return;
// v2.10.17: critical exclusion. modernizeTopicListIcons must never touch
// smiley images that live inside the compose smiley panel — those are
// owned by modernizeSmileys, which renders them as native emoji. The
// old selector matched smiley.gif/happy.gif/angry.gif anywhere on the
// page and inserted outline SVG spans on top of the emoji spans,
// producing two visually inconsistent smiley layers in the same <a>.
// Detection: the smiley panel uses <a onclick="replaceText(...)"> and
// sits inside the post form (#postmodify / form[name=postmodify]).
const inComposeSmileyPanel =
img.closest('a[onclick*="replaceText"]') ||
img.dataset.btSmiley === '1' ||
(img.closest('form[name="postmodify"], #postmodify, form[action*="action=post"]') &&
/\/Smileys\//i.test(img.getAttribute('src') || ''));
if (inComposeSmileyPanel) return;
// v2.10.15: Inline LOCK and NEW are handled by replacing the IMG src
// with a modern SVG data-URL. The IMG keeps its native SMF position
// (align="right" for lock, inside <a href="...new"> for new) so the
// browser does the layout for us. No synthetic span, no display:none.
// v2.10.16: STICKY pushpin added with the same approach.
if (isInlineStickyImage(img)) {
applyInlineIconSrc(img, INLINE_STICKY_DATAURL);
img.dataset.btTopicIcon = '1';
img.dataset.btTopicType = 'sticky';
img.classList.add('bt-inline-status-img', 'bt-inline-status-img--sticky');
markSpecialTopicRow(img, 'sticky');
return;
}
if (isInlineLockImage(img)) {
applyInlineIconSrc(img, INLINE_LOCK_DATAURL);
img.dataset.btTopicIcon = '1';
img.dataset.btTopicType = 'locked';
img.classList.add('bt-inline-status-img', 'bt-inline-status-img--locked');
markSpecialTopicRow(img, 'locked');
return;
}
if (isInlineNewImage(img)) {
applyInlineIconSrc(img, INLINE_NEW_DATAURL);
img.dataset.btTopicIcon = '1';
img.dataset.btTopicType = 'newPost';
img.classList.add('bt-inline-status-img', 'bt-inline-status-img--newPost');
return;
}
const { name, hay } = topicIconHaystack(img);
const compoundSticky = isCompoundStickyTopicIconName(name, hay);
const compoundLocked = isCompoundLockedTopicIconName(name, hay) || /sticky[_-]?locked/.test(name + ' ' + hay);
// Compound folder/status images live in the first column, so resolve
// the actual subject cell through its topic link before applying tints.
if (compoundLocked) markSpecialTopicRow(img, 'locked');
if (compoundSticky) markSpecialTopicRow(img, 'sticky');
// Compound SMF icons such as sticky_locked.gif are left-column state
// icons (folder + pin overlay). v2.10.15 follows the classic layout:
// sticky/locked compounds render only as the modernised left-column
// folder icon, with no extra inline marker beside the title. The lone
// quick_lock.gif is the only inline status indicator (handled above).
const type = topicIconTypeForImage(img);
if (!type || !TOPIC_ICON_SVGS[type]) return;
img.dataset.btTopicIcon = '1';
img.dataset.btReplaced = '1';
if (!Object.prototype.hasOwnProperty.call(img.dataset, 'btOriginalStyle')) {
img.dataset.btOriginalStyle = img.getAttribute('style') || '';
}
img.style.setProperty('display', 'none', 'important');
// Skip lone sticky/locked/newPost that fell through the inline detectors
// — let them render via their natural SMF image rather than producing a
// misplaced span in the left column.
if (type === 'sticky' || type === 'locked' || type === 'newPost') {
return;
}
const span = document.createElement('span');
span.className = `bt-topic-icon bt-topic-icon--${type}`;
span.dataset.btReplacement = '1';
span.dataset.btTopicType = type;
span.title = img.alt || img.title || type;
span.setAttribute('aria-label', span.title);
span.innerHTML = TOPIC_ICON_SVGS[type];
if (img.parentNode) {
img.parentNode.insertBefore(span, img);
}
});
if (!isThemeActiveNow()) setModernizedControlsActive(false, scope);
}
function modernizeBBCodeIcons(scope = document) {
const imgs = queryAllWithin(scope,
'#bbcBox_message img, .bbc_buttons img, [id^="bbc_"] img, ' +
'img[src*="/bbc/"], img[src*="bbc_"], ' +
'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);
const name = m ? m[1].toLowerCase().replace(/^(bbc|frostee|theme)_/, '') : '';
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.dataset.btReplacement = '1';
span.title = img.alt || img.title || label;
span.textContent = label;
span.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
img.click();
});
img.dataset.btReplaced = '1';
if (!Object.prototype.hasOwnProperty.call(img.dataset, 'btOriginalStyle')) {
img.dataset.btOriginalStyle = img.getAttribute('style') || '';
}
img.style.setProperty('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;
span.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
img.click();
});
img.dataset.btReplaced = '1';
if (!Object.prototype.hasOwnProperty.call(img.dataset, 'btOriginalStyle')) {
img.dataset.btOriginalStyle = img.getAttribute('style') || '';
}
img.style.setProperty('display', 'none', 'important');
img.parentNode.insertBefore(span, img);
});
if (!isThemeActiveNow()) setModernizedControlsActive(false, scope);
}
// ===========================================================================
// New-PM highlight (client-side port of PowerGlove's SMF patch)
// ===========================================================================
//
// PowerGlove's original patch is server-side (SMF Themes/default/index.template.php):
// when $context['user']['unread_messages'] > 0 it renders a small indicator
// next to the "Messages" maintab. We cannot touch bitcointalk's PHP, so we
// reproduce it in the DOM. SMF already prints the unread count inside the PM
// link as "[<strong>N</strong>]", so we read it and flag the tab; the
// stylesheet then lights it up with a pulsing accent dot.
const PM_LINK_SELECTOR =
'[class*="maintab_"] a[href*="action=pm"], ' +
'#main_menu a[href*="action=pm"], ' +
'.dropmenu a[href*="action=pm"]';
function pmUnreadCount(link) {
// Preferred signal: the bold count SMF renders for unread PMs.
const strong = link.querySelector('strong');
if (strong) {
const n = parseInt((strong.textContent || '').replace(/\D+/g, ''), 10);
if (n > 0) return n;
}
// Fallback: a bracketed number anywhere in the link text, e.g. "[3]".
const m = (link.textContent || '').match(/\[\s*(\d+)\s*\]/);
if (m) {
const n = parseInt(m[1], 10);
if (n > 0) return n;
}
return 0;
}
function highlightUnreadPm(scope = document) {
let links;
try {
links = queryAllWithin(scope, PM_LINK_SELECTOR);
} catch (e) {
return;
}
links.forEach((link) => {
const count = pmUnreadCount(link);
const tab = link.closest('td[class*="maintab_"], li') || link;
if (count > 0) {
link.classList.add('bt-pm-unread');
tab.classList.add('bt-pm-unread-tab');
link.setAttribute('data-bt-pm-count', String(count));
} else {
link.classList.remove('bt-pm-unread');
tab.classList.remove('bt-pm-unread-tab');
link.removeAttribute('data-bt-pm-count');
}
});
}
// ===========================================================================
// Smiley modernisation
// ===========================================================================
const SMILEY_SVGS = {
smiley: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><circle class="bt-smiley-eye" cx="8.7" cy="9.6" r="1.25"/><circle class="bt-smiley-eye" cx="15.3" cy="9.6" r="1.25"/><path class="bt-smiley-line" d="M7.8 14.1c1 1.7 2.5 2.6 4.2 2.6s3.2-.9 4.2-2.6"/></svg>',
wink: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><circle class="bt-smiley-eye" cx="15.3" cy="9.6" r="1.25"/><path class="bt-smiley-line" d="M7.5 9.6h2.6"/><path class="bt-smiley-line" d="M7.8 14.2c1 1.6 2.5 2.5 4.2 2.5s3.2-.9 4.2-2.5"/></svg>',
grin: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><circle class="bt-smiley-eye" cx="8.7" cy="9.3" r="1.2"/><circle class="bt-smiley-eye" cx="15.3" cy="9.3" r="1.2"/><path class="bt-smiley-fill" d="M7.4 13.2h9.2c-.35 2.4-2.1 3.9-4.6 3.9s-4.25-1.5-4.6-3.9z"/><path class="bt-smiley-line" d="M7.4 13.2h9.2"/></svg>',
laugh: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><path class="bt-smiley-line" d="M7.5 9.2l2.3 1.5M16.5 9.2l-2.3 1.5"/><path class="bt-smiley-fill" d="M7.3 13.4h9.4c-.45 2.7-2.2 4.1-4.7 4.1s-4.25-1.4-4.7-4.1z"/></svg>',
sad: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><circle class="bt-smiley-eye" cx="8.7" cy="9.6" r="1.25"/><circle class="bt-smiley-eye" cx="15.3" cy="9.6" r="1.25"/><path class="bt-smiley-line" d="M8 16.2c1-1.4 2.3-2.1 4-2.1s3 .7 4 2.1"/></svg>',
angry: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><path class="bt-smiley-line" d="M7.4 8.2l3 1.5M16.6 8.2l-3 1.5"/><circle class="bt-smiley-eye" cx="9" cy="10.8" r="1.05"/><circle class="bt-smiley-eye" cx="15" cy="10.8" r="1.05"/><path class="bt-smiley-line" d="M8.2 16.3c1-.9 2.3-1.4 3.8-1.4s2.8.5 3.8 1.4"/></svg>',
shocked: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><circle class="bt-smiley-eye" cx="8.7" cy="9.3" r="1.2"/><circle class="bt-smiley-eye" cx="15.3" cy="9.3" r="1.2"/><ellipse class="bt-smiley-line" cx="12" cy="15" rx="2.1" ry="2.6"/></svg>',
cool: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><path class="bt-smiley-fill" d="M5.9 9h5.1v2.1c0 1.25-.95 2.2-2.25 2.2H8.1c-1.3 0-2.2-.95-2.2-2.2V9zm7.1 0h5.1v2.1c0 1.25-.95 2.2-2.25 2.2h-.65c-1.3 0-2.2-.95-2.2-2.2V9z"/><path class="bt-smiley-line" d="M11 10h2M8.2 16c1 .7 2.3 1.1 3.8 1.1s2.8-.4 3.8-1.1"/></svg>',
confused: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><circle class="bt-smiley-eye" cx="8.7" cy="9.6" r="1.2"/><circle class="bt-smiley-eye" cx="15.3" cy="9.6" r="1.2"/><path class="bt-smiley-line" d="M8.5 15.6c2.1-.8 4.4-.8 7 0"/></svg>',
tongue: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><circle class="bt-smiley-eye" cx="8.7" cy="9.4" r="1.2"/><circle class="bt-smiley-eye" cx="15.3" cy="9.4" r="1.2"/><path class="bt-smiley-line" d="M8 13.7h8"/><path class="bt-smiley-tongue" d="M10 13.7v2.2a2 2 0 0 0 4 0v-2.2"/></svg>',
heart: '<svg viewBox="0 0 24 24" aria-hidden="true"><path class="bt-smiley-heart" d="M12 20s-7-4.4-9.1-8.3C1.3 8.8 3 5.7 6.1 5.3c1.8-.2 3.3.6 4.1 2 1-1.6 2.5-2.4 4.4-2 3.1.6 4.6 3.7 3 6.4C15.6 15.6 12 20 12 20z"/></svg>',
thumbsup: '<svg viewBox="0 0 24 24" aria-hidden="true"><path class="bt-smiley-line" d="M7 21H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3v11zM7 10l4-7a2.5 2.5 0 0 1 2.5 2.5V9H19a2 2 0 0 1 2 2l-1 6.5A3 3 0 0 1 17 20H7"/></svg>',
thumbsdown: '<svg viewBox="0 0 24 24" aria-hidden="true"><path class="bt-smiley-line" d="M17 3h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3V3zM17 14l-4 7a2.5 2.5 0 0 1-2.5-2.5V15H5a2 2 0 0 1-2-2l1-6.5A3 3 0 0 1 7 4h10"/></svg>',
star: '<svg viewBox="0 0 24 24" aria-hidden="true"><path class="bt-smiley-star" d="M12 2.8l2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.1 6.4 20l1.1-6.2L3 9.4l6.2-.9L12 2.8z"/></svg>',
fire: '<svg viewBox="0 0 24 24" aria-hidden="true"><path class="bt-smiley-fire" d="M12 22a7 7 0 0 1-7-7c0-3.8 2.5-5.9 5.1-8.7.1 2.1.9 3.6 2.4 4.6 1.5-2.2 1.7-4.7.6-8 3.8 2.8 5.9 6.7 5.9 10.8A7 7 0 0 1 12 22z"/></svg>',
check: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><path class="bt-smiley-line" d="M7 12.3l3.1 3.1L17.5 8"/></svg>',
cross: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle class="bt-smiley-face" cx="12" cy="12" r="10"/><path class="bt-smiley-line" d="M8.5 8.5l7 7M15.5 8.5l-7 7"/></svg>'
};
const SMILEY_ALIASES = {
happy: 'smiley', cheesy: 'grin', grin: 'grin', lol: 'laugh', laugh: 'laugh',
cry: 'sad', mad: 'angry', surprised: 'shocked', huh: 'confused', undecided: 'confused',
sunglasses: 'cool', embarrassed: 'confused', lipsrsealed: 'confused', kiss: 'smiley',
rolleyes: 'confused', evil: 'angry', azn: 'cool', afro: 'cool', police: 'cool',
wassat: 'confused', chocked: 'shocked', skeptical: 'confused', facepalm: 'sad',
thumbup: 'thumbsup', thumbs_up: 'thumbsup', thumbdown: 'thumbsdown', thumbs_down: 'thumbsdown',
tick: 'check',
// v2.10.16: SMF Bitcointalk serves the default-smile button as smile.gif
// (not smiley.gif), so without this alias it slipped through and stayed
// as the original GIF while every other glyph rendered as an emoji.
smile: 'smiley'
};
// Keep this map ASCII-only. Bitcointalk can expose a legacy page encoding,
// which would turn literal UTF-8 emoji in the userscript into mojibake.
// JavaScript decodes these Unicode escapes only after the script is loaded.
const SMILEY_EMOJI = {
smiley: '\u{1F60A}', wink: '\u{1F609}', grin: '\u{1F600}',
laugh: '\u{1F604}', sad: '\u{1F61E}', angry: '\u{1F62C}',
shocked: '\u{1F62E}', cool: '\u{1F60E}', confused: '\u{1F615}',
tongue: '\u{1F61B}', heart: '\u2764\uFE0F',
thumbsup: '\u{1F44D}', thumbsdown: '\u{1F44E}', star: '\u2B50',
fire: '\u{1F525}', check: '\u2705', cross: '\u274C'
};
function smileyGlyphForName(name) {
const key = SMILEY_EMOJI[name] ? name : SMILEY_ALIASES[name];
return key ? SMILEY_EMOJI[key] : null;
}
function smileySvgForName(name) {
const key = SMILEY_SVGS[name] ? name : SMILEY_ALIASES[name];
return key ? SMILEY_SVGS[key] : null;
}
function stylePostMetaButtons(scope = document) {
queryAllWithin(scope, 'a').forEach(a => {
if (a.dataset.btPostMeta === '1') return;
const text = (a.textContent || '').trim();
const href = a.getAttribute('href') || '';
const isMerit = /^\+\s*merit\b/i.test(text);
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(scope = document) {
const imgs = queryAllWithin(scope,
'img[src*="Smileys/"], img[src*="/smileys/"], img.smiley'
);
imgs.forEach(img => {
if (img.dataset.btSmiley === '1') return;
const src = img.getAttribute('src') || '';
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 glyph = smileyGlyphForName(name);
if (!glyph) return;
const span = document.createElement('span');
span.className = 'bt-smiley bt-smiley-modern bt-smiley-native';
span.textContent = glyph;
span.title = img.alt || img.title || name;
img.dataset.btSmiley = '1';
if (!Object.prototype.hasOwnProperty.call(img.dataset, 'btOriginalStyle')) {
img.dataset.btOriginalStyle = img.getAttribute('style') || '';
}
img.style.setProperty('display', 'none', 'important');
img.parentNode.insertBefore(span, img);
});
if (!isThemeActiveNow()) setModernizedControlsActive(false, scope);
}
// ===========================================================================
// CSS
// ===========================================================================
const css = `
: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);
}
:root[data-bt-theme="light"] {
--bt-bg: #F2F4F7;
--bt-surface-1: #E3E6EA;
--bt-surface-2: #D7DAE0;
--bt-surface-3: #C5C8CD;
--bt-post-bg: #EEF3F7;
--bt-post-alt-bg: #E3EBF1;
--bt-poster-bg: #E8EEF3;
--bt-poster-alt-bg: #DCE7EE;
--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);
}
: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;
--bt-accent: #F7931A;
--bt-accent-h: #FFA733;
--bt-accent-a: #E08410;
--bt-on-accent: #1A1D21;
}
/*-- BT_ALWAYS_ON_START --*/
:root[data-bt-active="false"] .bt-bbc-icon,
:root[data-bt-active="false"] .bt-smiley,
:root[data-bt-active="false"] .bt-topic-icon[data-bt-replacement="1"],
:root[data-bt-active="false"] .bt-post-action[data-bt-replacement="1"] {
display: none !important;
}
[data-bt-no-transitions] *,
[data-bt-no-transitions] *::before,
[data-bt-no-transitions] *::after {
transition: none !important;
animation-duration: 0s !important;
}
/*-- BT_ALWAYS_ON_END --*/
html,
body {
background: var(--bt-bg) !important;
color: var(--bt-text) !important;
font-family: var(--bt-font) !important;
font-size: 12px !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;
}
body, td, th, tr { color: var(--bt-text) !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-accent) !important;
text-decoration: none !important;
transition: color var(--bt-trans) !important;
}
a:hover, a:active, a:focus {
color: var(--bt-accent-h) !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;
}
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,
tr.windowbg3:hover > td {
background-color: var(--bt-surface-2) !important;
}
/* v2.8.1: restore the classic-looking post alternation on light-theme
thread pages. The previous attempt only caught a narrow header row on
some thread layouts (the "Report to moderator" strip), because the
actual post body/sidebar wrappers are often .td_headerandpost, .post,
.poster and .poster_info rather than the row cell that carries
windowbg/windowbg2 itself.
We therefore paint BOTH the row cells and the common wrappers inside
them, so the entire post block alternates again like classic
bitcointalk/SMF. */
:root[data-bt-theme="light"] tr.windowbg > td,
:root[data-bt-theme="light"] td.windowbg,
:root[data-bt-theme="light"] tr.windowbg td.td_headerandpost,
:root[data-bt-theme="light"] td.windowbg.td_headerandpost,
:root[data-bt-theme="light"] tr.windowbg td.post,
:root[data-bt-theme="light"] td.windowbg.post,
:root[data-bt-theme="light"] tr.windowbg td.poster,
:root[data-bt-theme="light"] tr.windowbg td.poster_info,
:root[data-bt-theme="light"] td.windowbg.poster,
:root[data-bt-theme="light"] td.windowbg.poster_info {
background-color: #e9f0f7 !important;
background-image: none !important;
}
:root[data-bt-theme="light"] tr.windowbg2 > td,
:root[data-bt-theme="light"] td.windowbg2,
:root[data-bt-theme="light"] tr.windowbg2 td.td_headerandpost,
:root[data-bt-theme="light"] td.windowbg2.td_headerandpost,
:root[data-bt-theme="light"] tr.windowbg2 td.post,
:root[data-bt-theme="light"] td.windowbg2.post,
:root[data-bt-theme="light"] tr.windowbg2 td.poster,
:root[data-bt-theme="light"] tr.windowbg2 td.poster_info,
:root[data-bt-theme="light"] td.windowbg2.poster,
:root[data-bt-theme="light"] td.windowbg2.poster_info {
background-color: #dfe9f3 !important;
background-image: none !important;
}
:root[data-bt-theme="light"] tr.windowbg3 > td,
:root[data-bt-theme="light"] td.windowbg3,
:root[data-bt-theme="light"] tr.windowbg3 td.td_headerandpost,
:root[data-bt-theme="light"] td.windowbg3.td_headerandpost,
:root[data-bt-theme="light"] tr.windowbg3 td.post,
:root[data-bt-theme="light"] td.windowbg3.post,
:root[data-bt-theme="light"] tr.windowbg3 td.poster,
:root[data-bt-theme="light"] tr.windowbg3 td.poster_info,
:root[data-bt-theme="light"] td.windowbg3.poster,
:root[data-bt-theme="light"] td.windowbg3.poster_info {
background-color: #e9f0f7 !important;
background-image: none !important;
}
:root[data-bt-theme="light"] tr.windowbg:hover > td,
:root[data-bt-theme="light"] tr.windowbg:hover td.td_headerandpost,
:root[data-bt-theme="light"] tr.windowbg:hover td.post,
:root[data-bt-theme="light"] tr.windowbg:hover td.poster,
:root[data-bt-theme="light"] tr.windowbg:hover td.poster_info,
:root[data-bt-theme="light"] tr.windowbg2:hover > td,
:root[data-bt-theme="light"] tr.windowbg2:hover td.td_headerandpost,
:root[data-bt-theme="light"] tr.windowbg2:hover td.post,
:root[data-bt-theme="light"] tr.windowbg2:hover td.poster,
:root[data-bt-theme="light"] tr.windowbg2:hover td.poster_info,
:root[data-bt-theme="light"] tr.windowbg3:hover > td,
:root[data-bt-theme="light"] tr.windowbg3:hover td.td_headerandpost,
:root[data-bt-theme="light"] tr.windowbg3:hover td.post,
:root[data-bt-theme="light"] tr.windowbg3:hover td.poster,
:root[data-bt-theme="light"] tr.windowbg3:hover td.poster_info {
background-color: #d7e3ef !important;
background-image: none !important;
}
:root[data-bt-active="true"] td.bt-read-post {
position: relative !important;
}
:root[data-bt-active="true"] td.bt-read-post::after {
content: "";
position: absolute;
inset: 0;
z-index: 30;
pointer-events: none;
background: rgba(128, 128, 128, 0.22);
backdrop-filter: grayscale(0.75) saturate(0.5);
-webkit-backdrop-filter: grayscale(0.75) saturate(0.5);
}
:root[data-bt-active="true"][data-bt-theme="dark"] td.bt-read-post::after {
background: rgba(170, 170, 170, 0.12);
}
.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;
}
:root[data-bt-theme="dark"] td.poster_info *,
:root[data-bt-theme="dark"] td.poster *,
:root[data-bt-theme="dark"] .poster_info * {
color: var(--bt-text-2) !important;
}
:root[data-bt-theme="dark"] td.poster_info b a,
:root[data-bt-theme="dark"] td.poster_info h4 a,
:root[data-bt-theme="dark"] .poster_info b a,
:root[data-bt-theme="dark"] .poster_info h4 a {
color: var(--bt-text) !important;
font-weight: 600 !important;
}
:root[data-bt-theme="dark"] td.poster_info b a:hover,
:root[data-bt-theme="dark"] td.poster_info h4 a:hover,
:root[data-bt-theme="dark"] .poster_info b a:hover,
:root[data-bt-theme="dark"] .poster_info h4 a:hover {
color: var(--bt-accent) !important;
}
:root[data-bt-theme="dark"] td.poster_info a,
:root[data-bt-theme="dark"] .poster_info a {
color: var(--bt-accent) !important;
}
:root[data-bt-theme="dark"] td.poster_info a:hover,
:root[data-bt-theme="dark"] .poster_info a:hover {
color: var(--bt-accent-h) !important;
}
:root[data-bt-theme="dark"] td.poster_info img,
:root[data-bt-theme="dark"] .poster_info img { color: initial !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;
}
[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;
}
/* v2.10.39: catbg/titlebg headers (the navy/grey banner above content
sections) include the topic header row "Topic: ... (Read N times)"
above each thread's posts. SMF inherits a large font-size for these
cells from the legacy theme stylesheet, which makes the topic title
look oversized vs the body text and the breadcrumb. Bring it back
down to match the classic SMF Bitcointalk subject size: 13-14px,
semibold (the bold weight is already set above). */
body [class*="catbg"],
body [class*="titlebg"],
body td[class*="catbg"],
body td[class*="titlebg"],
body tr[class*="catbg"] > td,
body tr[class*="titlebg"] > td,
body [class*="catbg"] b,
body [class*="titlebg"] b,
body [class*="catbg"] span:not(.smalltext),
body [class*="titlebg"] span:not(.smalltext),
body [class*="catbg"] a,
body [class*="titlebg"] a {
font-size: 14px !important;
line-height: 1.4 !important;
}
/* v3.0.35: data in alto a destra (span.smalltext dentro #smfheader td.titlebg2)
deve rimanere a 13px — escluso dalla regola sopra tramite :not(.smalltext)
e ribadito qui per battere qualsiasi .smalltext baseline */
body [class*="catbg"] span.smalltext,
body [class*="titlebg"] span.smalltext,
#smfheader td.titlebg2 span.smalltext,
#smfheader td.titlebg2[align="right"] span.smalltext,
#smfheader td.titlebg2 span.smalltext * {
font-size: 13px !important;
line-height: normal !important;
}
/* v2.10.28: when the Classic (sky) preset is active in the light theme,
reproduce the SMF Bitcointalk gradient look more faithfully:
- catbg = top-level category banners → deep blue gradient with white text
- titlebg = sub-section headers → light blue gradient with dark text
This matches the original cascade where each header layer has its own
distinct identity instead of both falling back to the same surface tone. */
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body [class*="catbg"],
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body td[class*="catbg"],
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body th[class*="catbg"],
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body tr[class*="catbg"] > td,
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body tr[class*="catbg"] > th {
background: linear-gradient(180deg, #8FA8CE 0%, #6E8EBE 100%) !important;
color: #FFFFFF !important;
border-bottom: 1px solid #5C7AAB !important;
}
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body [class*="catbg"] a,
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body tr[class*="catbg"] a {
color: #FFFFFF !important;
}
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body [class*="catbg"] a:hover,
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body tr[class*="catbg"] a:hover {
color: #FFE08A !important;
text-decoration: underline !important;
}
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body [class*="titlebg"],
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body td[class*="titlebg"],
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body th[class*="titlebg"],
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body tr[class*="titlebg"] > td,
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body tr[class*="titlebg"] > th {
background: linear-gradient(180deg, #DDE5F0 0%, #C2D2E5 100%) !important;
color: #1F2A3A !important;
border-bottom: 1px solid #A6B7CD !important;
}
/* Classic Bitcointalk uses the legendary #225588 link blue everywhere
except inside the dark catbg banners. Reinforce that for normal body
links so the page reads as authentically classic. */
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body td[class*="windowbg"] a:link,
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body td[class*="windowbg"] a:visited,
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body .post a:link,
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body .post a:visited {
color: #225588 !important;
}
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body td[class*="windowbg"] a:hover,
:root[data-bt-theme="light"][data-bt-main="sky"][data-bt-active="true"] body .post a:hover {
color: #1A4570 !important;
text-decoration: underline !important;
}
th {
background-color: var(--bt-surface-2) !important;
background-image: none !important;
color: var(--bt-text) !important;
}
.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, .middletext, .small_header, .nav, a.nav, .navPages, .prevnext,
.lastpostcol, .ignored_topic, .utusers, .utusers a {
color: var(--bt-text-2) !important;
}
.smalltext a, .middletext a, .small_header a, .lastpostcol a {
color: var(--bt-accent) !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;
}
.codeheader {
box-sizing: border-box !important;
width: 96% !important;
max-width: 100% !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;
box-sizing: border-box !important;
width: 96% !important;
max-width: 100% !important;
overflow: auto !important;
}
.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: 5px 10px !important;
font-family: var(--bt-font) !important;
font-weight: 600 !important;
font-size: 12px !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;
}
/* v2.10.27: topic action bar (Reply | Unwatch | Notify | Mark unread |
Print | View topic archive) compacted vertically and horizontally so
it occupies less of the precious top/bottom-of-thread real estate.
Reductions: padding 3×7 → 2×6, font-size 12→11px, letter-spacing
0.04em→0.02em, margin-right 3→2px. Results in a strip roughly 25%
shorter without losing readability. */
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: 2px 6px !important;
margin: 0 2px 0 0 !important;
font-family: var(--bt-font) !important;
font-weight: 500 !important;
font-size: 11px !important;
text-transform: uppercase !important;
letter-spacing: 0.02em !important;
line-height: 1.3 !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 {
display: inline-block !important;
padding: 3px 7px !important;
margin: 0 3px 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: 12px !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; }
span[title="Quote to clipboard"] { display: none !important; }
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-font) !important;
font-size: 13px !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; }
.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;
}
.bt-topic-icon {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 17px !important;
height: 17px !important;
margin: 0 4px 0 0 !important;
padding: 1px !important;
border-radius: 4px !important;
color: var(--bt-text-2) !important;
background: transparent !important;
border: none !important;
vertical-align: -3px !important;
box-sizing: border-box !important;
transition: color var(--bt-trans), background var(--bt-trans), opacity var(--bt-trans) !important;
opacity: 1 !important;
}
.bt-topic-icon svg {
width: 100% !important;
height: 100% !important;
display: block !important;
stroke-width: 2.25 !important;
}
.bt-topic-icon:hover {
color: var(--bt-accent) !important;
background: color-mix(in srgb, var(--bt-accent) 10%, transparent) !important;
}
/* v2.10.25: distinct hue for very-hot topics so they pop visually beyond
the default accent. Hot stays on the accent (warm orange) for continuity,
very-hot bumps to a saturated red — a visual "+1 alarm" over hot. Poll
gets its own colour (purple/indigo) so it's identifiable as a poll at a
glance without reading the icon shape. */
.bt-topic-icon--hot,
.bt-topic-icon--question,
.bt-topic-icon--idea,
.bt-topic-icon--thumbsUp {
color: var(--bt-accent) !important;
}
.bt-topic-icon--veryhot {
color: #d93025 !important; /* fire-red, theme-agnostic for instant recognition */
}
.bt-topic-icon--poll {
color: #8b5cf6 !important; /* indigo/violet — consistent across light & dark themes */
}
.bt-topic-icon--important,
.bt-topic-icon--thumbsDown,
.bt-topic-icon--angry {
color: var(--bt-danger) !important;
}
.bt-topic-icon--standard,
.bt-topic-icon--smile {
color: var(--bt-text-2) !important;
}
/* Locked must be clearly visible; 2.10.4 made some locks too faint or only
treated them as inline status. */
.bt-topic-icon--locked {
color: var(--bt-text) !important;
width: 18px !important;
height: 18px !important;
opacity: 1 !important;
}
/* Inline status icons inserted beside the actual topic title. This is used
for the PIN/sticky marker so it appears on the title side, not in the
left topic-state icon column. */
.bt-topic-icon--inline-status {
width: 15px !important;
height: 15px !important;
margin: 0 2px !important;
padding: 0 !important;
border-radius: 0 !important;
background: transparent !important;
border: none !important;
vertical-align: -2px !important;
opacity: 1 !important;
}
.bt-topic-icon--inline-status:hover {
background: transparent !important;
color: var(--bt-accent) !important;
}
.bt-topic-icon--sticky {
color: var(--bt-accent) !important;
}
.bt-topic-icon--inline-status.bt-topic-icon--locked {
color: var(--bt-text) !important;
width: 15px !important;
height: 15px !important;
}
/* v2.10.15: inline status IMGs whose src has been swapped to the modern
SVG data-URL (lock + new). The browser sizes them from the SVG's
width/height attributes, so we just need to harmonise vertical
alignment with the topic title baseline and add a small horizontal
margin so they don't kiss the surrounding text. */
img.bt-inline-status-img {
vertical-align: -2px !important;
margin: 0 4px !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
filter: none !important;
opacity: 1 !important;
}
img.bt-inline-status-img--locked {
/* SMF places this with align="right". Keep the baseline cleaner than
the SVG's natural drop. */
vertical-align: -3px !important;
}
img.bt-inline-status-img--sticky {
/* v2.10.16: pushpin sits on the right edge alongside the lock. Tilted
needle reads better with a slight baseline drop. */
vertical-align: -3px !important;
}
img.bt-inline-status-img--newPost {
vertical-align: -2px !important;
}
/* Locked and sticky tints apply only to the topic subject cell. Both are
derived from the active Main palette, so Cream, Mint, Rose and custom
themes never inherit the classic fixed light-blue background. */
:root[data-bt-theme="light"] {
--bt-locked-row-bg: color-mix(
in srgb,
var(--bt-post-bg, var(--bt-surface-1)) 72%,
var(--bt-surface-2) 28%
);
--bt-locked-row-bg-hover: color-mix(
in srgb,
var(--bt-post-bg, var(--bt-surface-1)) 55%,
var(--bt-surface-2) 45%
);
--bt-sticky-row-bg: color-mix(
in srgb,
var(--bt-post-bg, var(--bt-surface-1)) 55%,
var(--bt-surface-2) 45%
);
--bt-sticky-row-bg-hover: color-mix(
in srgb,
var(--bt-post-bg, var(--bt-surface-1)) 35%,
var(--bt-surface-2) 65%
);
}
:root[data-bt-theme="dark"] {
--bt-locked-row-bg: color-mix(in srgb, var(--bt-surface-1) 78%, var(--bt-surface-2) 22%);
--bt-locked-row-bg-hover: color-mix(in srgb, var(--bt-surface-1) 60%, var(--bt-surface-2) 40%);
--bt-sticky-row-bg: color-mix(in srgb, var(--bt-surface-1) 65%, var(--bt-surface-2) 35%);
--bt-sticky-row-bg-hover: color-mix(in srgb, var(--bt-surface-1) 40%, var(--bt-surface-2) 60%);
}
:root[data-bt-active="true"][data-bt-theme] body tr.bt-row-locked > td.bt-cell-locked-subject {
background-color: var(--bt-locked-row-bg) !important;
background-image: none !important;
}
:root[data-bt-active="true"][data-bt-theme] body tr.bt-row-locked:hover > td.bt-cell-locked-subject {
background-color: var(--bt-locked-row-bg-hover) !important;
}
:root[data-bt-active="true"][data-bt-theme] body tr.bt-row-sticky > td.bt-cell-sticky-subject {
background-color: var(--bt-sticky-row-bg) !important;
background-image: none !important;
}
:root[data-bt-active="true"][data-bt-theme] body tr.bt-row-sticky:hover > td.bt-cell-sticky-subject {
background-color: var(--bt-sticky-row-bg-hover) !important;
}
.bt-topic-icon--newPost {
width: auto !important;
min-width: 28px !important;
height: 14px !important;
margin: 0 4px 0 2px !important;
padding: 1px 4px !important;
border-radius: 4px !important;
background: color-mix(in srgb, var(--bt-accent) 14%, transparent) !important;
border: 1px solid color-mix(in srgb, var(--bt-accent) 34%, transparent) !important;
color: var(--bt-accent) !important;
font-family: var(--bt-font) !important;
font-size: 9px !important;
font-weight: 700 !important;
letter-spacing: 0.05em !important;
line-height: 1 !important;
text-transform: uppercase !important;
vertical-align: 1px !important;
opacity: 1 !important;
}
.bt-topic-icon--newPost:hover {
background: color-mix(in srgb, var(--bt-accent) 20%, transparent) !important;
color: var(--bt-accent-h) !important;
}
.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;
}
[class*="maintab_"],
[class*="mirrortab_"] {
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;
}
.maintab_first,
.mirrortab_first,
.maintab_active_first + .maintab_active_back,
.mirrortab_active_first + .mirrortab_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,
.mirrortab_last,
.maintab_active_back + .maintab_active_last,
.mirrortab_active_back + .mirrortab_active_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,
[class*="mirrortab_"] a,
[class*="mirrortab_"] a:link,
[class*="mirrortab_"] 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: 13px !important; /* v3.0.35: was 12px */
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;
}
/* ----------------------------------------------------------------------
New-PM highlight — client-side port of PowerGlove's SMF patch. When the
PM maintab has unread messages (flagged by highlightUnreadPm), show a
pulsing accent dot before the link, in place of the original icon.
---------------------------------------------------------------------- */
[class*="maintab_"] a.bt-pm-unread::before,
#main_menu a.bt-pm-unread::before,
.dropmenu a.bt-pm-unread::before {
content: "" !important;
display: inline-block !important;
width: 7px !important;
height: 7px !important;
margin-right: 7px !important;
border-radius: 50% !important;
background: var(--bt-accent) !important;
vertical-align: 1px !important;
animation: bt-pm-pulse 2s ease-out infinite !important;
}
.maintab_active_back a.bt-pm-unread::before,
.mirrortab_active_back a.bt-pm-unread::before {
background: var(--bt-on-accent) !important;
}
@keyframes bt-pm-pulse {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--bt-accent) 55%, transparent); }
70% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--bt-accent) 0%, transparent); }
100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--bt-accent) 0%, transparent); }
}
/* v2.10.29 / v3.2.3: compact override ONLY for mirrortab_ (topic action bar:
Reply | Unwatch | Notify | Mark unread | Print | view topic archive).
Earlier attempts failed because the markup uses raw <a> elements
inside <td class="mirrortab_back">, NOT a.replybutton/a.notifybutton —
those classes don't exist in this SMF theme. The general maintab/
mirrortab rule above (padding 6×14, font-size 12px, letter-spacing
0.05em) is shared with the top navigation menu, so we override only
mirrortab here to avoid affecting the main nav. */
.mirrortab_first,
.mirrortab_last,
.mirrortab_active_first,
.mirrortab_active_last {
display: none !important;
}
[class*="mirrortab_"] {
white-space: nowrap !important;
border-left: 1px solid var(--bt-border) !important;
border-radius: var(--bt-radius-sm) !important;
}
[class*="mirrortab_"] a,
[class*="mirrortab_"] a:link,
[class*="mirrortab_"] a:visited {
padding: 2px 5px !important;
font-size: 11px !important;
letter-spacing: 0 !important;
line-height: 1.15 !important;
white-space: nowrap !important;
}
/* Tighten the cell itself so vertical padding from the <td> doesn't
reintroduce the height we just removed from the links. */
td[class*="mirrortab_"] {
padding-top: 2px !important;
padding-bottom: 2px !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;
}
.maintab_active_back,
.mirrortab_active_back {
background-color: var(--bt-accent) !important;
border-color: var(--bt-accent) !important;
}
.maintab_active_back a,
.maintab_active_back a:hover,
.mirrortab_active_back a,
.mirrortab_active_back a:hover {
color: var(--bt-on-accent) !important;
font-weight: 600 !important;
}
/* v2.7.15: In LIGHT themes, selected main/mirror tabs use a
theme-derived extrapolated colour: it continues from Main -> surface3
by the same interval again. In other words, if surface3 is the
"intermediate/illuminated" tone, the active tab doubles that HSL move. */
:root[data-bt-theme="light"] .maintab_active_back,
:root[data-bt-theme="light"] .mirrortab_active_back {
background-color: var(--bt-nav-active-bg, var(--bt-surface-3)) !important;
border-color: var(--bt-nav-active-bg, var(--bt-border-2)) !important;
}
:root[data-bt-theme="light"] .maintab_active_back a,
:root[data-bt-theme="light"] .maintab_active_back a:hover,
:root[data-bt-theme="light"] .mirrortab_active_back a,
:root[data-bt-theme="light"] .mirrortab_active_back a:hover {
color: var(--bt-nav-active-text, var(--bt-text)) !important;
font-weight: 600 !important;
}
.maintab_active_first.maintab_active_first,
.maintab_active_last.maintab_active_last,
.mirrortab_active_first.mirrortab_active_first,
.mirrortab_active_last.mirrortab_active_last {
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: 13px !important; /* v3.0.35: was 12px */
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;
}
/* v2.10.33: breadcrumb sizing rule moved below the smalltext/middletext
block — see the override declared after that block. The classic
breadcrumb sits inside <td class="middletext">, which the smalltext
block was sizing down to --bt-fs-small. Since CSS at equal specificity
is decided by source order, the override has to come after. */
#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;
}
/* v2.10.26: top-of-page pagelinks (« 1 2 3 ... 1234 » above topic tables
or post lists) are page-level navigation and must be readable — same
size as body text in classic Bitcointalk. Earlier versions collapsed
them all to --bt-fs-pagelinks (10px), which is appropriate only for
the per-topic mini paginators inside subject cells. The dedicated
per-topic rule (further down, scoped to td[class*="windowbg"] with
:has(> a[href*="topic="])) keeps those at 10px without affecting the
page-level paginator. */
.pagelinks,
.pagelinks a,
.pagelinks a:link,
.pagelinks a:visited,
.pagelinks strong,
.pagelinks b {
font-size: var(--bt-fs-base) !important;
line-height: 1.35 !important;
font-weight: 400 !important;
vertical-align: baseline !important;
}
.pagelinks a,
.pagelinks a:link,
.pagelinks a:visited,
.pagelinks strong,
.pagelinks b {
display: inline-block !important;
min-width: 0 !important;
padding: 1px 4px !important;
margin: 0 1px !important;
border-radius: 4px !important;
}
.pagelinks strong,
.pagelinks b {
font-weight: 600 !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;
}
/* Mobile performance profiles keep the same layout and palette. Balanced
retains progressive DOM modernisation; Lite keeps the minimal path. */
:root[data-bt-mobile-lite="true"] {
--bt-radius: 6px;
--bt-radius-sm: 4px;
--bt-radius-lg: 8px;
--bt-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--bt-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--bt-trans: 0ms linear;
--bt-shadow: none;
--bt-shadow-lg: none;
}
:root[data-bt-mobile-lite="true"],
:root[data-bt-mobile-lite="true"] body {
-webkit-font-smoothing: auto;
text-rendering: auto;
}
:root[data-bt-mobile-lite="true"] body {
background: var(--bt-bg) !important;
background-attachment: scroll !important;
}
:root[data-bt-mobile-lite="true"] [class*="maintab_"] a.bt-pm-unread::before,
:root[data-bt-mobile-lite="true"] #main_menu a.bt-pm-unread::before,
:root[data-bt-mobile-lite="true"] .dropmenu a.bt-pm-unread::before,
:root[data-bt-mobile-lite="true"][data-bt-active="false"] #bt-switcher-button {
animation: none !important;
}
:root[data-bt-mobile-lite="true"] table.bordercolor,
:root[data-bt-mobile-lite="true"] table.table_list,
:root[data-bt-mobile-lite="true"] table.table_grid,
:root[data-bt-mobile-lite="true"] .tborder,
:root[data-bt-mobile-lite="true"] #bt-switcher-button,
:root[data-bt-mobile-lite="true"] #bt-switcher-panel {
box-shadow: none !important;
}
:root[data-bt-mobile-lite="true"] table.bordercolor,
:root[data-bt-mobile-lite="true"] table.table_list,
:root[data-bt-mobile-lite="true"] table.table_grid,
:root[data-bt-mobile-lite="true"] .tborder {
border-radius: var(--bt-radius-sm) !important;
}
:root[data-bt-mobile-lite="true"] #header img,
:root[data-bt-mobile-lite="true"] #logo,
:root[data-bt-mobile-lite="true"] #bbcBox_message img,
:root[data-bt-mobile-lite="true"] .bbc_buttons img {
filter: none !important;
}
/*-- BT_ALWAYS_ON_START --*/
#bt-switcher {
position: fixed !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, #1A1D21) !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, #2A2F35) !important;
color: var(--bt-text-2, #A8B0BA) !important;
border: 1px solid var(--bt-border-2, rgba(255,255,255,0.14)) !important;
cursor: pointer !important;
transition: background var(--bt-trans), color var(--bt-trans), transform 200ms ease !important;
box-shadow: var(--bt-shadow, 0 1px 0 rgba(0,0,0,0.2)) !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, #343A41) !important;
color: var(--bt-text, #E3E3E8) !important;
transform: rotate(20deg);
}
#bt-switcher-button svg {
width: 18px;
height: 18px;
pointer-events: none !important;
}
:root[data-bt-active="false"] #bt-switcher-button {
width: auto !important;
min-width: 44px !important;
padding: 0 12px !important;
border-radius: 14px !important;
background: #2A2F35 !important;
color: #FFFFFF !important;
border: 1px solid #4ADE80 !important;
font-weight: 700 !important;
font-size: 11px !important;
text-transform: uppercase !important;
letter-spacing: 0.08em !important;
box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.4) !important;
animation: bt-pulse-on 2.4s ease-in-out infinite;
}
:root[data-bt-active="false"] #bt-switcher-button:hover {
background: #4ADE80 !important;
color: #1A1D21 !important;
transform: none !important;
animation: none;
}
.bt-sw-on-text { display: inline-block; line-height: 1; }
@keyframes bt-pulse-on {
0%, 100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.0); }
50% { box-shadow: 0 0 0 4px rgba(74, 222, 128, 0.18); }
}
#bt-switcher-panel {
position: absolute !important;
top: 40px !important;
right: 0 !important;
min-width: 300px !important;
max-height: calc(100vh - 56px) !important;
max-height: calc(100dvh - 56px) !important;
overflow-y: auto !important;
overscroll-behavior: contain !important;
box-sizing: border-box !important;
background: var(--bt-surface-1, #22262B) !important;
border: 1px solid var(--bt-border-2, rgba(255,255,255,0.14)) !important;
border-radius: var(--bt-radius, 10px) !important;
padding: 14px !important;
box-shadow: var(--bt-shadow-lg, 0 12px 32px rgba(0,0,0,0.35)) !important;
display: block !important;
opacity: 0 !important;
pointer-events: none !important;
transform: translateY(-4px) !important;
visibility: hidden !important;
transition: opacity 120ms ease, transform 120ms ease, visibility 120ms ease !important;
color: var(--bt-text, #E3E3E8) !important;
}
#bt-switcher.open #bt-switcher-panel {
opacity: 1 !important;
pointer-events: auto !important;
transform: translateY(0) !important;
visibility: visible !important;
}
#bt-switcher-panel button {
touch-action: manipulation !important;
-webkit-tap-highlight-color: transparent !important;
}
#bt-switcher-panel input[type="color"] {
touch-action: auto !important;
-webkit-tap-highlight-color: transparent !important;
}
.bt-sw-section { margin-bottom: 14px; }
.bt-sw-section:last-child { margin-bottom: 0; }
.bt-sw-performance-section {
padding: 0 !important;
border: 1px solid var(--bt-border, rgba(255,255,255,0.07)) !important;
border-radius: var(--bt-radius-sm, 6px) !important;
background: var(--bt-surface-2, #2A2F35) !important;
min-height: 32px !important;
overflow: hidden !important;
}
.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, #6B7480) !important;
margin-bottom: 8px !important;
}
.bt-sw-version-badge {
display: inline-block !important;
margin-left: 6px !important;
padding: 2px 5px !important;
border-radius: 999px !important;
background: #EF4444 !important;
color: #FFFFFF !important;
font-size: 10px !important;
font-weight: 800 !important;
letter-spacing: 0 !important;
vertical-align: 1px !important;
}
.bt-sw-local-badge {
display: inline-block !important;
margin-left: 6px !important;
padding: 2px 6px !important;
border-radius: 999px !important;
background: #DC2626 !important;
color: #FFFFFF !important;
font-size: 10px !important;
font-weight: 800 !important;
letter-spacing: 0.06em !important;
vertical-align: 1px !important;
text-transform: uppercase !important;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3) inset !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, #2A2F35) !important;
color: var(--bt-text-2, #A8B0BA) !important;
border: 1px solid var(--bt-border, rgba(255,255,255,0.07)) !important;
border-radius: var(--bt-radius-sm, 6px) !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, #343A41) !important;
color: var(--bt-text, #E3E3E8) !important;
transform: none !important;
}
.bt-sw-theme-btn.selected {
background: var(--bt-accent, #F7931A) !important;
color: var(--bt-on-accent, #1A1D21) !important;
border-color: var(--bt-accent, #F7931A) !important;
}
.bt-sw-theme-btn svg { width: 14px; height: 14px; }
.bt-sw-theme-btn[data-theme="off"].selected {
background: var(--bt-danger, #EF4444) !important;
color: #FFFFFF !important;
border-color: var(--bt-danger, #EF4444) !important;
}
.bt-sw-toggle {
width: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
gap: 10px !important;
padding: 3px 8px !important;
margin: 0 !important;
background: transparent !important;
color: var(--bt-text-2, #A8B0BA) !important;
border: none !important;
border-radius: var(--bt-radius-sm, 6px) !important;
cursor: pointer !important;
font-family: var(--bt-font) !important;
font-size: 11px !important;
font-weight: 500 !important;
line-height: 1 !important;
text-shadow: none !important;
box-shadow: none !important;
transition: background var(--bt-trans), color var(--bt-trans), border-color var(--bt-trans) !important;
}
.bt-sw-toggle-spacer {
flex: 1 1 auto !important;
}
.bt-sw-toggle + .bt-sw-toggle {
border-top: 1px solid var(--bt-border, rgba(255,255,255,0.07)) !important;
border-radius: 0 !important;
}
.bt-sw-toggle .bt-sw-theme-label {
flex: 1 1 auto !important;
text-align: left !important;
font-size: 11px !important;
font-weight: 600 !important;
color: inherit !important;
white-space: nowrap !important;
}
.bt-sw-toggle:hover {
background: transparent !important;
color: var(--bt-text, #E3E3E8) !important;
transform: none !important;
}
/* v3.0.37: toggle ridisegnato — knob perfettamente centrato
Math (track 44×24, border-box):
track interno = 44×24 (con 1px border incluso)
knob 20×20 (border-box, niente bordo) posizionato a top:2 left:2
→ gap verticale: 2px sopra, 24-2-20 = 2px sotto ✓ centrato
→ gap orizzontale OFF: 2px sx, 22px dx
→ gap orizzontale ON: translateX(20px) → 22px sx, 2px dx */
.bt-sw-toggle-track {
width: 44px !important;
height: 24px !important;
flex: 0 0 44px !important;
position: relative !important;
box-sizing: border-box !important;
padding: 0 !important;
border-radius: 999px !important;
background: var(--bt-surface-2, #D8DDE5) !important;
border: 1px solid var(--bt-border-2, #99A0AB) !important;
transition: background 200ms cubic-bezier(0.4, 0, 0.2, 1),
border-color 200ms cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.bt-sw-toggle-knob {
position: absolute !important;
top: 2px !important;
left: 2px !important;
width: 20px !important;
height: 20px !important;
box-sizing: border-box !important;
border-radius: 50% !important;
background: #FFFFFF !important;
border: 0 !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.18),
0 1px 2px rgba(0,0,0,0.10) !important;
transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1),
background 200ms cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 200ms !important;
}
.bt-sw-toggle:hover .bt-sw-toggle-knob {
box-shadow: 0 3px 6px rgba(0,0,0,0.22),
0 1px 2px rgba(0,0,0,0.10) !important;
}
.bt-sw-toggle.selected {
color: var(--bt-text, #E3E3E8) !important;
}
.bt-sw-toggle.selected .bt-sw-toggle-track {
background: var(--bt-accent, #F7931A) !important;
border-color: var(--bt-accent, #F7931A) !important;
}
.bt-sw-toggle.selected .bt-sw-toggle-knob {
transform: translateX(20px) !important;
background: #FFFFFF !important;
}
.bt-sw-mobile-toggle.bt-sw-mobile-auto .bt-sw-toggle-track {
background: color-mix(in srgb, var(--bt-accent, #F7931A) 45%, var(--bt-surface-3, #343A41)) !important;
border-color: color-mix(in srgb, var(--bt-accent, #F7931A) 60%, transparent) !important;
}
.bt-sw-mobile-toggle.bt-sw-mobile-auto .bt-sw-toggle-knob {
transform: translateX(10px) !important;
}
.bt-sw-toggle:active .bt-sw-toggle-knob {
width: 22px !important; /* leggero squash al click per feel tattile */
}
.bt-sw-toggle.selected:active .bt-sw-toggle-knob {
transform: translateX(18px) !important;
}
.bt-sw-toggle-state {
display: none !important;
}
.bt-sw-toggle.selected .bt-sw-toggle-state {
color: var(--bt-accent, #F7931A) !important;
}
.bt-sw-customize {
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--bt-border, rgba(255,255,255,0.07));
}
.bt-sw-customize-title {
display: block;
text-align: center;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.05em;
color: var(--bt-text, #E3E3E8);
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, #E3E3E8) !important; }
.bt-sw-auto {
width: auto !important;
min-width: 44px !important;
padding: 0 8px !important;
height: 26px !important;
border-radius: 13px !important;
background: conic-gradient(#ef4444, #f59e0b, #10b981, #3b82f6, #8b5cf6, #ef4444) !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
color: #FFFFFF !important;
font-weight: 700 !important;
font-size: 9px !important;
letter-spacing: 0.06em !important;
line-height: 1 !important;
text-shadow: 0 1px 2px rgba(0,0,0,0.6) !important;
font-family: var(--bt-font) !important;
}
.bt-sw-auto::before { content: 'AUTO'; }
.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, #E3E3E8);
}
.bt-sw-info {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
gap: 8px !important;
margin-top: 14px !important;
padding-top: 12px !important;
border-top: 1px solid var(--bt-border, rgba(255,255,255,0.07)) !important;
color: var(--bt-text-3, #6B7480) !important;
font-size: 10px !important;
}
.bt-sw-credit {
color: var(--bt-text-2, #A8B0BA) !important;
letter-spacing: 0.02em !important;
}
.bt-sw-credit strong {
color: var(--bt-text, #E3E3E8) !important;
font-weight: 700 !important;
}
.bt-sw-info-meta {
width: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
gap: 8px !important;
flex-wrap: wrap !important;
}
.bt-sw-thread-link,
.bt-sw-thread-link:link,
.bt-sw-thread-link:visited {
display: inline-flex !important;
align-items: center !important;
gap: 3px !important;
color: var(--bt-accent, #F7931A) !important;
font-size: 10px !important;
font-weight: 600 !important;
text-decoration: none !important;
}
.bt-sw-thread-link:hover,
.bt-sw-thread-link:focus {
color: var(--bt-accent-h, #FFA733) !important;
text-decoration: underline !important;
}
.bt-sw-installed-version {
display: inline-flex !important;
align-items: center !important;
min-height: 18px !important;
padding: 2px 6px !important;
border: 1px solid var(--bt-border, rgba(255,255,255,0.07)) !important;
border-radius: 999px !important;
background: var(--bt-surface-2, #2A2F35) !important;
color: var(--bt-text-2, #A8B0BA) !important;
font-size: 9px !important;
font-weight: 600 !important;
letter-spacing: 0.03em !important;
white-space: nowrap !important;
}
:root[data-bt-mobile-lite="true"] #bt-switcher-button,
:root[data-bt-mobile-lite="true"] #bt-switcher-panel,
:root[data-bt-mobile-lite="true"] .bt-sw-theme-btn,
:root[data-bt-mobile-lite="true"] .bt-sw-toggle,
:root[data-bt-mobile-lite="true"] .bt-sw-toggle-track,
:root[data-bt-mobile-lite="true"] .bt-sw-toggle-knob,
:root[data-bt-mobile-lite="true"] .bt-sw-swatch,
:root[data-bt-mobile-lite="true"] .bt-sw-custom-display {
transition: none !important;
animation: none !important;
box-shadow: none !important;
}
:root[data-bt-mobile-lite="true"] #bt-switcher-button:hover,
:root[data-bt-mobile-lite="true"] .bt-sw-swatch:hover {
transform: none !important;
}
:root[data-bt-mobile-lite="true"] #bt-switcher-panel {
min-width: min(300px, calc(100vw - 24px)) !important;
max-width: calc(100vw - 24px) !important;
}
/*-- BT_ALWAYS_ON_END --*/
/* ============================================================
v2.8.3 — LIGHT THREAD POST ALTERNATION, FINAL OVERRIDE
Theme-aware version. The post colours come from the active light
palette, not from Classic hardcoded blue:
--bt-post-bg
--bt-post-alt-bg
--bt-poster-bg
--bt-poster-alt-bg
So Classic, Pearl, Cream, Mint and Rose each get their own matching
alternation. This block deliberately sits at the END of the theme CSS
so it wins over broad rules like .post, .poster_info and td[style].
============================================================ */
:root[data-bt-theme="light"] tr.windowbg,
:root[data-bt-theme="light"] td.windowbg,
:root[data-bt-theme="light"] tr.windowbg > td,
:root[data-bt-theme="light"] table.bordercolor tr.windowbg > td,
:root[data-bt-theme="light"] table.bordercolor td.windowbg,
:root[data-bt-theme="light"] .tborder tr.windowbg > td,
:root[data-bt-theme="light"] .tborder td.windowbg {
background-color: var(--bt-post-bg, var(--bt-surface-1)) !important;
background-image: none !important;
}
:root[data-bt-theme="light"] tr.windowbg2,
:root[data-bt-theme="light"] td.windowbg2,
:root[data-bt-theme="light"] tr.windowbg2 > td,
:root[data-bt-theme="light"] table.bordercolor tr.windowbg2 > td,
:root[data-bt-theme="light"] table.bordercolor td.windowbg2,
:root[data-bt-theme="light"] .tborder tr.windowbg2 > td,
:root[data-bt-theme="light"] .tborder td.windowbg2 {
background-color: var(--bt-post-alt-bg, var(--bt-surface-2)) !important;
background-image: none !important;
}
:root[data-bt-theme="light"] tr.windowbg3,
:root[data-bt-theme="light"] td.windowbg3,
:root[data-bt-theme="light"] tr.windowbg3 > td,
:root[data-bt-theme="light"] table.bordercolor tr.windowbg3 > td,
:root[data-bt-theme="light"] table.bordercolor td.windowbg3,
:root[data-bt-theme="light"] .tborder tr.windowbg3 > td,
:root[data-bt-theme="light"] .tborder td.windowbg3 {
background-color: var(--bt-post-bg, var(--bt-surface-1)) !important;
background-image: none !important;
}
/* Visible post body/wrapper. */
:root[data-bt-theme="light"] tr.windowbg .td_headerandpost,
:root[data-bt-theme="light"] td.windowbg.td_headerandpost,
:root[data-bt-theme="light"] td.windowbg .td_headerandpost,
:root[data-bt-theme="light"] tr.windowbg .post,
:root[data-bt-theme="light"] td.windowbg .post,
:root[data-bt-theme="light"] tr.windowbg td.post,
:root[data-bt-theme="light"] td.windowbg.post,
:root[data-bt-theme="light"] tr.windowbg td[style],
:root[data-bt-theme="light"] td.windowbg td[style] {
background-color: var(--bt-post-bg, var(--bt-surface-1)) !important;
background-image: none !important;
}
:root[data-bt-theme="light"] tr.windowbg2 .td_headerandpost,
:root[data-bt-theme="light"] td.windowbg2.td_headerandpost,
:root[data-bt-theme="light"] td.windowbg2 .td_headerandpost,
:root[data-bt-theme="light"] tr.windowbg2 .post,
:root[data-bt-theme="light"] td.windowbg2 .post,
:root[data-bt-theme="light"] tr.windowbg2 td.post,
:root[data-bt-theme="light"] td.windowbg2.post,
:root[data-bt-theme="light"] tr.windowbg2 td[style],
:root[data-bt-theme="light"] td.windowbg2 td[style] {
background-color: var(--bt-post-alt-bg, var(--bt-surface-2)) !important;
background-image: none !important;
}
:root[data-bt-theme="light"] tr.windowbg3 .td_headerandpost,
:root[data-bt-theme="light"] td.windowbg3.td_headerandpost,
:root[data-bt-theme="light"] td.windowbg3 .td_headerandpost,
:root[data-bt-theme="light"] tr.windowbg3 .post,
:root[data-bt-theme="light"] td.windowbg3 .post,
:root[data-bt-theme="light"] tr.windowbg3 td.post,
:root[data-bt-theme="light"] td.windowbg3.post,
:root[data-bt-theme="light"] tr.windowbg3 td[style],
:root[data-bt-theme="light"] td.windowbg3 td[style] {
background-color: var(--bt-post-bg, var(--bt-surface-1)) !important;
background-image: none !important;
}
/* Poster/sidebar column: same theme family, slightly deeper for separation. */
:root[data-bt-theme="light"] tr.windowbg .poster,
:root[data-bt-theme="light"] tr.windowbg .poster_info,
:root[data-bt-theme="light"] td.windowbg.poster,
:root[data-bt-theme="light"] td.windowbg.poster_info {
background-color: var(--bt-poster-bg, var(--bt-post-bg)) !important;
background-image: none !important;
}
:root[data-bt-theme="light"] tr.windowbg2 .poster,
:root[data-bt-theme="light"] tr.windowbg2 .poster_info,
:root[data-bt-theme="light"] td.windowbg2.poster,
:root[data-bt-theme="light"] td.windowbg2.poster_info {
background-color: var(--bt-poster-alt-bg, var(--bt-post-alt-bg)) !important;
background-image: none !important;
}
:root[data-bt-theme="light"] tr.windowbg3 .poster,
:root[data-bt-theme="light"] tr.windowbg3 .poster_info,
:root[data-bt-theme="light"] td.windowbg3.poster,
:root[data-bt-theme="light"] td.windowbg3.poster_info {
background-color: var(--bt-poster-bg, var(--bt-post-bg)) !important;
background-image: none !important;
}
/* Keep quote/code/input blocks distinct inside alternating posts. */
:root[data-bt-theme="light"] tr.windowbg .quote,
:root[data-bt-theme="light"] tr.windowbg2 .quote,
:root[data-bt-theme="light"] tr.windowbg3 .quote,
:root[data-bt-theme="light"] td.windowbg .quote,
:root[data-bt-theme="light"] td.windowbg2 .quote,
:root[data-bt-theme="light"] td.windowbg3 .quote {
background-color: var(--bt-quote-bg) !important;
border-left-color: var(--bt-accent) !important;
}
:root[data-bt-theme="light"] tr.windowbg .code,
:root[data-bt-theme="light"] tr.windowbg2 .code,
:root[data-bt-theme="light"] tr.windowbg3 .code,
:root[data-bt-theme="light"] td.windowbg .code,
:root[data-bt-theme="light"] td.windowbg2 .code,
:root[data-bt-theme="light"] td.windowbg3 .code {
background-color: var(--bt-code-bg) !important;
}
:root[data-bt-theme="light"] tr.windowbg input,
:root[data-bt-theme="light"] tr.windowbg2 input,
:root[data-bt-theme="light"] tr.windowbg3 input,
:root[data-bt-theme="light"] tr.windowbg textarea,
:root[data-bt-theme="light"] tr.windowbg2 textarea,
:root[data-bt-theme="light"] tr.windowbg3 textarea,
:root[data-bt-theme="light"] tr.windowbg select,
:root[data-bt-theme="light"] tr.windowbg2 select,
:root[data-bt-theme="light"] tr.windowbg3 select {
background-color: var(--bt-input-bg) !important;
}
/* ============================================================
v2.10.7 — TYPOGRAPHY NORMALISATION
Goal: keep the modern theme visually close to classic Bitcointalk text
sizing, while removing the internal inconsistency introduced by many
scattered 10/11/12px overrides. OFF mode remains clean because this CSS
block is part of the disabled theme stylesheet, not an always-on rule.
============================================================ */
:root {
/* v3.0.3 typography pass against measured classic/OFF Bitcointalk:
- base/post 13px match classic table and body text
- small 11px matches classic smalltext/meta blocks
- micro 10px preserves compact auxiliary text
- compose 14px keeps the message editor aligned with forum body text
- pagelinks stay compact as per-topic micro-navigation */
--bt-fs-base: 13px;
--bt-fs-small: 11px;
--bt-fs-micro: 10px;
--bt-fs-post: 13px;
--bt-fs-pagelinks: 10px;
--bt-fs-compose: 14px;
--bt-lh-base: normal;
--bt-lh-post: 1.55;
}
html,
body {
font-size: var(--bt-fs-base) !important;
line-height: var(--bt-lh-base) !important;
}
body,
table,
td,
th,
tr,
dd,
dt,
input,
select,
textarea,
button:not([id^="bt-"]):not([class^="bt-sw-"]) {
font-size: var(--bt-fs-base) !important;
}
table.table_list td,
table.table_grid td,
table.bordercolor td,
.tborder td,
[class*="windowbg"],
[class*="windowbg"] td,
.windowbg,
.windowbg2,
.windowbg3,
.catbg,
.catbg2,
.titlebg,
.titlebg2,
[class*="catbg"],
[class*="titlebg"] {
font-size: var(--bt-fs-base) !important;
line-height: var(--bt-lh-base) !important;
}
.post,
td.post,
.personalmessage,
td.personalmessage,
#preview_body,
#preview_section,
#preview_section td {
font-size: var(--bt-fs-post) !important;
line-height: var(--bt-lh-post) !important;
}
.post font[size="1"],
td.post font[size="1"],
.personalmessage font[size="1"],
td.personalmessage font[size="1"],
#preview_body font[size="1"],
#preview_section font[size="1"],
.post font[size="1"] *,
td.post font[size="1"] *,
.personalmessage font[size="1"] *,
td.personalmessage font[size="1"] *,
#preview_body font[size="1"] *,
#preview_section font[size="1"] * {
font-size: var(--bt-fs-small) !important;
line-height: normal !important;
}
/* Classic Bitcointalk uses many small meta labels, but they should not
collapse to tiny 10/11px text in the modern layer.
v2.10.26: removed .nav / a.nav from this list — those are breadcrumb
links (Bitcoin Forum > Economy > Economics > ...) and should render at
body size, NOT at small-meta size. Same for .navPages. */
.smalltext,
.middletext,
.small_header,
.prevnext,
.lastpostcol,
.ignored_topic,
.utusers,
.utusers a,
.poster ul,
.poster li,
.poster_info,
td.poster_info,
.poster,
td.poster,
.signature,
.quoteheader,
.codeheader {
font-size: var(--bt-fs-small) !important;
line-height: normal !important;
}
.codeheader {
font-size: 10px !important;
line-height: 12px !important;
}
table td.smalltext,
table td.middletext,
table.bordercolor td.smalltext,
table.bordercolor td.middletext,
table.tborder td.smalltext,
table.tborder td.middletext,
.tborder td.smalltext,
.tborder td.middletext,
[class*="windowbg"] td.smalltext,
[class*="windowbg"] td.middletext,
td[class*="windowbg"] td.smalltext,
td[class*="windowbg"] td.middletext {
font-size: var(--bt-fs-small) !important;
line-height: normal !important;
}
/* Breadcrumb (Bitcoin Forum > Economy > Economics > ...) at 14px to
keep page navigation legible above a thread. The earlier rule was placed
BEFORE the smalltext/middletext block above and lost the source-order
battle since SMF puts the breadcrumb inside <td class="middletext">.
Now declared after, with higher specificity (.navigate_section.navigate_section
and the body-prefixed form for direct .nav containers) so it wins
both by specificity AND by source order. */
/* v2.10.35: SMF Bitcointalk injects an inline style="font-size: smaller"
directly on the breadcrumb container <div class="nav">. That inline
style cascades to every descendant via inheritance. CSS rules without
!important can never beat an inline style by specificity, but a CSS
rule WITH !important does win. The fix: !important on the .nav
container itself plus a universal descendant selector so every <a>,
<b>, text-wrapping element ends up at body size, including the
<b><a>...</a></b> structure SMF actually uses for breadcrumb links
and the bare " > " text nodes that float as direct children. */
body div.nav.nav,
body div.nav.nav *,
body .navigate_section.navigate_section,
body .navigate_section.navigate_section *,
body td.middletext .nav,
body td.middletext .nav * {
font-size: 14px !important;
line-height: 1.4 !important;
}
/* Static replacements for the former text-scanning news-header pass. */
#upshrink_news,
#upshrink_news *,
#news,
#news *,
.news,
.news *,
.newstitle,
.newstitle * {
font-size: var(--bt-fs-small) !important;
line-height: normal !important;
}
/* Topic rows and board indexes: make titles and row text match the classic
density instead of looking smaller than the surrounding chrome. */
table.table_list a[href*="topic="],
table.bordercolor a[href*="topic="],
.tborder a[href*="topic="],
[class*="windowbg"] a[href*="topic="],
span[id^="msg_"] a,
.subject,
.topic_subject {
font-size: var(--bt-fs-base) !important;
line-height: var(--bt-lh-base) !important;
}
table.table_list .pagelinks a[href*="topic="],
table.bordercolor .pagelinks a[href*="topic="],
.tborder .pagelinks a[href*="topic="],
[class*="windowbg"] .pagelinks a[href*="topic="] {
font-size: var(--bt-fs-pagelinks) !important;
line-height: 1.15 !important;
}
/* v2.10.25: per-topic mini pagelinks sit inside <span class="smalltext">
(or "middletext"), NOT inside .pagelinks — SMF renders them as raw
<a> elements. The earlier rule [class*="windowbg"] a[href*="topic="]
was hitting them with --bt-fs-base (13px) before our pagelink rule
had a chance to match.
v2.10.25 narrows the match to ONLY the smalltext blocks that actually
contain topic/board page-jump links, using :has(). The previous v2.10.23
form was too broad — it caught every smalltext inside any windowbg cell,
including "Show unread posts since last visit", "Watchlist", "Total time
logged in:", etc. which live in user-info windowbg cells. */
:root[data-bt-active="true"][data-bt-theme] body td[class*="windowbg"] span.smalltext:has(> a[href*="topic="]),
:root[data-bt-active="true"][data-bt-theme] body td[class*="windowbg"] span.middletext:has(> a[href*="topic="]),
:root[data-bt-active="true"][data-bt-theme] body td[class*="windowbg"] span.smalltext:has(> a[href*="topic="]) > a,
:root[data-bt-active="true"][data-bt-theme] body td[class*="windowbg"] span.middletext:has(> a[href*="topic="]) > a {
font-size: var(--bt-fs-pagelinks) !important;
line-height: 1.2 !important;
}
:root[data-bt-active="true"] #smfheader td.titlebg2[align="right"],
:root[data-bt-active="true"] #smfheader td.titlebg2[align="right"] span.smalltext,
:root[data-bt-active="true"] #smfheader td.titlebg2[align="right"] span.smalltext * {
font-family: Verdana, Arial, Helvetica, sans-serif !important;
font-size: 13px !important;
font-weight: 400 !important;
line-height: normal !important;
letter-spacing: 0 !important;
}
[class*="catbg"] a,
[class*="titlebg"] a,
th a,
td[class*="catbg"] a,
td[class*="titlebg"] a {
font-size: var(--bt-fs-base) !important;
line-height: var(--bt-lh-base) !important;
}
/* v3.0.19: Merit summary needs to follow the classic skin, where the
document-like content is visibly larger than the modern table density.
This is deliberately late in the typography block so it wins over the
global 13px table/body normalisation above. */
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea {
font-family: Verdana, Arial, Helvetica, sans-serif !important;
font-size: 13px !important;
line-height: 1.32 !important;
}
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea p,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea div,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea span,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea table,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea tbody,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea tr,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea td,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea th,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea ul,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea ol,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea li,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea b,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea strong,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea a {
font-family: Verdana, Arial, Helvetica, sans-serif !important;
font-size: 13px !important;
line-height: 1.32 !important;
}
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea h1,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea h1 * {
font-family: Verdana, Arial, Helvetica, sans-serif !important;
font-size: 23px !important;
line-height: 1.24 !important;
letter-spacing: 0 !important;
}
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea h2,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea h2 *,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea h3,
:root[data-bt-active="true"][data-bt-page="merit"] #bodyarea h3 * {
font-family: Verdana, Arial, Helvetica, sans-serif !important;
font-size: 17px !important;
line-height: 1.28 !important;
letter-spacing: 0 !important;
}
/* v3.0: normal form controls use body-sized text; the post-composition
message area gets its own larger writing size below. */
textarea,
textarea[name="message"],
#message,
#quickReply textarea,
#postmodify textarea,
#post_modify textarea,
input[type="text"],
input[type="password"],
input[type="email"],
input[type="search"],
input[type="url"],
input[type="number"],
select {
font-family: var(--bt-font) !important;
font-size: 14px !important;
line-height: 1.4 !important;
}
textarea[name="message"],
#message,
#quickReply textarea,
#postmodify textarea[name="message"],
#post_modify textarea[name="message"] {
font-size: var(--bt-fs-compose) !important;
line-height: 1.45 !important;
}
textarea.editor[name="message"],
#postmodify textarea.editor[name="message"],
#post_modify textarea.editor[name="message"],
form[name="postmodify"] textarea.editor[name="message"],
form[action*="action=post"] textarea.editor[name="message"] {
font-size: var(--bt-fs-compose) !important;
line-height: 1.45 !important;
}
input[type="submit"],
/* v2.10.28: removed topic-action buttons (replybutton/notifybutton/etc.)
and .bt-post-action from this universal button rule. They get a more
compact treatment in their own rule below — this rule kept overriding
the compact sizing because it appears later in the cascade and
therefore won at equal specificity. */
input[type="button"],
input[type="reset"],
button:not([id^="bt-"]):not([class^="bt-sw-"]),
a.button,
.button_submit,
.button_reset,
.button,
a[role="button"],
span[role="button"],
.poster_info button,
.poster_info input[type="button"],
.poster_info input[type="submit"],
.poster_info a[class*="button"],
.poster_info span[class*="button"],
.poster_info a[class*="Button"],
.poster_info span[class*="Button"] {
font-size: var(--bt-fs-small) !important;
line-height: 1.2 !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
padding-left: 8px !important;
padding-right: 8px !important;
min-height: 0 !important;
}
/* v2.10.28: compact topic action bar (Reply | Unwatch | Notify | ...).
Declared after the universal button rule above so it wins by source
order at equal specificity, and uses high-specificity selectors anyway
(a.replybutton.replybutton — class repetition trick — to outweigh any
theme rule that targets just the single class). */
a.quotebutton.quotebutton,
a.replybutton.replybutton,
a.modifybutton.modifybutton,
a.removebutton.removebutton,
a.reportbutton.reportbutton,
a.splitbutton.splitbutton,
a.notifybutton.notifybutton,
a.markreadbutton.markreadbutton,
a.unwatchbutton.unwatchbutton,
a.printbutton.printbutton,
a.archivebutton.archivebutton,
.bt-post-action.bt-post-action {
font-size: 11px !important;
line-height: 1.3 !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
padding-left: 6px !important;
padding-right: 6px !important;
margin: 0 2px 0 0 !important;
letter-spacing: 0.02em !important;
min-height: 0 !important;
height: auto !important;
}
input[type="submit"],
.button_submit {
padding-top: 5px !important;
padding-bottom: 5px !important;
padding-left: 10px !important;
padding-right: 10px !important;
font-size: var(--bt-fs-small) !important;
line-height: 1.2 !important;
}
[class*="maintab_"] a,
.dropmenu a,
#main_menu a {
font-size: 13px !important;
line-height: 1.15 !important;
}
.quote,
.code {
font-size: var(--bt-fs-small) !important;
line-height: 1.3 !important;
}
.code {
font-size: 12px !important;
line-height: 1.3 !important;
}
.bt-topic-icon--newPost {
font-size: 10px !important;
line-height: 1 !important;
min-width: 30px !important;
height: 15px !important;
}
/* Compose toolbar icon buttons remain compact, but do not shrink text in
the editor or in the rest of the compose page. */
#bbcBox_message .bt-bbc-icon,
.bbc_buttons .bt-bbc-icon,
[id^="bbc_"] .bt-bbc-icon {
width: 22px !important;
height: 22px !important;
padding: 3px !important;
}
#bbcBox_message img,
.bbc_buttons img,
[id^="bbc_"] img {
max-width: 22px !important;
max-height: 22px !important;
}
#smileyBox_message .bt-smiley,
#smileyBox_message img,
[id^="smileyBox"] .bt-smiley,
[id^="smileyBox"] img,
#postmodify .bt-smiley,
#postmodify img.smiley,
#post_modify .bt-smiley,
#post_modify img.smiley,
form[action*="post"] .bt-smiley,
form[action*="post"] img.smiley {
font-size: var(--bt-fs-base) !important;
width: 14px !important;
height: 14px !important;
max-width: 14px !important;
max-height: 14px !important;
line-height: 1 !important;
vertical-align: -0.12em !important;
}
#smileyBox_message .bt-smiley,
[id^="smileyBox"] .bt-smiley,
#postmodify .bt-smiley,
#post_modify .bt-smiley,
form[action*="post"] .bt-smiley {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 18px !important;
height: 18px !important;
color: var(--bt-text2) !important;
}
#smileyBox_message .bt-smiley-modern svg,
[id^="smileyBox"] .bt-smiley-modern svg,
#postmodify .bt-smiley-modern svg,
#post_modify .bt-smiley-modern svg,
form[action*="post"] .bt-smiley-modern svg {
width: 18px !important;
height: 18px !important;
display: block !important;
}
#smileyBox_message .bt-smiley-native,
[id^="smileyBox"] .bt-smiley-native,
#postmodify .bt-smiley-native,
#post_modify .bt-smiley-native,
form[action*="post"] .bt-smiley-native {
font-family: Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif !important;
font-size: 16px !important;
line-height: 18px !important;
color: initial !important;
text-rendering: optimizeLegibility !important;
}
.bt-smiley-face {
fill: rgba(247, 147, 26, 0.16);
stroke: currentColor;
stroke-width: 1.7;
}
.bt-smiley-eye,
.bt-smiley-fill {
fill: currentColor;
}
.bt-smiley-line {
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.bt-smiley-tongue {
fill: rgba(247, 147, 26, 0.22);
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.bt-smiley-heart,
.bt-smiley-star,
.bt-smiley-fire {
fill: currentColor;
}
/* The theme switcher is intentionally separate UI: keep its compact scale
and avoid forum-wide typography overrides leaking into it. */
#bt-switcher,
#bt-switcher * {
line-height: 1 !important;
}
/* =========================================================================
v3.0.34 — DEFENSIVE FINAL OVERRIDES (must stay at end of stylesheet)
Force 13px on the SMF header date (top-right) and the main navigation
menu links. Specificity is bumped with html body and the [data-bt-active]
root attribute so these rules beat any earlier --bt-fs-small reset.
Source-order placement at the end of the CSS string guarantees they win
over rules with equal specificity. */
html body #smfheader td.titlebg2[align="right"],
html body #smfheader td.titlebg2[align="right"] *,
html body #smfheader td.titlebg2[align="right"] span.smalltext,
html body #smfheader td.titlebg2[align="right"] span.smalltext * {
font-size: 13px !important;
line-height: normal !important;
}
html body [class*="maintab_"],
html body [class*="maintab_"] a,
html body [class*="maintab_"] a:link,
html body [class*="maintab_"] a:visited,
html body [class*="maintab_"] a:hover,
html body [class*="maintab_"] a:active,
html body .dropmenu,
html body .dropmenu a,
html body #main_menu,
html body #main_menu a {
font-size: 13px !important;
}
/* v3.2.3/v3.2.4: final topic-action-bar override. Keep this after all generic
navigation typography so the main nav can stay roomy while Reply /
Unwatch / Notify / Add poll / Print remain compact on one line.
v3.2.4 marks both mirrortab and maintab action tables with
.bt-topic-action-bar, because Bitcointalk uses maintab at the bottom
of some topic pages. */
html body table.bt-topic-action-bar {
display: inline-table !important;
width: auto !important;
max-width: 100% !important;
border-collapse: separate !important;
border-spacing: 0 !important;
white-space: nowrap !important;
}
html body table.bt-topic-action-bar tbody {
display: inline !important;
white-space: nowrap !important;
}
html body table.bt-topic-action-bar tr {
display: inline !important;
white-space: nowrap !important;
}
html body table.bt-topic-action-bar .maintab_first,
html body table.bt-topic-action-bar .maintab_last,
html body table.bt-topic-action-bar .maintab_active_first,
html body table.bt-topic-action-bar .maintab_active_last,
html body table.bt-topic-action-bar .mirrortab_first,
html body table.bt-topic-action-bar .mirrortab_last,
html body table.bt-topic-action-bar .mirrortab_active_first,
html body table.bt-topic-action-bar .mirrortab_active_last {
display: none !important;
}
html body table.bt-topic-action-bar [class*="maintab_"],
html body table.bt-topic-action-bar [class*="mirrortab_"] {
display: inline-block !important;
white-space: nowrap !important;
padding-top: 1px !important;
padding-bottom: 1px !important;
border-left: 1px solid var(--bt-border) !important;
border-radius: 0 !important;
}
html body table.bt-topic-action-bar [class*="maintab_"]:first-child,
html body table.bt-topic-action-bar [class*="mirrortab_"]:first-child {
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;
}
html body table.bt-topic-action-bar [class*="maintab_"]:last-child,
html body table.bt-topic-action-bar [class*="mirrortab_"]:last-child {
border-top-right-radius: var(--bt-radius-sm) !important;
border-bottom-right-radius: var(--bt-radius-sm) !important;
}
html body table.bt-topic-action-bar [class*="maintab_"] a,
html body table.bt-topic-action-bar [class*="maintab_"] a:link,
html body table.bt-topic-action-bar [class*="maintab_"] a:visited,
html body table.bt-topic-action-bar [class*="maintab_"] a:hover,
html body table.bt-topic-action-bar [class*="maintab_"] a:active,
html body table.bt-topic-action-bar [class*="mirrortab_"] a,
html body table.bt-topic-action-bar [class*="mirrortab_"] a:link,
html body table.bt-topic-action-bar [class*="mirrortab_"] a:visited,
html body table.bt-topic-action-bar [class*="mirrortab_"] a:hover,
html body table.bt-topic-action-bar [class*="mirrortab_"] a:active {
display: inline-block !important;
padding: 2px 5px !important;
font-size: 11px !important;
line-height: 1.15 !important;
letter-spacing: 0 !important;
white-space: nowrap !important;
}
html body [class*="mirrortab_"] {
display: inline-block !important;
white-space: nowrap !important;
padding-top: 1px !important;
padding-bottom: 1px !important;
border-left: 1px solid var(--bt-border) !important;
border-radius: var(--bt-radius-sm) !important;
}
html body .mirrortab_first,
html body .mirrortab_last,
html body .mirrortab_active_first,
html body .mirrortab_active_last {
display: none !important;
}
html body [class*="mirrortab_"] a,
html body [class*="mirrortab_"] a:link,
html body [class*="mirrortab_"] a:visited,
html body [class*="mirrortab_"] a:hover,
html body [class*="mirrortab_"] a:active {
display: inline-block !important;
padding: 2px 5px !important;
font-size: 11px !important;
line-height: 1.15 !important;
letter-spacing: 0 !important;
white-space: nowrap !important;
}
/* =========================================================================
v3.0.38 — TYPOGRAPHIC HARMONY FINAL PASS
Several SMF pages inject large inherited sizes into headings, title bars
and post wrappers. Keep the modern theme close to the classic scale:
normal content/post text at 13px, compact meta at 11px, compose at 14px.
Merit has its own page-specific block above, so it is excluded here. */
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #bodyarea,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #content_section,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #main_content_section,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body table.bordercolor td,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .tborder td,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body td.windowbg,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body td.windowbg2,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body td.windowbg3,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .post,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body td.post,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .personalmessage,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body td.personalmessage,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #preview_body,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #preview_section,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #preview_section td {
font-size: 13px !important;
}
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="catbg"],
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="titlebg"],
html[data-bt-active="true"]:not([data-bt-page="merit"]) body td[class*="catbg"],
html[data-bt-active="true"]:not([data-bt-page="merit"]) body td[class*="titlebg"],
html[data-bt-active="true"]:not([data-bt-page="merit"]) body th[class*="catbg"],
html[data-bt-active="true"]:not([data-bt-page="merit"]) body th[class*="titlebg"],
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="catbg"] b,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="titlebg"] b,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="catbg"] strong,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="titlebg"] strong,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="catbg"] span:not(.smalltext),
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="titlebg"] span:not(.smalltext),
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="catbg"] a,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body [class*="titlebg"] a {
font-size: 13px !important;
line-height: 1.35 !important;
letter-spacing: 0 !important;
}
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #bodyarea h1,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #content_section h1,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #main_content_section h1 {
font-size: 18px !important;
line-height: 1.25 !important;
font-weight: 600 !important;
letter-spacing: 0 !important;
}
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #bodyarea h2,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #bodyarea h3,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #content_section h2,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #content_section h3,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #main_content_section h2,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body #main_content_section h3,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .poster h4,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .poster h4 a,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .poster_info h4,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .poster_info h4 a {
font-size: 15px !important;
line-height: 1.3 !important;
font-weight: 600 !important;
letter-spacing: 0 !important;
}
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .smalltext,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .middletext,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .small_header,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .prevnext,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .lastpostcol,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .ignored_topic,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .utusers,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .poster ul,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .poster li,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .poster_info,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body td.poster_info,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .signature,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .quoteheader,
html[data-bt-active="true"]:not([data-bt-page="merit"]) body .codeheader {
font-size: 11px !important;
line-height: normal !important;
}
`;
// ===========================================================================
// CSS injection
// ===========================================================================
function splitCss(src) {
const startMarker = '/*-- BT_ALWAYS_ON_START --*/';
const endMarker = '/*-- BT_ALWAYS_ON_END --*/';
let theme = '';
let always = '';
let i = 0;
while (i < src.length) {
const startIdx = src.indexOf(startMarker, i);
if (startIdx === -1) {
theme += src.slice(i);
break;
}
theme += src.slice(i, startIdx);
const after = startIdx + startMarker.length;
const endIdx = src.indexOf(endMarker, after);
if (endIdx === -1) {
always += src.slice(after);
break;
}
always += src.slice(after, endIdx);
i = endIdx + endMarker.length;
}
return { theme, always };
}
let themeStyleEl = null;
let alwaysStyleEl = null;
let fontIdleHandle = null;
let fontFrameHandle = null;
function cancelPendingFontLoad() {
if (fontIdleHandle !== null && typeof cancelIdleCallback === 'function') {
cancelIdleCallback(fontIdleHandle);
}
if (fontFrameHandle !== null) cancelAnimationFrame(fontFrameHandle);
fontIdleHandle = null;
fontFrameHandle = null;
}
function appendAsyncFontStylesheet() {
fontIdleHandle = null;
fontFrameHandle = null;
if (!isThemeActiveNow() || isMobilePerformanceMode() || document.getElementById('bt-fonts')) return;
const link = document.createElement('link');
link.id = 'bt-fonts';
link.rel = 'stylesheet';
link.media = 'print';
link.dataset.btAsync = '1';
link.href =
'https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;500;600;700' +
'&family=JetBrains+Mono:wght@400;500&display=swap';
link.addEventListener('load', () => {
if (isThemeActiveNow() && !isMobilePerformanceMode()) link.media = 'all';
else link.remove();
}, { once: true });
(document.head || document.documentElement).appendChild(link);
}
function ensureFonts() {
// Optional fonts are deferred until the browser is idle and start with a
// non-matching media query, so they never block the initial render. OFF
// and both performance modes cancel work queued while Full was active.
if (!isThemeActiveNow() || isMobilePerformanceMode()) {
cancelPendingFontLoad();
return;
}
if (document.getElementById('bt-fonts') || fontIdleHandle !== null || fontFrameHandle !== null) return;
if (typeof requestIdleCallback === 'function') {
fontIdleHandle = requestIdleCallback(appendAsyncFontStylesheet, { timeout: 1200 });
} else {
fontFrameHandle = requestAnimationFrame(appendAsyncFontStylesheet);
}
}
function injectCSS() {
const { theme, always } = splitCss(css);
themeStyleEl = document.createElement('style');
themeStyleEl.id = 'bt-theme-css';
themeStyleEl.textContent = theme;
(document.head || document.documentElement).appendChild(themeStyleEl);
alwaysStyleEl = document.createElement('style');
alwaysStyleEl.id = 'bt-always-css';
alwaysStyleEl.textContent = always;
(document.head || document.documentElement).appendChild(alwaysStyleEl);
if (isThemeActiveNow()) ensureFonts();
}
// ===========================================================================
// 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>',
power: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/></svg>',
};
// True for the "Locale" dev build (its @name ends with " Locale"), so the
// settings panel can show red version/LOCAL badges to tell it apart from the
// published build at a glance.
const BT_IS_LOCAL = !!(
typeof GM_info !== 'undefined' &&
GM_info.script &&
/ Locale\b/i.test(GM_info.script.name || '')
);
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" aria-controls="bt-switcher-panel" aria-expanded="false">
${ICONS.cog}
</button>
<div id="bt-switcher-panel" role="dialog" aria-label="Theme settings" aria-hidden="true">
<div class="bt-sw-section">
<span class="bt-sw-label">Mode${BT_IS_LOCAL ? ` <span class="bt-sw-version-badge">v${SCRIPT_VERSION}</span> <span class="bt-sw-local-badge">LOCAL</span>` : ''}</span>
<div class="bt-sw-row">
<button type="button" class="bt-sw-theme-btn" data-theme="light" title="Light">
${ICONS.sun}<span class="bt-sw-theme-label">Light</span>
</button>
<button type="button" class="bt-sw-theme-btn" data-theme="dark" title="Dark">
${ICONS.moon}<span class="bt-sw-theme-label">Dark</span>
</button>
<button type="button" class="bt-sw-theme-btn" data-theme="system" title="Follow system">
${ICONS.monitor}<span class="bt-sw-theme-label">Auto</span>
</button>
<button type="button" class="bt-sw-theme-btn" data-theme="off" title="Turn the theme off">
${ICONS.power}<span class="bt-sw-theme-label">OFF</span>
</button>
</div>
</div>
<div class="bt-sw-section bt-sw-performance-section">
<button type="button" class="bt-sw-toggle bt-sw-mobile-toggle" id="bt-sw-mobile-version" title="Use the lightweight mobile version" aria-pressed="false">
<span class="bt-sw-theme-label">Mobile version</span>
<span class="bt-sw-toggle-spacer"></span>
<span class="bt-sw-toggle-track" aria-hidden="true"><span class="bt-sw-toggle-knob"></span></span>
<span class="bt-sw-toggle-state">OFF</span>
</button>
<button type="button" class="bt-sw-toggle bt-sw-read-toggle" id="bt-sw-read-shading" title="Shade posts you have already read" aria-pressed="true">
<span class="bt-sw-theme-label">Read shading</span>
<span class="bt-sw-toggle-spacer"></span>
<span class="bt-sw-toggle-track" aria-hidden="true"><span class="bt-sw-toggle-knob"></span></span>
<span class="bt-sw-toggle-state">ON</span>
</button>
</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 class="bt-sw-info" aria-label="Theme information">
<span class="bt-sw-credit">Developed with love by <strong>fillippone</strong></span>
<div class="bt-sw-info-meta">
<a class="bt-sw-thread-link" href="https://bitcointalk.org/index.php?topic=5581374.0" target="_blank" rel="noopener noreferrer">Bitcointalk thread <span aria-hidden="true">↗</span></a>
<span class="bt-sw-installed-version">Installed v${SCRIPT_VERSION}${BT_IS_LOCAL ? ' | LOCAL' : ''}</span>
</div>
</div>
</div>
`;
host.appendChild(container);
const btn = container.querySelector('#bt-switcher-button');
const panel = container.querySelector('#bt-switcher-panel');
let activeCustomColorPreview = null;
let colorPreviewFocusTimer = null;
let colorPickerWindowBlurred = false;
const clearColorPreviewState = () => {
activeCustomColorPreview = null;
colorPickerWindowBlurred = false;
if (colorPreviewFocusTimer) clearTimeout(colorPreviewFocusTimer);
colorPreviewFocusTimer = null;
};
const rollbackCustomColorPreview = () => {
if (!activeCustomColorPreview) return;
const hadPreview = activeCustomColorPreview.previewed;
clearColorPreviewState();
// The live preview never writes preferences, so reapplying the stored
// theme is a complete rollback, including Auto accent derivation and UI.
if (hadPreview) applyTheme();
};
const beginCustomColorSession = (input) => {
if (!input || !input.matches('input[type="color"][data-target]')) return;
if (activeCustomColorPreview && activeCustomColorPreview.input === input) return;
activeCustomColorPreview = {
kind: input.dataset.target,
input,
previewed: false,
};
colorPickerWindowBlurred = false;
};
const setSwitcherOpen = (open, restoreFocus = false) => {
container.classList.toggle('open', open);
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
panel.setAttribute('aria-hidden', open ? 'false' : 'true');
if (!open && restoreFocus) btn.focus();
};
const activateSwitcher = (e) => {
if (e && typeof e.button === 'number' && e.button !== 0) return;
e.stopPropagation();
const currentPref = getPref(KEY_THEME, DEFAULT_THEME);
if (currentPref === 'off') {
const previousMode = getEffectiveMobileMode();
const prior = getPref(KEY_THEME_PRIOR, 'dark');
setPref(KEY_THEME, prior);
applyTheme();
reconcileMobileRuntime(previousMode, getEffectiveMobileMode(), true);
return;
}
setSwitcherOpen(!container.classList.contains('open'));
};
// Resolve pointer actions on release, after checking that the pointer did
// not turn into a swipe. This preserves immediate one-tap controls without
// firing a button while the user scrolls the internally scrollable panel.
const MAX_PRESS_MOVE = 8;
const installSafePointerActivation = (surface, resolveControl, activate) => {
let pending = null;
let suppressedControl = null;
let suppressionTimer = null;
const clearPending = () => { pending = null; };
surface.addEventListener('pointerdown', (e) => {
if (typeof e.button === 'number' && e.button !== 0) return;
if (e.isPrimary === false) return;
const control = resolveControl(e);
if (!control || control.disabled) return;
pending = {
control,
pointerId: e.pointerId,
startX: e.clientX,
startY: e.clientY,
moved: false,
};
});
surface.addEventListener('pointermove', (e) => {
if (!pending || e.pointerId !== pending.pointerId) return;
if (Math.hypot(e.clientX - pending.startX, e.clientY - pending.startY) > MAX_PRESS_MOVE) {
pending.moved = true;
}
});
surface.addEventListener('pointercancel', clearPending);
surface.addEventListener('pointerup', (e) => {
if (!pending || e.pointerId !== pending.pointerId) return;
const press = pending;
clearPending();
if (resolveControl(e) !== press.control || !press.control.isConnected) return;
e.preventDefault();
e.stopPropagation();
suppressedControl = press.control;
if (suppressionTimer) clearTimeout(suppressionTimer);
suppressionTimer = setTimeout(() => {
suppressedControl = null;
suppressionTimer = null;
}, 1000);
// A mouse drag can still emit a compatible click when it is released
// over its starting button. Suppress that click without activating.
if (press.moved) return;
activate(press.control, e);
});
surface.addEventListener('click', (e) => {
// Keyboard and assistive-technology clicks have detail 0 and continue
// through the normal click handlers.
if (!suppressedControl || e.detail === 0) return;
const control = resolveControl(e);
if (control !== suppressedControl) return;
e.preventDefault();
e.stopImmediatePropagation();
suppressedControl = null;
if (suppressionTimer) clearTimeout(suppressionTimer);
suppressionTimer = null;
}, true);
};
installSafePointerActivation(
btn,
(e) => (e.target === btn || btn.contains(e.target) ? btn : null),
(_control, e) => activateSwitcher(e)
);
btn.addEventListener('click', (e) => {
activateSwitcher(e);
});
installSafePointerActivation(
panel,
(e) => {
const control = e.target.closest && e.target.closest('button');
return control && panel.contains(control) ? control : null;
},
control => control.click()
);
document.addEventListener('click', (e) => {
if (!container.contains(e.target)) {
rollbackCustomColorPreview();
setSwitcherOpen(false);
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && container.classList.contains('open')) {
rollbackCustomColorPreview();
setSwitcherOpen(false, true);
}
});
// A native colour picker does not have a uniform close signal across
// browsers. Cover its explicit cancel event, focus changes, Escape, and
// the common window blur/focus cycle used by operating-system pickers.
panel.addEventListener('pointerdown', (e) => beginCustomColorSession(e.target), true);
panel.addEventListener('click', (e) => beginCustomColorSession(e.target), true);
panel.addEventListener('cancel', (e) => {
if (activeCustomColorPreview && e.target === activeCustomColorPreview.input) {
rollbackCustomColorPreview();
}
}, true);
panel.addEventListener('focusout', (e) => {
if (!activeCustomColorPreview || e.target !== activeCustomColorPreview.input) return;
if (colorPreviewFocusTimer) clearTimeout(colorPreviewFocusTimer);
colorPreviewFocusTimer = setTimeout(() => {
colorPreviewFocusTimer = null;
if (
activeCustomColorPreview &&
activeCustomColorPreview.previewed &&
document.activeElement !== activeCustomColorPreview.input
) {
rollbackCustomColorPreview();
}
}, 0);
});
window.addEventListener('blur', () => {
if (activeCustomColorPreview) colorPickerWindowBlurred = true;
});
window.addEventListener('focus', () => {
if (!activeCustomColorPreview || !colorPickerWindowBlurred) return;
if (colorPreviewFocusTimer) clearTimeout(colorPreviewFocusTimer);
colorPreviewFocusTimer = setTimeout(() => {
colorPreviewFocusTimer = null;
rollbackCustomColorPreview();
}, 100);
});
container.querySelectorAll('.bt-sw-theme-btn').forEach(b => {
b.addEventListener('click', () => {
if (!b.dataset.theme) return;
const wasActive = isThemeActiveNow();
const previousMode = getEffectiveMobileMode();
const choice = b.dataset.theme;
if (choice === 'off') {
const current = getPref(KEY_THEME, DEFAULT_THEME);
if (current === 'off') {
const prior = getPref(KEY_THEME_PRIOR, 'dark');
setPref(KEY_THEME, prior);
} else {
setPref(KEY_THEME_PRIOR, current);
setPref(KEY_THEME, 'off');
}
setSwitcherOpen(false);
} else {
setPref(KEY_THEME, choice);
}
applyTheme();
reconcileMobileRuntime(
previousMode,
getEffectiveMobileMode(),
!wasActive && isThemeActiveNow()
);
});
});
ensureSettingsToggleControls(container);
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));
if (themeNow === 'off') return;
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'));
function getCustomColorChange(kind, input) {
const themeNow = getEffectiveTheme(getPref(KEY_THEME, DEFAULT_THEME));
if (themeNow === 'off') return null;
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);
const fallback = kind === 'main'
? (themeNow === 'dark' ? DEFAULT_CUSTOM_MAIN_DARK : DEFAULT_CUSTOM_MAIN_LIGHT)
: (themeNow === 'dark' ? DEFAULT_CUSTOM_ACCENT_DARK : DEFAULT_CUSTOM_ACCENT_LIGHT);
return {
themeNow,
storageKey,
customStorageKey,
safeValue: sanitizeHex(input.value, fallback),
};
}
function previewCustomColor(kind, input, change) {
const root = document.documentElement;
if (kind === 'main') {
const palette = deriveMainPalette(change.safeValue, change.themeNow, 'custom');
Object.entries(palette).forEach(([k, v]) => root.style.setProperty(toCssVar(k), v));
root.setAttribute('data-bt-main', 'custom');
// Auto accent follows a custom Main colour during the drag as well.
if (getAccentKey(change.themeNow) === 'auto') {
const accent = deriveAccent(
deriveAutoAccent(change.safeValue, change.themeNow, 'custom')
);
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);
}
} else {
const accent = deriveAccent(change.safeValue);
root.setAttribute('data-bt-accent', 'custom');
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);
}
const row = input.closest('.bt-sw-row');
if (!row) return;
row.dataset.btPreviewing = '1';
row.querySelectorAll('.bt-sw-swatch.selected').forEach(swatch => {
swatch.classList.remove('selected');
});
const wrap = input.closest('.bt-sw-custom-wrap');
if (!wrap) return;
wrap.classList.add('selected');
const display = wrap.querySelector('.bt-sw-custom-display');
if (display) display.style.background = change.safeValue;
}
function handleColorInput(kind) {
return (e) => {
if (!e.target.matches(`input[type="color"][data-target="${kind}"]`)) return;
const change = getCustomColorChange(kind, e.target);
if (!change) return;
beginCustomColorSession(e.target);
activeCustomColorPreview.previewed = true;
// Do not call applyTheme() here: it rebuilds the swatch row and would
// remove the active color input before the drag can continue.
previewCustomColor(kind, e.target, change);
};
}
function handleColorChange(kind) {
return (e) => {
if (!e.target.matches(`input[type="color"][data-target="${kind}"]`)) return;
const change = getCustomColorChange(kind, e.target);
if (!change) return;
const input = e.target;
// WebKit may emit change more than once while its native picker is
// still being manipulated. Keep the original input node connected:
// rebuilding this row here detaches the picker after its first value.
beginCustomColorSession(input);
previewCustomColor(kind, input, change);
setPref(change.customStorageKey, change.safeValue);
setPref(change.storageKey, 'custom');
const row = input.closest('.bt-sw-row');
if (row) {
row.dataset.btSwatchSignature = [
kind,
change.themeNow,
'custom',
change.safeValue,
change.safeValue,
].join('|');
delete row.dataset.btPreviewing;
}
clearColorPreviewState();
};
}
container.querySelector('#bt-sw-main-row').addEventListener('input', handleColorInput('main'));
container.querySelector('#bt-sw-accent-row').addEventListener('input', handleColorInput('accent'));
container.querySelector('#bt-sw-main-row').addEventListener('change', handleColorChange('main'));
container.querySelector('#bt-sw-accent-row').addEventListener('change', handleColorChange('accent'));
updateSwitcherFromPreferences();
}
function injectSwitcher() {
const host =
document.body ||
document.getElementById('upper_section') ||
document.getElementById('header') ||
document.documentElement;
if (host) buildSwitcher(host);
}
// ===========================================================================
// Integrated Safari flash guard
// ===========================================================================
function getHoldBackground() {
const pref = getPref(KEY_THEME, DEFAULT_THEME);
const effective = getEffectiveTheme(pref);
if (effective === 'off') {
clearNavigationHold();
return null;
}
const fallback = effective === 'dark' ? DEFAULT_CUSTOM_MAIN_DARK : DEFAULT_CUSTOM_MAIN_LIGHT;
const holdUntil = Number(ssGet(NAV_UNTIL_SESSION_KEY) || 0);
const fromSession = holdUntil >= Date.now() ? ssGet(NAV_BG_SESSION_KEY) : null;
if (fromSession) return sanitizeHex(fromSession, fallback);
clearNavigationHold();
return sanitizeHex(getActiveMainHex(effective) || fallback, fallback);
}
function getCurrentThemeBackground() {
const root = document.documentElement;
const fallback = getHoldBackground() || DEFAULT_CUSTOM_MAIN_DARK;
try {
const inlineVar = root && root.style ? root.style.getPropertyValue('--bt-bg').trim() : '';
if (inlineVar) return sanitizeHex(inlineVar, fallback);
} catch (e) {}
try {
const computedVar = getComputedStyle(root).getPropertyValue('--bt-bg').trim();
if (computedVar) return sanitizeHex(computedVar, fallback);
} catch (e) {}
return sanitizeHex(fallback, DEFAULT_CUSTOM_MAIN_DARK);
}
function holdCss(bg, revealedSelector) {
bg = sanitizeHex(bg, getEffectiveTheme(getPref(KEY_THEME, DEFAULT_THEME)) === 'light' ? DEFAULT_CUSTOM_MAIN_LIGHT : DEFAULT_CUSTOM_MAIN_DARK);
const scheme = getEffectiveTheme(getPref(KEY_THEME, DEFAULT_THEME)) === 'light' ? 'light' : 'dark';
return `
html${revealedSelector} {
background: ${bg} !important;
color-scheme: ${scheme} !important;
}
html${revealedSelector}::before {
content: '' !important;
position: fixed !important;
inset: 0 !important;
width: 100vw !important;
height: 100vh !important;
background: ${bg} !important;
z-index: 2147483647 !important;
pointer-events: none !important;
}
html${revealedSelector} body {
visibility: hidden !important;
opacity: 0 !important;
background: ${bg} !important;
}
`;
}
function injectTransientStyle(id, cssText) {
let style = document.getElementById(id);
if (!style) {
style = document.createElement('style');
style.id = id;
(document.documentElement || document.head || document.body).appendChild(style);
}
style.textContent = cssText;
return style;
}
function removeTransientStyle(id) {
const style = document.getElementById(id);
if (style) style.remove();
}
let outgoingRestoreTimer = null;
function clearNavigationHold() {
ssRemove(NAV_UNTIL_SESSION_KEY);
ssRemove(NAV_BG_SESSION_KEY);
}
function clearOutgoingRestoreTimer() {
if (outgoingRestoreTimer !== null) clearTimeout(outgoingRestoreTimer);
outgoingRestoreTimer = null;
}
function releaseOutgoingHold() {
clearOutgoingRestoreTimer();
removeTransientStyle(OUTGOING_HOLD_STYLE_ID);
clearNavigationHold();
}
function holdPaint() {
if (document.readyState === 'complete') return false;
const bg = getHoldBackground();
if (!bg) return false;
const root = document.documentElement;
const holdStyle = injectTransientStyle(
INCOMING_HOLD_STYLE_ID,
holdCss(bg, `:not([${HOLD_REVEALED_ATTR}])`)
);
if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(() => {
if (!document.getElementById(INCOMING_HOLD_STYLE_ID)) {
try { document.documentElement.appendChild(holdStyle); } catch (e) {}
}
});
try {
observer.observe(root, { childList: true });
holdStyle._btObserver = observer;
} catch (e) {}
}
setTimeout(() => revealNow(), 1500);
return true;
}
function revealNow() {
document.documentElement.setAttribute(HOLD_REVEALED_ATTR, '');
const holdStyle = document.getElementById(INCOMING_HOLD_STYLE_ID);
if (holdStyle && holdStyle._btObserver) {
try { holdStyle._btObserver.disconnect(); } catch (e) {}
}
clearNavigationHold();
}
function releasePaint() {
revealNow();
}
function markNavigationHold(bg) {
const effective = getEffectiveTheme(getPref(KEY_THEME, DEFAULT_THEME));
const fallback = effective === 'light' ? DEFAULT_CUSTOM_MAIN_LIGHT : DEFAULT_CUSTOM_MAIN_DARK;
ssSet(NAV_UNTIL_SESSION_KEY, String(Date.now() + NAV_HOLD_SESSION_MS));
ssSet(NAV_BG_SESSION_KEY, sanitizeHex(bg, fallback));
}
function primeNavigationHold() {
if (getPref(KEY_THEME, DEFAULT_THEME) === 'off') {
releaseOutgoingHold();
return false;
}
const bg = getCurrentThemeBackground();
markNavigationHold(bg);
return true;
}
function applyOutgoingHold(restoreAfterMs = OUTGOING_UNLOAD_FAILSAFE_MS) {
if (!primeNavigationHold()) return false;
const bg = getCurrentThemeBackground();
injectTransientStyle(OUTGOING_HOLD_STYLE_ID, holdCss(bg, ''));
clearOutgoingRestoreTimer();
if (restoreAfterMs !== null) {
outgoingRestoreTimer = setTimeout(
() => releaseOutgoingHold(),
restoreAfterMs
);
}
return true;
}
function isPlainPrimaryActivation(e) {
if (e.defaultPrevented) return false;
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return false;
if (typeof e.button === 'number' && e.button !== 0) return false;
return true;
}
function shouldHoldForLink(a, e) {
if (!a || !isPlainPrimaryActivation(e)) return false;
if (!targetsCurrentBrowsingContext(a.getAttribute('target'))) return false;
if (a.hasAttribute('download')) return false;
const href = a.getAttribute('href') || '';
if (!href) return false;
if (href.startsWith('#')) return false;
if (/^\s*javascript:/i.test(href)) return false;
let url;
try {
url = new URL(href, window.location.href);
} catch (e2) {
return false;
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
if (
url.origin === window.location.origin &&
url.pathname === window.location.pathname &&
url.search === window.location.search &&
url.hash
) {
return false;
}
return true;
}
function targetsCurrentBrowsingContext(target) {
target = String(target || '').trim().toLowerCase();
if (!target || target === '_self' || target === '_parent' || target === '_top') return true;
return !!window.name && target === String(window.name).toLowerCase();
}
function shouldHoldForForm(form, e) {
if (!form || e.defaultPrevented) return false;
const submitter = e.submitter || null;
const targetOverride = submitter && submitter.getAttribute
? submitter.getAttribute('formtarget')
: null;
const target = targetOverride !== null ? targetOverride : form.getAttribute('target');
if (!targetsCurrentBrowsingContext(target)) return false;
const methodOverride = submitter && submitter.getAttribute
? submitter.getAttribute('formmethod')
: null;
const method = String(
methodOverride !== null ? methodOverride : (form.getAttribute('method') || 'get')
).toLowerCase();
return method !== 'dialog';
}
function targetLinkFromEvent(e) {
const target = e.target;
return target && target.closest ? target.closest(NAV_SELECTOR) : null;
}
function installPreNavigationHold() {
// Prime only after a complete, uncancelled click. Hiding the document on
// pointerdown removes the link from hit-testing before mouseup/click and
// prevents navigation entirely. Click covers mouse, touch, keyboard and
// programmatic anchor activation without altering the current page.
document.addEventListener('click', (e) => {
const a = targetLinkFromEvent(e);
if (!shouldHoldForLink(a, e)) return;
primeNavigationHold();
});
// Delegation covers reply/new-topic composition, PM sends, Merit awards,
// login/search and any forms inserted after startup. The bubble phase lets
// form-level validation or preventDefault handlers cancel first.
document.addEventListener('submit', (e) => {
const form = e.target;
if (!shouldHoldForForm(form, e)) return;
primeNavigationHold();
});
window.addEventListener('beforeunload', () => {
// A real page exit may outlive the short cancelled-click timeout. Keep
// the cover longer, with a failsafe for a cancelled beforeunload prompt.
applyOutgoingHold(OUTGOING_UNLOAD_FAILSAFE_MS);
});
window.addEventListener('pagehide', () => {
// pagehide confirms that the document is leaving or entering BFCache;
// pageshow releases the persistent cover if this document returns.
applyOutgoingHold(null);
});
window.addEventListener('pageshow', () => {
releaseOutgoingHold();
document.documentElement.setAttribute(HOLD_REVEALED_ATTR, '');
});
}
// ===========================================================================
// Topic read tracker
// ===========================================================================
let topicReadView = null;
function parseTopicIdFromUrl(url) {
try {
const u = new URL(url || window.location.href, window.location.href);
const topic = u.searchParams.get('topic') || '';
const m = topic.match(/^(\d+)/);
if (m) return m[1];
} catch (e) {}
const fallback = String(url || window.location.href || '').match(/[?;&]topic=(\d+)/i);
return fallback ? fallback[1] : null;
}
function parsePostNo(text) {
const m = String(text || '').match(/#\s*(\d+)/);
if (!m) return 0;
const n = parseInt(m[1], 10);
return Number.isFinite(n) && n > 0 ? n : 0;
}
function parseMsgIdFromHref(href) {
const m = String(href || '').match(/(?:[.#]|%23)msg(\d+)/i);
if (!m) return 0;
const n = parseInt(m[1], 10);
return Number.isFinite(n) && n > 0 ? n : 0;
}
function readTopicReadState() {
const raw = getPref(KEY_TOPIC_READ_STATE, '');
if (!raw) return { v: 1, topics: {} };
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return { v: 1, topics: {} };
if (!parsed.topics || typeof parsed.topics !== 'object') parsed.topics = {};
parsed.v = 1;
return parsed;
} catch (e) {
return { v: 1, topics: {} };
}
}
function writeTopicReadState(state) {
if (!state || !state.topics) return false;
const entries = Object.entries(state.topics);
if (entries.length > TOPIC_READ_STATE_MAX_ENTRIES) {
entries
.sort((a, b) => ((b[1] && b[1].updatedAt) || 0) - ((a[1] && a[1].updatedAt) || 0))
.slice(TOPIC_READ_STATE_MAX_ENTRIES)
.forEach(([topicId]) => { delete state.topics[topicId]; });
}
try {
return setPref(KEY_TOPIC_READ_STATE, JSON.stringify(state));
} catch (e) {
return false;
}
}
function mergeReadRanges(ranges, nextRange) {
const all = Array.isArray(ranges) ? ranges.slice() : [];
if (nextRange && nextRange.length === 2 && nextRange[0] > 0 && nextRange[1] >= nextRange[0]) {
all.push([nextRange[0], nextRange[1]]);
}
all.sort((a, b) => a[0] - b[0]);
const merged = [];
all.forEach(range => {
if (!Array.isArray(range) || range.length !== 2) return;
const start = parseInt(range[0], 10);
const end = parseInt(range[1], 10);
if (!Number.isFinite(start) || !Number.isFinite(end) || start <= 0 || end < start) return;
const last = merged[merged.length - 1];
if (last && start <= last[1] + 1) {
if (end > last[1]) last[1] = end;
} else {
merged.push([start, end]);
}
});
return merged.slice(-80);
}
function normaliseReadRanges(record) {
if (!record || typeof record !== 'object') return [];
const ranges = mergeReadRanges(record.ranges, null);
if (ranges.length) return ranges;
const lastPostNo = parseInt(record.lastPostNo, 10) || 0;
return lastPostNo > 0 ? [[1, lastPostNo]] : [];
}
function postNoInReadRanges(postNo, ranges) {
return Array.isArray(ranges) && ranges.some(range => {
if (!Array.isArray(range) || range.length !== 2) return false;
return postNo >= range[0] && postNo <= range[1];
});
}
function readPostSurfaceNodes(postShell) {
if (!postShell) return [];
const nodes = [postShell];
try {
postShell.querySelectorAll(
':scope > table, :scope > table > tbody > tr > td, ' +
'td.td_headerandpost, td.td_headerandpost > table, ' +
'td.td_headerandpost > table > tbody > tr > td, ' +
'div.post, td.poster, td.poster_info'
).forEach(node => {
if (node && node.nodeType === Node.ELEMENT_NODE) nodes.push(node);
});
} catch (e) {}
return nodes;
}
function setReadPostMark(post, isRead) {
readPostSurfaceNodes(post.shell).forEach(node => {
node.classList.remove(
'bt-read-post-surface',
'bt-read-post-surface-alt',
'bt-read-post-surface-main'
);
node.removeAttribute('data-bt-read-post');
});
post.shell.classList.toggle('bt-read-post', isRead);
if (isRead) post.shell.setAttribute('data-bt-read-post', 'true');
else post.shell.removeAttribute('data-bt-read-post');
}
function clearReadPostMarks() {
document.querySelectorAll('.bt-read-post, .bt-read-post-surface').forEach(node => {
node.classList.remove(
'bt-read-post',
'bt-read-post-surface',
'bt-read-post-surface-main',
'bt-read-post-surface-alt'
);
node.removeAttribute('data-bt-read-post');
});
if (topicReadView) topicReadView.lastProcessedSignature = '';
}
function collectTopicPosts(topicId) {
const seen = new Set();
const posts = [];
document.querySelectorAll('a.message_number[href*="msg"]').forEach(link => {
const postNo = parsePostNo(link.textContent);
const msgId = parseMsgIdFromHref(link.getAttribute('href') || link.href);
if (!postNo || !msgId || seen.has(msgId)) return;
const hrefTopicId = parseTopicIdFromUrl(link.getAttribute('href') || link.href);
if (hrefTopicId && hrefTopicId !== topicId) return;
const headerCell = link.closest('td.td_headerandpost');
const postShell = headerCell ? headerCell.closest('td[class*="windowbg"]') : null;
if (!postShell) return;
seen.add(msgId);
posts.push({ postNo, msgId, shell: postShell });
});
posts.sort((a, b) => a.postNo - b.postNo);
return posts;
}
function markReadPostsForCurrentTopic() {
if (!isTopicReadShadingEnabled()) {
clearReadPostMarks();
return;
}
const topicId = parseTopicIdFromUrl(window.location.href);
if (!topicId) return;
const posts = collectTopicPosts(topicId);
if (!posts.length) return;
const first = posts[0];
const last = posts[posts.length - 1];
const postSignature = `${posts.length}:${first.msgId}:${last.msgId}`;
if (
topicReadView &&
topicReadView.topicId === topicId &&
topicReadView.lastProcessedSignature === postSignature
) {
return;
}
const state = readTopicReadState();
if (!topicReadView || topicReadView.topicId !== topicId) {
const record = state.topics[topicId] || {};
topicReadView = {
topicId,
readRangesBefore: normaliseReadRanges(record),
lastProcessedSignature: '',
};
}
posts.forEach(post => {
setReadPostMark(post, postNoInReadRanges(post.postNo, topicReadView.readRangesBefore));
});
const current = state.topics[topicId] || {};
const lastPostNo = Math.max(parseInt(current.lastPostNo, 10) || 0, last.postNo);
const lastMsgId = lastPostNo === last.postNo ? last.msgId : (parseInt(current.lastMsgId, 10) || 0);
const ranges = mergeReadRanges(current.ranges, [first.postNo, last.postNo]);
const previousRanges = JSON.stringify(normaliseReadRanges(current));
const nextRanges = JSON.stringify(ranges);
state.topics[topicId] = {
lastPostNo,
lastMsgId,
updatedAt: Date.now(),
ranges,
};
if (
lastPostNo !== (parseInt(current.lastPostNo, 10) || 0) ||
lastMsgId !== (parseInt(current.lastMsgId, 10) || 0) ||
nextRanges !== previousRanges
) {
writeTopicReadState(state);
}
topicReadView.lastProcessedSignature = postSignature;
}
// ===========================================================================
// Init
// ===========================================================================
function setupRootAttributes() {
let pref = null;
if (hasGM) {
try {
const v = GM_getValue(KEY_THEME, null);
if (v != null && v !== '') pref = String(v);
} catch (e) {}
}
if (!pref) {
try {
const v = localStorage.getItem(KEY_THEME);
if (v != null && v !== '') pref = v;
} catch (e) {}
}
if (!pref) pref = DEFAULT_THEME;
const effective = getEffectiveTheme(pref);
const root = document.documentElement;
root.setAttribute('data-bt-no-transitions', '');
updateMobileLiteAttribute();
updatePageAttribute();
if (effective === 'off') {
root.setAttribute('data-bt-active', 'false');
return;
}
root.setAttribute('data-bt-active', 'true');
root.setAttribute('data-bt-theme', effective);
root.style.colorScheme = effective;
}
const paintHeld = holdPaint();
setupRootAttributes();
syncStores();
injectCSS();
clearLegacyInlineSizing();
applyTheme();
if (paintHeld) releasePaint();
installPreNavigationHold();
function enableTransitions() {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.documentElement.removeAttribute('data-bt-no-transitions');
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', enableTransitions, { once: true });
} else {
enableTransitions();
}
window.addEventListener('storage', (e) => {
if (e && e.key && ALL_KEYS.indexOf(e.key) !== -1) {
if (e.key === KEY_TOPIC_READ_STATE) {
topicReadView = null;
markReadPostsForCurrentTopic();
} else {
const wasActive = document.documentElement.getAttribute('data-bt-active') === 'true';
const previousMode = document.documentElement.getAttribute('data-bt-mobile-mode') || getEffectiveMobileMode();
applyTheme();
const nextMode = getEffectiveMobileMode();
reconcileMobileRuntime(previousMode, nextMode, !wasActive && isThemeActiveNow());
}
}
});
let modernizeTimer = null;
let modernizeIdleHandle = null;
let modernizeFrameHandle = null;
let modernizeObserver = null;
let modernizeQueuedAt = 0;
const pendingModernizeRoots = new Set();
const MODERNIZE_DEBOUNCE_MS = 180;
const BALANCED_MODERNIZE_DEBOUNCE_MS = 320;
const MODERNIZE_MAX_WAIT_MS = 600;
const BALANCED_WORK_BUDGET_MS = 7;
const BALANCED_WORK_CHUNK_MAX = 24;
const BALANCED_CANDIDATE_SELECTOR = [
'img[src*="/topic/"]',
'img[src*="/post/"]',
'img[src*="/Post/"]',
'img[src*="normal_post"]',
'img[src*="hot_post"]',
'img[src*="veryhot_post"]',
'img[src*="lock"]',
'img[src*="sticky"]',
'img[src*="pin"]',
'img[src*="new"]',
'img[src*="unread"]',
'img[alt*="Locked"]',
'img[title*="Locked"]',
'img[alt*="Sticky"]',
'img[title*="Sticky"]',
'#bbcBox_message img',
'.bbc_buttons img',
'[id^="bbc_"] img',
'img[src*="/bbc/"]',
'img[src*="bbc_"]',
'img[src*="/frostee/"]',
'img[src*="frostee_"]',
'img.reply_button',
'img.remove_button',
'img.modify_button',
'img[class$="_button"]',
'img[src*="Smileys/"]',
'img[src*="/smileys/"]',
'img.smiley',
'a.message_number',
'a[href*="action=merit"]',
].join(', ');
let balancedVisibilityObserver = null;
let balancedIdleHandle = null;
let balancedFrameHandle = null;
const balancedReadyElements = [];
const balancedPendingElements = new Set();
const balancedProcessedElements = new WeakSet();
function queryAllWithin(scope, selector) {
if (!scope || scope === document || scope.nodeType === Node.DOCUMENT_NODE) {
return Array.from(document.querySelectorAll(selector));
}
if (scope.nodeType !== Node.ELEMENT_NODE) return [];
const results = [];
if (scope.matches(selector)) results.push(scope);
scope.querySelectorAll(selector).forEach(element => results.push(element));
return results;
}
function scopeTouches(scope, selector) {
if (!scope || scope === document || scope.nodeType === Node.DOCUMENT_NODE) return true;
if (scope.nodeType !== Node.ELEMENT_NODE) return false;
return scope.matches(selector) || !!scope.closest(selector) || !!scope.querySelector(selector);
}
function runBalancedCandidate(element) {
if (!element || !element.isConnected || balancedProcessedElements.has(element)) return;
modernizeBBCodeIcons(element);
modernizeTopicListIcons(element);
modernizeSmileys(element);
stylePostMetaButtons(element);
balancedProcessedElements.add(element);
}
function scheduleBalancedWork() {
if (
balancedIdleHandle !== null || balancedFrameHandle !== null ||
!balancedReadyElements.length || !isMobileBalanced() || !isThemeActiveNow()
) return;
if (typeof requestIdleCallback === 'function') {
balancedIdleHandle = requestIdleCallback(processBalancedWork, { timeout: 900 });
} else {
balancedFrameHandle = requestAnimationFrame(processBalancedWork);
}
}
function processBalancedWork(deadline) {
balancedIdleHandle = null;
balancedFrameHandle = null;
if (!isMobileBalanced() || !isThemeActiveNow()) {
stopBalancedModernization();
return;
}
const startedAt = performance.now();
let processed = 0;
while (balancedReadyElements.length && processed < BALANCED_WORK_CHUNK_MAX) {
if (
processed > 0 &&
(performance.now() - startedAt >= BALANCED_WORK_BUDGET_MS ||
(deadline && typeof deadline.timeRemaining === 'function' && !deadline.didTimeout && deadline.timeRemaining() < 2))
) break;
const element = balancedReadyElements.shift();
balancedPendingElements.delete(element);
runBalancedCandidate(element);
processed += 1;
}
if (balancedReadyElements.length) scheduleBalancedWork();
}
function ensureBalancedVisibilityObserver() {
if (balancedVisibilityObserver || typeof IntersectionObserver === 'undefined') return balancedVisibilityObserver;
balancedVisibilityObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (!entry.isIntersecting && entry.intersectionRatio <= 0) return;
if (balancedVisibilityObserver) balancedVisibilityObserver.unobserve(entry.target);
balancedReadyElements.push(entry.target);
});
scheduleBalancedWork();
}, { rootMargin: '500px 0px' });
return balancedVisibilityObserver;
}
function isBalancedCandidateComplete(element) {
if (balancedProcessedElements.has(element)) return true;
if (element.matches('a')) return element.dataset.btPostMeta === '1';
return (
element.dataset.btTopicIcon === '1' ||
element.dataset.btReplaced === '1' ||
element.dataset.btSmiley === '1'
);
}
function queueBalancedCandidates(scope = document) {
if (!isMobileBalanced() || !isThemeActiveNow()) return;
const observer = ensureBalancedVisibilityObserver();
queryAllWithin(scope, BALANCED_CANDIDATE_SELECTOR).forEach(element => {
if (
!element.isConnected || isBalancedCandidateComplete(element) ||
balancedPendingElements.has(element)
) return;
balancedPendingElements.add(element);
if (observer) observer.observe(element);
else balancedReadyElements.push(element);
});
if (!observer) scheduleBalancedWork();
}
function stopBalancedModernization() {
if (balancedIdleHandle !== null && typeof cancelIdleCallback === 'function') {
cancelIdleCallback(balancedIdleHandle);
}
if (balancedFrameHandle !== null) cancelAnimationFrame(balancedFrameHandle);
balancedIdleHandle = null;
balancedFrameHandle = null;
balancedReadyElements.length = 0;
balancedPendingElements.clear();
if (balancedVisibilityObserver) {
try { balancedVisibilityObserver.disconnect(); } catch (e) {}
balancedVisibilityObserver = null;
}
}
function queueModernizeRoot(node) {
if (!node || node.nodeType !== Node.ELEMENT_NODE || isModernThemeOwnedNode(node)) return;
const root = node === document.body || node === document.documentElement ? document : node;
for (const queued of pendingModernizeRoots) {
if (queued === document || (queued.contains && queued.contains(root))) return;
if (root === document || (root.contains && root.contains(queued))) pendingModernizeRoots.delete(queued);
}
pendingModernizeRoots.add(root);
}
function runModernizeForScope(scope) {
modernizeBBCodeIcons(scope);
modernizeTopicListIcons(scope);
modernizeSmileys(scope);
stylePostMetaButtons(scope);
markTopicActionBars(scope);
fixMaintabCaps(scope);
highlightUnreadPm(scope);
}
function runBalancedModernizeForScope(scope) {
markTopicActionBars(scope);
fixMaintabCaps(scope);
highlightUnreadPm(scope);
queueBalancedCandidates(scope);
}
function flushModernizeQueue() {
modernizeIdleHandle = null;
modernizeFrameHandle = null;
if (isMobileLite() || !isThemeActiveNow()) {
pendingModernizeRoots.clear();
modernizeQueuedAt = 0;
return;
}
const roots = Array.from(pendingModernizeRoots).filter(root => root === document || root.isConnected);
pendingModernizeRoots.clear();
modernizeQueuedAt = 0;
if (!roots.length) return;
let needsReadTracking = false;
roots.forEach(scope => {
if (isMobileBalanced()) runBalancedModernizeForScope(scope);
else runModernizeForScope(scope);
needsReadTracking ||= scopeTouches(scope, 'a.message_number[href*="msg"]');
});
if (needsReadTracking) markReadPostsForCurrentTopic();
}
function scheduleModernize(nodes = []) {
if (isMobileLite() || !isThemeActiveNow()) return;
Array.from(nodes).forEach(queueModernizeRoot);
if (!pendingModernizeRoots.size) return;
const now = Date.now();
if (!modernizeQueuedAt) modernizeQueuedAt = now;
if (modernizeTimer) clearTimeout(modernizeTimer);
const elapsed = now - modernizeQueuedAt;
modernizeTimer = setTimeout(() => {
modernizeTimer = null;
if (isMobileLite() || !isThemeActiveNow()) {
pendingModernizeRoots.clear();
modernizeQueuedAt = 0;
return;
}
if (typeof requestIdleCallback === 'function') {
modernizeIdleHandle = requestIdleCallback(flushModernizeQueue, { timeout: 500 });
} else {
modernizeFrameHandle = requestAnimationFrame(flushModernizeQueue);
}
}, elapsed >= MODERNIZE_MAX_WAIT_MS
? 0
: isMobileBalanced() ? BALANCED_MODERNIZE_DEBOUNCE_MS : MODERNIZE_DEBOUNCE_MS);
}
function runEssentialPasses() {
markTopicActionBars();
markReadPostsForCurrentTopic();
fixMaintabCaps();
highlightUnreadPm();
}
function runFullEnhancementPasses() {
stopBalancedModernization();
modernizeBBCodeIcons();
modernizeTopicListIcons();
modernizeSmileys();
stylePostMetaButtons();
}
function runInitialPasses() {
runEssentialPasses();
if (isMobileLite()) {
stopBalancedModernization();
stopMeritSizingObserver();
return;
}
if (isMobileBalanced()) {
queueBalancedCandidates();
return;
}
runFullEnhancementPasses();
}
function reconcileMobileRuntime(previousMode = null, nextMode = getEffectiveMobileMode(), forceInitialPass = false) {
if (nextMode === 'lite' || !isThemeActiveNow()) {
stopModernizeObserver();
stopBalancedModernization();
stopMeritSizingObserver();
return;
}
ensureFonts();
if (nextMode === 'balanced') {
if (forceInitialPass || previousMode !== 'balanced') {
runEssentialPasses();
queueBalancedCandidates();
}
} else {
stopBalancedModernization();
if (forceInitialPass || previousMode !== 'full') {
runEssentialPasses();
runFullEnhancementPasses();
}
}
installModernizeObserver();
}
function stopModernizeObserver() {
if (modernizeTimer) {
clearTimeout(modernizeTimer);
modernizeTimer = null;
}
if (modernizeIdleHandle !== null && typeof cancelIdleCallback === 'function') {
cancelIdleCallback(modernizeIdleHandle);
modernizeIdleHandle = null;
}
if (modernizeFrameHandle !== null) {
cancelAnimationFrame(modernizeFrameHandle);
modernizeFrameHandle = null;
}
pendingModernizeRoots.clear();
modernizeQueuedAt = 0;
if (modernizeObserver) {
try { modernizeObserver.disconnect(); } catch (e) {}
modernizeObserver = null;
}
}
const MODERN_THEME_OWNED_SELECTOR = [
'#bt-switcher',
'[data-bt-replacement="1"]',
'[data-bt-synthetic="1"]',
'.bt-bbc-icon',
'.bt-smiley-modern',
].join(', ');
function isModernThemeOwnedNode(node) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
return !!(node.matches(MODERN_THEME_OWNED_SELECTOR) || node.closest(MODERN_THEME_OWNED_SELECTOR));
}
function addedModernizeNodes(mutation) {
if (isModernThemeOwnedNode(mutation.target)) return [];
return Array.from(mutation.addedNodes || []).filter(node => (
node.nodeType === Node.ELEMENT_NODE && !isModernThemeOwnedNode(node)
));
}
function installModernizeObserver() {
if (modernizeObserver || isMobileLite() || !isThemeActiveNow() || typeof MutationObserver === 'undefined') return;
modernizeObserver = new MutationObserver((mutations) => {
const addedNodes = mutations.flatMap(addedModernizeNodes);
if (addedNodes.length) scheduleModernize(addedNodes);
});
if (document.body) {
modernizeObserver.observe(document.body, { childList: true, subtree: true });
} else {
document.addEventListener('DOMContentLoaded', () => {
if (!isMobileLite() && document.body) {
modernizeObserver.observe(document.body, { childList: true, subtree: true });
}
}, { once: true });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
injectSwitcher();
runInitialPasses();
}, { once: true });
} else {
injectSwitcher();
runInitialPasses();
}
installModernizeObserver();
if (window.matchMedia) {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const reapply = () => {
if (getPref(KEY_THEME, DEFAULT_THEME) === 'system') applyTheme();
};
if (mq.addEventListener) mq.addEventListener('change', reapply);
else if (mq.addListener) mq.addListener(reapply);
const mobileMq = window.matchMedia('(max-width: 820px)');
let lastAutomaticMobileMode = getEffectiveMobileMode();
let mobileModeTimer = null;
const reapplyAutomaticMobileMode = () => {
if (getMobileVersionPreference() !== 'auto') {
lastAutomaticMobileMode = getEffectiveMobileMode();
return;
}
if (mobileModeTimer) clearTimeout(mobileModeTimer);
mobileModeTimer = setTimeout(() => {
mobileModeTimer = null;
const nextMobileMode = getEffectiveMobileMode();
if (nextMobileMode === lastAutomaticMobileMode) return;
const previousMobileMode = lastAutomaticMobileMode;
lastAutomaticMobileMode = nextMobileMode;
applyTheme();
reconcileMobileRuntime(previousMobileMode, nextMobileMode, true);
}, 150);
};
if (mobileMq.addEventListener) mobileMq.addEventListener('change', reapplyAutomaticMobileMode);
else if (mobileMq.addListener) mobileMq.addListener(reapplyAutomaticMobileMode);
window.addEventListener('orientationchange', reapplyAutomaticMobileMode);
}
})();