// ==UserScript== // @name LowEndTalk Compact — Modern (Graphite) // @namespace https://lowendtalk.com/ // @version 4.1 // @description Graphite-themed compact LowEndTalk: dark/light + accent picker, dense discussion rows, comment cards — with user monitoring, Pushover alerts, category watching and per-user ignore // @match *://lowendtalk.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect api.pushover.net // @run-at document-start // @downloadURL https://raw.githubusercontent.com/AlteredParadox/let_userscript/main/lowendtalk-modern.user.js // @updateURL https://raw.githubusercontent.com/AlteredParadox/let_userscript/main/lowendtalk-modern.user.js // @license AGPL-3.0-or-later // @homepageURL https://github.com/AlteredParadox/let_userscript // @supportURL https://github.com/AlteredParadox/let_userscript/issues // ==/UserScript== /* * LowEndTalk Compact — Modern (Graphite) * Copyright (C) 2026 AlteredParadox * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * Source code: https://github.com/AlteredParadox/let_userscript */ (function () { "use strict"; var IS_IFRAME = window !== window.top; // --- Theme (Graphite) --- var LETM_ACCENTS = ["#60a96d", "#5b8def", "#a78bfa", "#f0883e"]; // Category → dot colour, keyed by the /categories/ segment. var LETM_CAT_COLORS = { announcements: "#e06c75", meta: "#8b93a1", general: "#8b93a1", offers: "#60a96d", "shared-hosting-offers": "#61afef", "industry-news": "#56b6c2", outages: "#e5c07b", domains: "#c678dd", "giveaways-freebies": "#60a96d", help: "#e5c07b", news: "#56b6c2", reviews: "#f0883e", requests: "#a78bfa", "service-transfers": "#c678dd", tutorial: "#61afef", offtopic: "#8b93a1", }; // --- Userscript Settings (persisted via GM storage) --- var LET_DEFAULTS = { titleFontSize: 13, metaFontSize: 11, userMonitorInterval: 30, userMonitorAutoOn: false, // Empty by default — add users in Userscript Settings › Monitored Users. monitoredUsers: [], hideSidebar: false, pushEnabled: false, pushoverUser: "", pushoverToken: "", autoRefreshOn: false, autoRefreshSec: 120, readingPane: false, ignoredUsers: [], ignoredText: "bla bla bla", catNotifOffers: false, catNotifTransfers: false, catNotifIntervalSec: 300, catNotifLastSeen: {}, postCounter: true, theme: "dark", accent: LETM_ACCENTS[0], sidebarMode: "drawer", // column | drawer | hidden compact: false, }; var LET_STORAGE_KEY = "let-modern-settings"; var LET_LAST_SEEN_KEY = "let-user-monitor-last-seen"; var LET_LEADER_KEY = "let-poll-leader"; // GM storage keeps Pushover credentials out of the site's localStorage // (where any on-site XSS could read them) and lets tabs observe each // other's writes via GM_addValueChangeListener. localStorage remains as a // fallback so the script still runs under managers without GM_* APIs. var HAS_GM = typeof GM_getValue === "function" && typeof GM_setValue === "function"; function storeGet(key) { try { return HAS_GM ? GM_getValue(key, null) : localStorage.getItem(key); } catch (e) { return null; } } function storeSet(key, value) { try { if (HAS_GM) GM_setValue(key, value); else localStorage.setItem(key, value); } catch (e) {} } function storeRemove(key) { try { if (HAS_GM) GM_deleteValue(key); else localStorage.removeItem(key); } catch (e) {} } function storeWatch(key, handler) { if (HAS_GM && typeof GM_addValueChangeListener === "function") { GM_addValueChangeListener(key, function (name, oldV, newV, remote) { if (remote) handler(newV); }); } else if (!HAS_GM) { window.addEventListener("storage", function (e) { if (e.key === key && e.storageArea === localStorage) handler(e.newValue); }); } } // One-time move of a key out of the site's localStorage into GM storage. function storeMigrate(key) { if (!HAS_GM) return; try { var v = localStorage.getItem(key); if (v != null) { if (storeGet(key) == null) storeSet(key, v); localStorage.removeItem(key); } } catch (e) {} } var LET_CATEGORIES = [ { key: "offers", slug: "offers", label: "Offers" }, { key: "transfers", slug: "service-transfers", label: "Service Transfers", }, ]; function normalizeSettings(s) { return { titleFontSize: Number(s.titleFontSize) || LET_DEFAULTS.titleFontSize, metaFontSize: Number(s.metaFontSize) || LET_DEFAULTS.metaFontSize, userMonitorInterval: Math.max( 30, Number(s.userMonitorInterval) || LET_DEFAULTS.userMonitorInterval, ), userMonitorAutoOn: s.userMonitorAutoOn === true, monitoredUsers: Array.isArray(s.monitoredUsers) && s.monitoredUsers.length ? s.monitoredUsers : LET_DEFAULTS.monitoredUsers.slice(), hideSidebar: s.hideSidebar === true, pushEnabled: s.pushEnabled === true, pushoverUser: s.pushoverUser || "", pushoverToken: s.pushoverToken || "", autoRefreshOn: s.autoRefreshOn === true, autoRefreshSec: Number(s.autoRefreshSec) || LET_DEFAULTS.autoRefreshSec, readingPane: s.readingPane === true, ignoredUsers: Array.isArray(s.ignoredUsers) ? s.ignoredUsers : [], ignoredText: typeof s.ignoredText === "string" ? s.ignoredText : LET_DEFAULTS.ignoredText, catNotifOffers: s.catNotifOffers === true, catNotifTransfers: s.catNotifTransfers === true, catNotifIntervalSec: Number(s.catNotifIntervalSec) || LET_DEFAULTS.catNotifIntervalSec, catNotifLastSeen: s.catNotifLastSeen && typeof s.catNotifLastSeen === "object" ? s.catNotifLastSeen : {}, postCounter: s.postCounter !== false, theme: s.theme === "light" ? "light" : "dark", accent: typeof s.accent === "string" && /^#[0-9a-f]{6}$/i.test(s.accent) ? s.accent : LET_DEFAULTS.accent, sidebarMode: s.sidebarMode === "column" || s.sidebarMode === "drawer" || s.sidebarMode === "hidden" ? s.sidebarMode : s.hideSidebar === true ? "hidden" : LET_DEFAULTS.sidebarMode, compact: s.compact === true, }; } function loadSettings() { storeMigrate(LET_STORAGE_KEY); try { var s = JSON.parse(storeGet(LET_STORAGE_KEY)); if (s && typeof s === "object") return normalizeSettings(s); } catch (e) {} return Object.assign({}, LET_DEFAULTS); } function saveSettings(obj) { storeSet(LET_STORAGE_KEY, JSON.stringify(obj)); } function escapeHtml(value) { return String(value == null ? "" : value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } var letSettings = loadSettings(); var renderIgnoredList = function () {}; // updated once settings flyout is built // Another tab saved settings — refresh our copy so a later save from this // tab doesn't clobber it with stale data. storeWatch(LET_STORAGE_KEY, function (raw) { try { var s = JSON.parse(raw); if (s && typeof s === "object") letSettings = normalizeSettings(s); } catch (e) {} }); // Only one tab may send notifications (and run the category poller); // otherwise every open tab re-pushes the same posts. The leader holds a // heartbeat record; any tab may take over once it goes stale. var LET_TAB_ID = Math.random().toString(36).slice(2); var LET_LEADER_TTL_MS = 25000; function amLeader() { var rec = null; try { rec = JSON.parse(storeGet(LET_LEADER_KEY)); } catch (e) {} var now = Date.now(); if (rec && rec.id !== LET_TAB_ID && now - rec.ts < LET_LEADER_TTL_MS) { return false; } storeSet(LET_LEADER_KEY, JSON.stringify({ id: LET_TAB_ID, ts: now })); return true; } if (!IS_IFRAME) { amLeader(); setInterval(amLeader, 10000); } // Route Pushover through GM_xmlhttpRequest when available: it is exempt // from the site's CSP (see @connect api.pushover.net). function sendPushover(fields) { if (!letSettings.pushoverUser || !letSettings.pushoverToken) return; fields.token = letSettings.pushoverToken; fields.user = letSettings.pushoverUser; if (typeof GM_xmlhttpRequest === "function") { GM_xmlhttpRequest({ method: "POST", url: "https://api.pushover.net/1/messages.json", headers: { "Content-Type": "application/x-www-form-urlencoded" }, data: new URLSearchParams(fields).toString(), }); } else { var fd = new FormData(); Object.keys(fields).forEach(function (k) { fd.append(k, fields[k]); }); fetch("https://api.pushover.net/1/messages.json", { method: "POST", body: fd, }).catch(function () {}); } } // Dynamic style element for user-configurable font sizes var dynStyle = document.createElement("style"); dynStyle.id = "let-dynamic-styles"; document.documentElement.appendChild(dynStyle); // --- Graphite theme plumbing ------------------------------------------- // The palette lives in CSS custom properties on ; switching theme or // accent is a single attribute/variable write, no stylesheet rebuild. function applyTheme() { var root = document.documentElement; root.setAttribute("data-letm", letSettings.theme); root.style.setProperty("--ac", letSettings.accent); } applyTheme(); function sidebarMode() { var isProfilePage = /\/profile\//i.test(window.location.pathname); // The profile sidebar carries the user card, so never fold it away there. if (isProfilePage) return "column"; return letSettings.sidebarMode; } function applyDynamicStyles() { var titleSize = letSettings.titleFontSize; var metaSize = letSettings.metaFontSize; var css = ".let-title a { font-size: " + titleSize + "px !important; }" + ".let-meta, .let-meta span, .let-meta a, .let-meta time { font-size: " + metaSize + "px !important; }" + ".let-meta .let-num { font-size: " + (metaSize - 0.5) + "px !important; }"; var isProfilePage = /\/profile\//i.test(window.location.pathname); var isDiscussionPage = /\/discussion\/\d+/i.test(window.location.pathname); var isListingPage = !isProfilePage && !isDiscussionPage; var mode = sidebarMode(); var readingPaneOn = !IS_IFRAME && letSettings.readingPane && isListingPage; if (readingPaneOn) { // Reading pane splits the frame 50/50, so the panel always leaves flow. css += ".Column.PanelColumn { display: none !important; }"; css += ".Column.ContentColumn { flex: 0 0 50% !important; max-width: 50% !important; overflow-y: auto !important; overflow-x: hidden !important; height: calc(100vh - var(--letm-chrome)) !important; }"; css += "#let-reading-pane { display: block !important; flex: 1 1 0 !important; min-width: 0 !important; }"; // Narrow columns can't hold the fixed meta grid — let rows breathe. css += "ul.DataList.Discussions > li.ItemDiscussion { flex-wrap: wrap !important; }"; css += ".let-meta { grid-template-columns: none !important; display: flex !important; flex-wrap: wrap !important; gap: 4px 10px !important; }"; css += ".let-meta > * { grid-column: auto !important; }"; // Rows reflow to wrapped flex in the narrow pane — nothing to label. css += ".letm-colhead { display: none !important; }"; } else if (mode === "hidden") { css += ".Column.PanelColumn { display: none !important; }"; } else if (mode === "drawer") { css += ".letm-ctl, .letm-drawerbtn { display: inline-grid !important; }"; } if (mode !== "drawer" || readingPaneOn) { // The drawer toggle is the cluster's only button — hide the lot. css += ".letm-ctl, .letm-drawerbtn { display: none !important; }"; } if (!readingPaneOn) { css += "#let-reading-pane { display: none !important; }"; } dynStyle.textContent = css; document.documentElement.classList.toggle("letm-compact", !!letSettings.compact); document.documentElement.classList.toggle( "letm-drawer", mode === "drawer" && !readingPaneOn, ); if (mode !== "drawer") { document.documentElement.classList.remove("letm-drawer-open"); } } applyDynamicStyles(); // Reading pane functions function createReadingPane() { if (document.getElementById("let-reading-pane")) return; var row = document.querySelector("#Body .Row"); if (!row) return; var pane = document.createElement("div"); pane.id = "let-reading-pane"; pane.innerHTML = '
Click a post to read it here
'; row.appendChild(pane); } function loadInReadingPane(url) { var pane = document.getElementById("let-reading-pane"); if (!pane) return; pane.innerHTML = '
Loading...
'; fetch(url, { credentials: "same-origin" }) .then(function (r) { return r.text(); }) .then(function (html) { var doc = new DOMParser().parseFromString(html, "text/html"); var content = doc.querySelector(".Column.ContentColumn") || doc.querySelector("main.Content.MainContent") || doc.querySelector("#Body"); if (!content) { pane.innerHTML = '
Failed to load content
'; return; } content .querySelectorAll(".Column.PanelColumn, aside.Panel, .Panel-main") .forEach(function (el) { el.remove(); }); pane.innerHTML = ""; var wrapper = document.createElement("div"); wrapper.className = "let-reading-content"; wrapper.innerHTML = content.innerHTML; pane.appendChild(wrapper); restructureCommentHeaders(pane); interceptPaneLinks(pane); }) .catch(function () { pane.innerHTML = '
Failed to load
'; }); } function interceptPaneLinks(pane) { pane.addEventListener("click", function (e) { var a = e.target.closest("a"); if (!a) return; var href = a.getAttribute("href"); if (!href || href.startsWith("#") || href.startsWith("javascript:")) return; e.preventDefault(); e.stopPropagation(); try { var linkUrl = new URL(href, location.origin); if (linkUrl.origin !== location.origin) { window.open(href, "_blank", "noopener,noreferrer"); } else { loadInReadingPane(linkUrl.href); } } catch (ex) { window.open(href, "_blank", "noopener,noreferrer"); } }); pane.addEventListener("submit", function (e) { var form = e.target.closest("form"); if (!form) return; e.preventDefault(); e.stopPropagation(); var action = form.action || location.href; var method = (form.method || "GET").toUpperCase(); var data = new FormData(form); var opts = { credentials: "same-origin", method: method }; if (method === "POST") { opts.body = data; } else { action += (action.indexOf("?") > -1 ? "&" : "?") + new URLSearchParams(data).toString(); } fetch(action, opts) .then(function (r) { if (r.redirected) { loadInReadingPane(r.url); } else { loadInReadingPane(action); } }) .catch(function () { loadInReadingPane(action); }); }); } function toggleReadingPane(enabled) { if (enabled) { createReadingPane(); } else { var pane = document.getElementById("let-reading-pane"); if (pane) { pane.innerHTML = '
Click a post to read it here
'; } } } // ---- Post counter bar (integrated from lowendtalk-post-counter.user.js) ---- var POSTS_PER_PAGE = 30; function postCounterCurrentPage() { var urlMatch = window.location.pathname.match(/\/p(\d+)\/?$/); if (urlMatch) return parseInt(urlMatch[1], 10); var highlight = document.querySelector(".Pager .Highlight"); if (highlight) { var num = parseInt(highlight.textContent.trim(), 10); if (!isNaN(num)) return num; } return 1; } function postCounterLastPage() { var maxPage = 1; document.querySelectorAll('.Pager a[href*="/p"]').forEach(function (link) { var m = link.href.match(/\/p(\d+)/); if (m) maxPage = Math.max(maxPage, parseInt(m[1], 10)); }); document.querySelectorAll(".Pager .Highlight").forEach(function (el) { var m = el.textContent.trim().match(/^(\d+)$/); if (m) maxPage = Math.max(maxPage, parseInt(m[1], 10)); }); return maxPage; } function postCounterCount() { var currentPage = postCounterCurrentPage(); var comments = document.querySelectorAll( "ul.DataList.Comments > li.ItemComment", ); var opCount = 0; if (currentPage === 1) { var op = document.querySelector( ".MessageList.Discussion .ItemDiscussion", ); if (op) opCount = 1; } return comments.length + opCount; } function updatePostCounter() { var existing = document.getElementById("let-post-counter"); if (existing) existing.remove(); if (document.body) document.body.style.paddingBottom = ""; if (!letSettings.postCounter) return; if (!/\/discussion\/\d+/i.test(window.location.pathname)) return; if (!document.body) return; var currentPage = postCounterCurrentPage(); var lastPage = postCounterLastPage(); var postsOnPage = postCounterCount(); if (postsOnPage === 0) return; var remaining = POSTS_PER_PAGE - postsOnPage; var nextPage = currentPage + 1; var bar = document.createElement("div"); bar.id = "let-post-counter"; var span = document.createElement("span"); if (remaining > 0) { span.innerHTML = "" + postsOnPage + " posts on this page — " + '' + remaining + " more until page " + nextPage; } else if (remaining === 0) { if (currentPage === lastPage) { span.innerHTML = 'Page full (' + POSTS_PER_PAGE + "/" + POSTS_PER_PAGE + ") — next post starts page " + nextPage; } else { span.innerHTML = 'Page full (' + POSTS_PER_PAGE + "/" + POSTS_PER_PAGE + ")"; } } else { span.innerHTML = "" + postsOnPage + " posts on this page"; } if (lastPage > 1) { span.innerHTML += "  •  Page " + currentPage + " of " + lastPage; } bar.appendChild(span); var close = document.createElement("span"); close.className = "let-pc-close"; close.textContent = "✕"; close.title = "Hide post counter (re-enable in Userscript Settings)"; close.addEventListener("click", function (e) { e.stopPropagation(); letSettings.postCounter = false; saveSettings(letSettings); var chk = document.querySelector(".let-chk-postcounter"); if (chk) chk.checked = false; bar.remove(); document.body.style.paddingBottom = ""; }); bar.appendChild(close); document.body.style.paddingBottom = "40px"; document.body.appendChild(bar); } // Intercept post title clicks for reading pane if (!IS_IFRAME) { document.addEventListener("click", function (e) { if (!letSettings.readingPane) return; var link = e.target.closest(".let-title a"); if (!link) return; e.preventDefault(); e.stopPropagation(); document.querySelectorAll("li.let-reading-active").forEach(function (li) { li.classList.remove("let-reading-active"); }); var li = link.closest("li"); if (li) li.classList.add("let-reading-active"); loadInReadingPane(link.href); }); } const style = document.createElement("style"); style.textContent = ` @import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;450;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap'); /* ============================ palette ============================ */ html[data-letm="dark"] { --bg0:#08090b; --bg:#0e0f12; --bg2:#15171b; --bg3:#1c1f25; --line:#24272e; --line2:#2f333c; --tx:#e6e8ec; --tx2:#9aa1ab; --tx3:#646b76; --shadow:rgba(0,0,0,.45); --letm-chrome:82px; } html[data-letm="light"] { --bg0:#eceef0; --bg:#f6f7f8; --bg2:#ffffff; --bg3:#edeff2; --line:#e2e5e9; --line2:#d2d6dc; --tx:#1a1d21; --tx2:#5a616b; --tx3:#8e959f; --shadow:rgba(20,25,35,.18); --letm-chrome:82px; } /* ============================ base ============================ */ html, body, body.Vanilla { background: var(--bg0) !important; color: var(--tx) !important; } html { overflow-x: hidden !important; } body, input, button, select, textarea, .Frame { font-family: Geist, system-ui, -apple-system, sans-serif !important; } a { color: var(--ac) !important; text-decoration: none !important; } a:hover { color: var(--ac) !important; text-decoration: underline !important; } /* The whole frame is one column: header, card, footer */ #Frame { background: var(--bg0) !important; display: flex !important; flex-direction: column !important; min-height: 100vh !important; margin: 0 !important; border: none !important; box-shadow: none !important; } #Body { flex: 1 !important; background: var(--bg0) !important; } #Frame, #Body, #Head, #Foot, #Head .Row, #Body .Row, #Foot .Row, .Row, .Column.ContentColumn, #Content { max-width: none !important; width: 100% !important; box-sizing: border-box !important; } #Content, .Section, .Section-Content, .DataList, ul.DataList, .CategoryList, .DataTable, .PageBox, .Content, .MessageList, .Item { background: transparent !important; color: var(--tx) !important; } /* ============================ header ============================ */ #Head { background: var(--bg2) !important; border: none !important; border-bottom: 1px solid var(--line) !important; padding: 0 !important; margin: 0 !important; position: sticky !important; top: 0 !important; z-index: 1000 !important; box-shadow: none !important; } #Head .Row, #Head .Row > *, #Head .SiteMenu, #Head .SiteMenu li, #Head .SiteMenu ul, #Head .MeBox, #Head #let-head-mebox { border: none !important; background: transparent !important; } #Head, #Head .Row, #Head .Row > * { color: var(--tx2) !important; } #Head .Row { display: flex !important; align-items: center !important; gap: 4px !important; min-height: 40px !important; padding: 0 14px !important; background: var(--bg2) !important; } /* Centre every header child on its own, so no item can fall back to baseline alignment and drift against the others. */ #Head .Row > * { align-self: center !important; } /* wordmark */ #Head .SiteTitle { /* Flex only on the wrapper: it centres the link without contributing a line-box strut (the theme leaves this element at 24px). */ display: flex !important; align-items: center !important; align-self: center !important; margin: 0 10px 0 0 !important; padding: 0 !important; flex: none !important; font-size: 13px !important; line-height: 1.4 !important; } /* The theme pins the wordmark to its own "helvetica neue" stack. Left alone it keeps those metrics while the nav links render in Geist, and two typefaces in one line box put their baselines in different places — the wordmark rode high in Firefox, whose Helvetica substitute diverges from Geist more than Chrome's. Same family here means same metrics means same baseline. */ #Head .SiteTitle, #Head .SiteTitle a { font-family: Geist, system-ui, -apple-system, sans-serif !important; } /* Same trap, other headings the theme pins to its own stack. h4 is left out on purpose — the panel/section h4s are deliberately Geist Mono. */ .BoxFilter-Heading, .BoxFilter h2, .Column.ContentColumn h1, .Column.ContentColumn h2, .Column.ContentColumn h3 { font-family: Geist, system-ui, -apple-system, sans-serif !important; } #Head .SiteTitle a { /* Deliberately NOT a flex container. As inline-flex, the wordmark text becomes an anonymous flex item, and Firefox positions those higher than Chrome does — the accent dot (a real flex item) stayed put while the text rode up. A plain block keeps the text in an ordinary line box, laid out exactly like .SiteMenu a, so both engines agree. */ display: block !important; font-size: 13px !important; line-height: 1.4 !important; /* Matches .SiteMenu a's vertical padding. */ padding: 4px 0 !important; margin: 0 !important; font-weight: 600 !important; letter-spacing: .01em !important; text-transform: none !important; color: var(--tx) !important; white-space: nowrap !important; } #Head .SiteTitle a:hover { text-decoration: none !important; } .letm-logomark { width: 10px; height: 10px; border-radius: 3px; background: var(--ac); display: inline-block; vertical-align: middle; margin-right: 8px; /* nudge off the x-height centre onto the cap-height centre */ position: relative; top: -1px; } /* nav pills */ #Head .SiteMenu { display: flex !important; align-items: center !important; gap: 2px !important; margin: 0 !important; padding: 0 !important; list-style: none !important; } #Head .SiteMenu li { margin: 0 !important; padding: 0 !important; } #Head .SiteMenu a { display: inline-block !important; color: var(--tx2) !important; font-size: 12.5px !important; font-weight: 400 !important; padding: 4px 8px !important; border-radius: 6px !important; line-height: 1.4 !important; } #Head .SiteMenu a:hover { color: var(--tx) !important; background: var(--bg3) !important; text-decoration: none !important; } #Head .SiteMenu li.Active a, #Head .SiteMenu a.Selected { color: var(--tx) !important; background: var(--bg3) !important; font-weight: 500 !important; } /* header search */ #Head .SiteSearch { display: block !important; margin-left: auto !important; margin-right: 10px !important; padding: 0 !important; background: transparent !important; } #Head .SiteSearch div { display: flex !important; gap: 4px !important; } #Head .SiteSearch input.InputBox { width: 190px !important; font-size: 12px !important; padding: 4px 9px !important; box-sizing: border-box !important; border: 1px solid var(--line) !important; background: var(--bg3) !important; color: var(--tx) !important; border-radius: 6px !important; } #Head .SiteSearch input.InputBox:focus { border-color: var(--ac) !important; outline: none !important; } #Head .SiteSearch .Button { font-size: 11px !important; padding: 4px 9px !important; filter: none !important; } /* MeBox icons in the header bar */ #let-head-mebox { display: inline-flex !important; align-items: center !important; gap: 6px !important; padding: 0 !important; margin: 0 !important; position: relative !important; } #let-head-mebox a { color: var(--tx2) !important; padding: 3px 4px !important; border-radius: 6px !important; display: inline-flex !important; align-items: center !important; } #let-head-mebox a:hover { color: var(--tx) !important; background: var(--bg3) !important; text-decoration: none !important; } #let-head-mebox .PhotoWrap { width: 22px !important; height: 22px !important; padding: 0 !important; margin: 0 !important; } #let-head-mebox .PhotoWrap img { width: 22px !important; height: 22px !important; border-radius: 50% !important; } #let-head-mebox .Alert { font-family: 'Geist Mono', monospace !important; font-size: 9px !important; padding: 1px 5px !important; border-radius: 99px !important; background: var(--ac) !important; color: #0b0d0f !important; } #let-head-mebox a[title="Account Options"], #let-head-mebox .MeButton-user, #let-head-mebox > a.MeButton:not([title]) { display: none !important; } .letm-userwrap { gap: 7px !important; margin-left: 2px !important; } .letm-userwrap > a.PhotoWrap { cursor: pointer !important; } #let-head-mebox a.Username { font-size: 12.5px !important; font-weight: 500 !important; color: var(--tx2) !important; max-width: 130px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; display: inline-block !important; } .letm-usermenu { width: 190px !important; left: auto !important; right: 0 !important; padding: 5px !important; } .letm-usermenu a { display: block !important; padding: 7px 10px !important; border-radius: 6px !important; font-size: 12px !important; color: var(--tx2) !important; } .letm-usermenu a:hover { background: var(--bg3) !important; color: var(--tx) !important; text-decoration: none !important; } html[data-letm="dark"] #let-head-mebox .Sprite { filter: invert(.72); } /* ============================ layout card ============================ */ #Body > .Row { display: flex !important; align-items: stretch !important; gap: 12px !important; padding: 12px 14px 16px !important; border-top: none !important; margin: 0 !important; } .Column.ContentColumn { flex: 1 1 auto !important; min-width: 0 !important; order: 1 !important; background: var(--bg) !important; border: 1px solid var(--line2) !important; border-radius: 10px !important; overflow: hidden !important; padding: 0 !important; margin: 0 !important; } .Column.PanelColumn { width: 236px !important; min-width: 236px !important; max-width: 236px !important; flex-shrink: 0 !important; order: 2 !important; background: var(--bg2) !important; border: 1px solid var(--line2) !important; border-radius: 10px !important; padding: 10px !important; box-sizing: border-box !important; color: var(--tx2) !important; align-self: flex-start !important; max-height: calc(100vh - var(--letm-chrome) - 28px) !important; overflow-y: auto !important; } /* sidebar as a slide-over drawer */ html.letm-drawer .Column.PanelColumn { position: fixed !important; top: 0 !important; right: 0 !important; bottom: 0 !important; width: 300px !important; min-width: 300px !important; max-width: 300px !important; height: 100vh !important; max-height: none !important; z-index: 1200 !important; margin: 0 !important; padding: 16px !important; border: none !important; border-left: 1px solid var(--line2) !important; border-radius: 0 !important; box-shadow: -16px 0 40px var(--shadow) !important; transform: translateX(105%); transition: transform .22s ease; } html.letm-drawer-open .Column.PanelColumn { transform: none; } .letm-scrim { position: fixed; inset: 0; z-index: 1100; background: rgba(0,0,0,.4); opacity: 0; pointer-events: none; transition: opacity .22s; } html.letm-drawer-open .letm-scrim { opacity: 1; pointer-events: auto; } /* sidebar contents */ .Column.PanelColumn .Box, .Column.PanelColumn .PanelInfo, .Column.PanelColumn .BoxButtons { background: none !important; border: none !important; padding: 0 !important; margin: 0 0 14px !important; box-shadow: none !important; } .Column.PanelColumn h4, .PanelInfo h4 { font-family: 'Geist Mono', monospace !important; font-size: 9.5px !important; text-transform: uppercase !important; letter-spacing: .1em !important; color: var(--tx3) !important; background: none !important; border: none !important; margin: 0 0 6px !important; padding: 0 !important; } .Column.PanelColumn a { color: var(--tx2) !important; font-size: 12px !important; } .Column.PanelColumn a:hover { color: var(--tx) !important; } .Column.PanelColumn .PanelInfo li { padding: 3px 0 !important; font-size: 12px !important; border: none !important; background: none !important; display: flex !important; align-items: center !important; gap: 7px !important; } .Column.PanelColumn .PanelInfo li a { font-size: 12px !important; } .Column.PanelColumn .BigButton, .Column.PanelColumn .Button.Action.Primary { display: block !important; text-align: center !important; padding: 7px 12px !important; font-size: 12px !important; margin: 0 0 10px !important; } /* hide sidebar ads and the duplicated MeBox */ .Column.PanelColumn [id^="bsap_"], .Column.PanelColumn [class*="bsap"], .Column.PanelColumn [class*="bsa_"], .Column.PanelColumn .bsarocks, .Column.PanelColumn a[rel="nofollow noopener"][target="_blank"], .Column.PanelColumn .MeBox.MeBox-SignedIn, .Column.PanelColumn .MeBox, .Column.PanelColumn .Box.MeBox, .Panel .MeBox { display: none !important; } .Column.PanelColumn .SiteSearch { display: none !important; } /* ============================ tabs / controls ============================ */ div.Headings, div.Tabs, .DiscussionsTabs, .DiscussionTabs, .Column.ContentColumn > div.Tabs, .Column.ContentColumn > div.Headings { background: var(--bg2) !important; border: none !important; border-bottom: 1px solid var(--line) !important; margin: 0 !important; padding: 5px 12px !important; display: flex !important; align-items: center !important; gap: 4px !important; min-height: 0 !important; } .Tabs ul, .Tabs .TabList { display: flex !important; align-items: center !important; gap: 2px !important; margin: 0 !important; padding: 0 !important; list-style: none !important; } .Tabs li { margin: 0 !important; padding: 0 !important; border: none !important; background: none !important; } .Tabs a { color: var(--tx2) !important; font-size: 12px !important; padding: 4px 9px !important; border-radius: 6px !important; border: none !important; background: none !important; line-height: 1.4 !important; } .Tabs a:hover { color: var(--tx) !important; background: var(--bg3) !important; text-decoration: none !important; } .Tabs .Active a, .Tabs li.Active a, .Tabs a.Active { color: var(--tx) !important; background: var(--bg3) !important; font-weight: 500 !important; border: none !important; } .BoxFilter, .FilterMenu { padding: 0 !important; margin: 0 !important; font-size: 11.5px !important; background: transparent !important; } .PageControls { display: flex !important; align-items: center !important; justify-content: flex-end !important; gap: 10px !important; padding: 6px 12px !important; margin: 0 !important; background: var(--bg2) !important; border-bottom: 1px solid var(--line) !important; min-height: 0 !important; max-width: none !important; width: 100% !important; box-sizing: border-box !important; } .PageControls.Bottom { border-bottom: none !important; border-top: 1px solid var(--line) !important; background: var(--bg) !important; } .Pager { padding: 0 !important; margin: 0 !important; } .PagerNub { display: none !important; } .Pager a, .Pager span { font-family: 'Geist Mono', monospace !important; font-size: 11px !important; color: var(--tx3) !important; background: none !important; border: none !important; padding: 3px 8px !important; border-radius: 5px !important; margin: 0 !important; min-width: 0 !important; } .Pager a:hover { color: var(--tx) !important; background: var(--bg3) !important; text-decoration: none !important; } .Pager .Active, .Pager .Highlight { background: var(--bg3) !important; color: var(--tx) !important; } /* sort control */ .let-sort-wrap { margin-right: auto !important; display: flex !important; align-items: center !important; gap: 4px !important; } .let-sort-select, .let-sort-dir { display: inline-block !important; visibility: visible !important; opacity: 1 !important; width: auto !important; height: auto !important; max-height: none !important; overflow: visible !important; background: var(--bg3) !important; color: var(--tx2) !important; border: 1px solid var(--line) !important; font-size: 11px !important; font-family: Geist, sans-serif !important; padding: 3px 7px !important; border-radius: 6px !important; cursor: pointer !important; line-height: 1.3 !important; } .let-sort-select { -webkit-appearance: menulist !important; appearance: menulist !important; } .let-sort-select:hover, .let-sort-dir:hover { border-color: var(--ac) !important; color: var(--tx) !important; } /* ============================ discussion rows ============================ */ .Breadcrumbs, .BreadcrumbsWrapper, #Body .HomeCrumb, .Column.ContentColumn > .Breadcrumbs { display: none !important; } ul.DataList.Discussions { padding: 0 !important; margin: 0 !important; border: none !important; } .DataList li, .DataList .Item, ul.DataList > li { border: none !important; box-shadow: none !important; outline: none !important; } ul.DataList.Discussions > li.ItemDiscussion { display: flex !important; flex-wrap: nowrap !important; align-items: center !important; gap: 10px !important; min-height: 34px !important; padding: 4px 14px !important; margin: 0 !important; background: transparent !important; border: none !important; border-bottom: 1px solid var(--line) !important; box-shadow: none !important; animation: none !important; overflow: hidden !important; } ul.DataList.Discussions > li.ItemDiscussion:last-child { border-bottom: none !important; } ul.DataList.Discussions > li.ItemDiscussion:hover { background: var(--bg2) !important; } ul.DataList.Discussions > li.Announcement, ul.DataList.Discussions > li.Alt, ul.DataList.Discussions > li.Read, .Discussions .DataList .Item, .Item, .Item.Read { background: transparent !important; } /* unread dot */ ul.DataList.Discussions > li.ItemDiscussion::before { content: ''; flex: none; width: 6px; height: 6px; border-radius: 50%; background: transparent; } ul.DataList.Discussions > li.ItemDiscussion:not(.Read)::before { background: var(--ac); } /* column header strip above the list */ .letm-colhead { display: flex !important; align-items: center !important; gap: 10px !important; padding: 5px 14px !important; background: var(--bg2) !important; border-bottom: 1px solid var(--line) !important; white-space: nowrap !important; user-select: none; } /* stand-in for the unread dot so the columns start at the same offset */ .letm-colhead::before { content: ''; flex: none; width: 6px; height: 6px; } /* Two classes deep so these beat the dynamic font-size sheet, which targets .let-meta / .let-title with a single class and is appended after this one. */ .letm-colhead .let-title, .letm-colhead .let-meta, .letm-colhead .let-meta > * { font-family: 'Geist Mono', monospace !important; font-size: 9px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: .08em !important; color: var(--tx3) !important; line-height: 1.5 !important; } /* strip the pill off the "new" label — it is a heading here, not a badge */ .letm-colhead .let-new { background: none !important; padding: 0 !important; border-radius: 0 !important; } .letm-colhead .let-avatar, .letm-colhead .let-bookmark { flex: none !important; } .letm-colhead .let-bookmark { width: 16px !important; } .let-avatar { flex: none !important; width: 18px !important; height: 18px !important; order: 1 !important; } .let-avatar img { width: 18px !important; height: 18px !important; border-radius: 50% !important; vertical-align: middle !important; } .let-title { order: 2 !important; flex: 1 1 0 !important; min-width: 90px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } .let-title a { color: var(--tx2) !important; font-weight: 450 !important; line-height: 1.5 !important; } .let-title a:hover { color: var(--tx) !important; text-decoration: none !important; } li.ItemDiscussion:not(.Read) .let-title a { color: var(--tx) !important; font-weight: 600 !important; } /* fixed meta grid so columns line up across every row */ .let-meta { order: 3 !important; display: grid !important; grid-template-columns: max-content 128px 50px 58px 70px 118px 74px; /* tags cat cmts views new by date */ align-items: center !important; column-gap: 10px !important; flex: none !important; white-space: nowrap !important; color: var(--tx3) !important; line-height: 1.5 !important; } .let-meta > * { grid-row: 1; margin: 0 !important; min-width: 0 !important; } .let-meta .let-tags { grid-column: 1; justify-self: end; display: flex; gap: 5px; align-items: center; } .let-meta .let-cat { grid-column: 2; justify-self: start; overflow: hidden; text-overflow: ellipsis; } .let-meta .let-comments { grid-column: 3; justify-self: end; } .let-meta .let-views { grid-column: 4; justify-self: end; } .let-meta .let-new { grid-column: 5; justify-self: end; } .let-meta .let-recent { grid-column: 6; justify-self: end; overflow: hidden; text-overflow: ellipsis; } .let-meta .let-date { grid-column: 7; justify-self: end; } .let-meta .let-num { font-family: 'Geist Mono', monospace !important; color: var(--tx2) !important; } .let-meta .let-views .let-num { color: var(--tx3) !important; } .let-meta .let-comments .let-num::after { content: ' \\21A9'; color: var(--tx3); font-size: 10px; } .let-meta .let-date, .let-meta .let-date time { color: var(--tx3) !important; } .let-meta .let-recent a { color: var(--tx2) !important; text-decoration: none !important; } .let-meta .let-recent a:hover { color: var(--tx) !important; } /* category chip: plain text + colour dot */ .let-meta .let-cat, .let-meta .let-cat a { background: none !important; border: none !important; border-radius: 0 !important; padding: 0 !important; box-shadow: none !important; color: var(--tx2) !important; font-weight: 400 !important; } .let-meta .let-cat a { display: inline-flex !important; align-items: center !important; gap: 5px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; text-decoration: none !important; } .let-meta .let-cat a:hover { color: var(--tx) !important; } .letm-dot { flex: none; width: 7px; height: 7px; border-radius: 2px; display: inline-block; } /* tags + "N new" badge */ .let-meta .let-tag { font-family: 'Geist Mono', monospace !important; font-size: 9px !important; text-transform: uppercase !important; letter-spacing: .06em !important; font-weight: 600 !important; color: var(--ac) !important; background: color-mix(in srgb, var(--ac) 14%, transparent) !important; border: none !important; border-radius: 99px !important; padding: 2px 7px !important; } .let-meta .let-tag.Tag-Closed { color: var(--tx3) !important; background: var(--bg3) !important; } .let-meta .let-new { font-family: 'Geist Mono', monospace !important; font-size: 9px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: .05em !important; color: var(--ac) !important; background: color-mix(in srgb, var(--ac) 15%, transparent) !important; border-radius: 99px !important; padding: 2px 7px !important; } .let-bookmark { order: 4 !important; flex: none !important; display: flex !important; align-items: center !important; gap: 3px !important; } .let-bookmark a { display: inline-flex !important; align-items: center !important; justify-content: center !important; width: 16px !important; height: 16px !important; font-size: 0 !important; flex-shrink: 0 !important; } .let-bookmark .OptionsMenu { display: none !important; } html[data-letm="dark"] .let-bookmark .Sprite.SpBookmarks, html[data-letm="dark"] .let-bookmark .Bookmark { filter: invert(.72) !important; } html[data-letm="light"] .let-bookmark .Sprite.SpBookmarks, html[data-letm="light"] .let-bookmark .Bookmark { filter: none !important; } /* hide original children after restructure */ ul.DataList.Discussions > li.ItemDiscussion.let-done > a.IndexPhoto, ul.DataList.Discussions > li.ItemDiscussion.let-done > .ItemContent, ul.DataList.Discussions > li.ItemDiscussion.let-done > .Options { display: none !important; } /* category header band (category pages) and the category index */ ul.DataList.CategoryList { padding: 0 !important; margin: 0 !important; } ul.DataList.CategoryList > li.Item { background: var(--bg2) !important; border: none !important; border-bottom: 1px solid var(--line) !important; padding: 9px 14px !important; margin: 0 !important; } .ItemContent.Category .Title, .ItemContent.Category .Title a, .ItemContent.Category h3, .ItemContent.Category h3 a { font-size: 14px !important; font-weight: 600 !important; color: var(--tx) !important; text-decoration: none !important; } .ItemContent.Category .Description { font-size: 12px !important; color: var(--tx2) !important; margin: 3px 0 0 !important; } .ItemContent.Category .Meta { font-size: 11px !important; color: var(--tx3) !important; margin: 5px 0 0 !important; display: flex !important; flex-wrap: wrap !important; align-items: center !important; gap: 3px 10px !important; } .ItemContent.Category .Meta a { color: var(--tx2) !important; } .ItemContent.Category .Meta .Number { font-family: 'Geist Mono', monospace !important; color: var(--tx2) !important; } .ItemContent.Category .Meta .RSS, .ItemContent.Category .Meta img { display: none !important; } /* ============================ thread view ============================ */ .MessageList.Discussion .ItemDiscussion, ul.DataList.Comments > li.ItemComment { background: var(--bg2) !important; border: 1px solid var(--line) !important; border-radius: 9px !important; margin: 0 12px 8px !important; padding: 10px 14px !important; box-shadow: none !important; color: var(--tx) !important; } ul.DataList.Comments { padding: 12px 0 4px !important; margin: 0 !important; } .MessageList.Discussion { padding: 12px 0 0 !important; margin: 0 !important; } ul.DataList.Comments > li.ItemComment:hover { border-color: var(--line2) !important; } li.ItemComment > .Comment { background: transparent !important; } .Item-Header.CommentHeader, .CommentHeader, .Comment .Header, .ItemComment .Header { background: transparent !important; border: none !important; padding: 0 0 6px !important; margin: 0 !important; display: flex !important; align-items: center !important; flex-wrap: wrap !important; gap: 8px !important; } .AuthorWrap { display: inline-flex !important; align-items: center !important; gap: 8px !important; flex-wrap: wrap !important; } /* two-row header: avatar beside (username + role) over (date · flag · …) */ .Item-Header.letm-hdr { display: flex !important; align-items: flex-start !important; gap: 9px !important; padding: 0 0 7px !important; margin: 0 !important; flex-wrap: nowrap !important; } .letm-hdr > a.PhotoWrap { flex: none !important; width: 30px !important; height: 30px !important; margin: 1px 0 0 !important; padding: 0 !important; } .letm-hdr > a.PhotoWrap img { width: 30px !important; height: 30px !important; border-radius: 50% !important; } .letm-hdrcol { display: flex !important; flex-direction: column !important; align-items: flex-start !important; gap: 2px !important; min-width: 0 !important; flex: 1 1 auto !important; } .letm-hdr .letm-hdrcol .AuthorWrap { display: flex !important; align-items: baseline !important; gap: 7px !important; flex-wrap: wrap !important; margin: 0 !important; padding: 0 !important; line-height: 1.35 !important; } .letm-hdr .letm-hdrcol .Meta { margin: 0 !important; padding: 0 !important; display: flex !important; align-items: center !important; flex-wrap: wrap !important; gap: 2px 10px !important; line-height: 1.35 !important; } .AuthorWrap .Author { display: inline-flex !important; align-items: center !important; gap: 7px !important; } .CommentHeader .PhotoWrap, .AuthorWrap .PhotoWrap { width: 24px !important; height: 24px !important; } .CommentHeader .PhotoWrap img, .AuthorWrap .PhotoWrap img { width: 24px !important; height: 24px !important; border-radius: 50% !important; } .AuthorWrap a.Username, .Username a, .AuthorWrap .Author a { font-size: 12.5px !important; font-weight: 600 !important; color: var(--tx) !important; text-decoration: none !important; } .AuthorWrap a.Username:hover { color: var(--ac) !important; } .AuthorInfo .MItem, .RoleTitle, .Author .Rank, .RankLabel { font-family: 'Geist Mono', monospace !important; font-size: 9px !important; text-transform: uppercase !important; letter-spacing: .06em !important; color: var(--tx3) !important; background: none !important; border: none !important; padding: 0 !important; } .Meta.CommentMeta, .CommentMeta, .MessageList .Meta { margin-left: auto !important; display: inline-flex !important; align-items: center !important; gap: 10px !important; background: none !important; } .CommentMeta .MItem, .CommentMeta .MItem a, .DateCreated, .DateCreated a, .DateCreated time { font-size: 11px !important; color: var(--tx3) !important; padding: 0 !important; margin: 0 !important; background: none !important; } .CommentMeta .MItem a:hover { color: var(--tx) !important; } .Item-BodyWrap, .Item-Body { background: transparent !important; padding: 0 !important; margin: 0 !important; } .Message, .Comment .Message, .Discussion .Message { background: transparent !important; color: var(--tx) !important; font-size: 13px !important; line-height: 1.6 !important; padding: 0 !important; } .Message p, .Message li, .Message td { color: var(--tx) !important; } .Message img { max-width: 100% !important; height: auto !important; } blockquote, .Message blockquote, .UserQuote, .Quote { background: var(--bg3) !important; border: none !important; border-left: 2px solid var(--ac) !important; border-radius: 0 6px 6px 0 !important; padding: 7px 11px !important; margin: 0 0 8px !important; font-size: 12.5px !important; color: var(--tx2) !important; } code, pre { background: var(--bg3) !important; color: var(--tx) !important; border: 1px solid var(--line) !important; border-radius: 6px !important; font-family: 'Geist Mono', monospace !important; font-size: 12px !important; } .Reactions, .ReactButton { font-family: 'Geist Mono', monospace !important; font-size: 10.5px !important; } .Reactions a { color: var(--tx3) !important; background: none !important; border: none !important; } .Reactions a:hover { color: var(--tx) !important; text-decoration: none !important; } h2.CommentHeading, .CommentHeading { margin: 0 !important; padding: 4px 14px 8px !important; font-size: 13px !important; font-weight: 600 !important; color: var(--tx2) !important; border: none !important; background: none !important; } .DataBox, .DataBox-Comments, .PageBox { background: transparent !important; border: none !important; } .CategoryHeading, h1.H, h2.H, .PageTitle h1 { color: var(--tx) !important; font-size: 17px !important; font-weight: 600 !important; letter-spacing: -.01em !important; background: none !important; border: none !important; margin: 0 !important; padding: 14px 14px 8px !important; } /* composer */ .CommentForm, .MessageForm, .DiscussionForm, .FormWrapper, .CommentFormWrap { background: var(--bg2) !important; border: 1px solid var(--line) !important; border-radius: 9px !important; margin: 8px 12px 14px !important; padding: 12px 14px !important; box-shadow: none !important; } .CommentForm .FormWrapper, .CommentForm .CommentFormWrap, .MessageForm .FormWrapper { background: none !important; border: none !important; margin: 0 !important; padding: 0 !important; } .CommentForm h2, .MessageForm h2, .DiscussionForm h2, .CommentForm h1, .MessageForm h1 { margin: 0 0 10px !important; padding: 0 !important; font-size: 14px !important; font-weight: 600 !important; color: var(--tx) !important; border: none !important; background: none !important; } .CommentForm textarea, .MessageForm textarea, .DiscussionForm textarea, .CommentForm .BodyBox, .MessageForm .BodyBox { width: 100% !important; min-height: 110px !important; box-sizing: border-box !important; resize: vertical !important; } .CommentForm .Buttons, .MessageForm .Buttons { text-align: right !important; margin: 10px 0 0 !important; } .DiscussionForm input[type=text], .DiscussionForm select { width: 100% !important; box-sizing: border-box !important; } .richEditor, .richEditor-menu { background: transparent !important; border: none !important; } .richEditor-frame, .richEditor-text, .BodyBox, .TextBoxWrapper { background: var(--bg3) !important; border: 1px solid var(--line) !important; border-radius: 7px !important; color: var(--tx) !important; } /* ============================ forms & buttons ============================ */ input, textarea, select, .InputBox, .TextBox { background: var(--bg3) !important; color: var(--tx) !important; border: 1px solid var(--line) !important; border-radius: 6px !important; font-size: 12.5px !important; box-shadow: none !important; } input:focus, textarea:focus, select:focus { border-color: var(--ac) !important; outline: none !important; } .Button, .btn, .Btn, input[type=submit] { background: var(--bg3) !important; color: var(--tx2) !important; border: 1px solid var(--line2) !important; border-radius: 6px !important; font: 500 12px Geist, sans-serif !important; padding: 5px 11px !important; text-shadow: none !important; box-shadow: none !important; text-decoration: none !important; } .Button:hover { color: var(--tx) !important; } .Button.Primary, .btn-primary, .BigButton, .Button.Primary a, .BigButton a, a.Button.Primary, a.BigButton, .btn-primary a { background: var(--ac) !important; color: #0b0d0f !important; border-color: transparent !important; } .Button.Primary:hover, .BigButton:hover { filter: brightness(1.08); } /* flyouts / dropdown menus */ .Flyout, .MenuItems { background: var(--bg2) !important; border: 1px solid var(--line2) !important; border-radius: 8px !important; box-shadow: 0 8px 24px var(--shadow) !important; z-index: 1300 !important; } .Flyout a, .MenuItems a { color: var(--tx2) !important; } .Flyout a:hover, .MenuItems a:hover { background: var(--bg3) !important; color: var(--tx) !important; } /* ============================ monitor banner ============================ */ #let-user-monitor-banner { display: flex !important; align-items: center !important; background: var(--bg2) !important; padding: 4px 14px !important; font-size: 11px !important; color: var(--tx3) !important; border-top: 1px solid var(--line) !important; gap: 8px !important; white-space: nowrap !important; overflow: visible !important; } #let-user-monitor-banner.let-um-banner-multi { align-items: center !important; flex-wrap: nowrap !important; } #let-user-monitor-banner .let-um-controls { display: inline-flex !important; align-items: center !important; gap: 6px !important; flex-shrink: 0 !important; } #let-user-monitor-banner .let-um-posts { display: flex !important; flex: 1 1 auto !important; min-width: 0 !important; flex-direction: column !important; gap: 1px !important; } #let-user-monitor-banner .let-um-post-row { display: flex !important; align-items: center !important; gap: 6px !important; min-width: 0 !important; line-height: 17px !important; } #let-user-monitor-banner .let-um-label { font-family: 'Geist Mono', monospace !important; font-size: 9.5px !important; text-transform: uppercase !important; letter-spacing: .08em !important; color: var(--tx3) !important; flex-shrink: 0 !important; } #let-user-monitor-banner .let-um-label a { color: var(--ac) !important; font-weight: 600 !important; } #let-user-monitor-banner .let-um-title { color: var(--tx2) !important; overflow: hidden !important; text-overflow: ellipsis !important; flex: 1 1 auto !important; min-width: 0 !important; } #let-user-monitor-banner .let-um-title a { color: var(--tx2) !important; } #let-user-monitor-banner .let-um-title a:hover { color: var(--tx) !important; } #let-user-monitor-banner .let-um-toggle { display: flex !important; align-items: center !important; gap: 5px !important; flex-shrink: 0 !important; cursor: pointer !important; color: var(--tx3) !important; font-size: 10px !important; } #let-user-monitor-banner .let-um-switch, .let-um-switch { position: relative !important; width: 24px !important; height: 13px !important; background: var(--line2) !important; border-radius: 99px !important; transition: background .2s !important; cursor: pointer !important; flex: none !important; } #let-user-monitor-banner .let-um-switch.on, .let-um-switch.on { background: var(--ac) !important; } #let-user-monitor-banner .let-um-switch::after, .let-um-switch::after { content: '' !important; position: absolute !important; top: 2px !important; left: 2px !important; width: 9px !important; height: 9px !important; background: var(--bg2) !important; border-radius: 50% !important; transition: left .2s !important; } #let-user-monitor-banner .let-um-switch.on::after, .let-um-switch.on::after { left: 13px !important; } #let-user-monitor-banner .let-um-interval, .let-um-interval { width: 32px !important; background: var(--bg3) !important; border: 1px solid var(--line) !important; color: var(--tx2) !important; font-family: 'Geist Mono', monospace !important; font-size: 10px !important; padding: 0 3px !important; text-align: center !important; border-radius: 5px !important; height: 16px !important; line-height: 16px !important; } #let-user-monitor-banner .let-um-interval:focus { border-color: var(--ac) !important; outline: none !important; } #let-user-monitor-banner .let-um-unit, .let-um-unit { color: var(--tx3) !important; font-size: 10px !important; } #let-user-monitor-banner .let-um-divider, .let-um-divider { width: 1px !important; height: 11px !important; background: var(--line2) !important; margin: 0 3px !important; flex-shrink: 0 !important; } #let-user-monitor-banner .let-um-updated { font-family: 'Geist Mono', monospace !important; color: var(--tx3) !important; font-size: 10px !important; flex-shrink: 0 !important; } #let-user-monitor-banner .let-um-refresh { color: var(--ac) !important; font-size: 10px !important; flex-shrink: 0 !important; } /* toast */ .let-um-toast { position: fixed !important; top: 66px !important; right: 18px !important; background: var(--bg2) !important; border: 1px solid var(--line2) !important; border-left: 3px solid var(--ac) !important; border-radius: 9px !important; padding: 12px 34px 12px 14px !important; max-width: 340px !important; z-index: 100001 !important; box-shadow: 0 10px 30px var(--shadow) !important; font-size: 12px !important; color: var(--tx2) !important; animation: let-toast-in .25s ease-out !important; cursor: pointer !important; } .let-um-toast:hover { border-color: var(--ac) !important; } .let-um-toast-label { font-family: 'Geist Mono', monospace !important; color: var(--ac) !important; font-weight: 600 !important; font-size: 9.5px !important; text-transform: uppercase !important; letter-spacing: .08em !important; margin-bottom: 5px !important; display: block !important; } .let-um-toast-title { color: var(--tx) !important; font-weight: 600 !important; display: block !important; margin-bottom: 3px !important; line-height: 1.4 !important; } .let-um-toast-title a { color: var(--tx) !important; text-decoration: none !important; } .let-um-toast-title a:hover { color: var(--ac) !important; } .let-um-toast-time { font-family: 'Geist Mono', monospace !important; color: var(--tx3) !important; font-size: 10px !important; } .let-um-toast-close { position: absolute !important; top: 6px !important; right: 8px !important; color: var(--tx3) !important; cursor: pointer !important; font-size: 14px !important; background: none !important; border: none !important; padding: 2px !important; } .let-um-toast-close:hover { color: var(--tx) !important; } @keyframes let-toast-in { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } } /* ============================ custom flyouts ============================ */ .let-flyout-wrap { position: relative !important; display: inline-flex !important; align-items: center !important; } .let-flyout { display: none; position: absolute; top: calc(100% + 6px); left: 0; width: 340px; max-height: 420px; overflow-y: auto; background: var(--bg2) !important; border: 1px solid var(--line2) !important; border-radius: 9px !important; box-shadow: 0 10px 30px var(--shadow) !important; z-index: 10001 !important; font-size: 12px !important; color: var(--tx2) !important; padding: 0 !important; /* The banner sets nowrap and it inherits down into the flyouts. */ white-space: normal !important; } .let-flyout.let-flyout-open { display: block !important; } /* The MeBox sits hard against the right edge, so a left-anchored panel hangs off screen. Anchor these to the right of their icon and cap the width so they stay inside the viewport at any window size. */ #let-head-mebox .let-flyout { left: auto !important; right: 0 !important; max-width: calc(100vw - 24px) !important; } .let-flyout a { color: var(--tx2) !important; font-size: 12px !important; } .let-flyout a:hover { color: var(--tx) !important; } .let-flyout .Item, .let-flyout li { padding: 8px 12px !important; border: none !important; border-bottom: 1px solid var(--line) !important; list-style: none !important; background: transparent !important; } .let-flyout .Item:hover, .let-flyout li:hover { background: var(--bg3) !important; } .let-flyout .PhotoWrap img { width: 28px !important; height: 28px !important; border-radius: 50% !important; } .let-flyout-footer { padding: 9px 12px !important; text-align: center !important; border-top: 1px solid var(--line) !important; } .let-flyout-footer a { color: var(--ac) !important; } .let-flyout-loading { padding: 18px !important; text-align: center !important; color: var(--tx3) !important; } /* ============================ settings flyout ============================ */ #let-user-monitor-banner .let-flyout-wrap { margin-left: auto !important; } .let-settings-link { font-family: 'Geist Mono', monospace !important; color: var(--tx3) !important; font-size: 9.5px !important; text-transform: uppercase !important; letter-spacing: .08em !important; cursor: pointer !important; text-decoration: none !important; flex-shrink: 0 !important; } .let-settings-link:hover { color: var(--ac) !important; } .let-settings-flyout { width: 280px !important; padding: 14px !important; max-height: 75vh !important; overflow-x: hidden !important; z-index: 100000 !important; left: auto !important; right: 0 !important; } .let-settings-flyout * { box-sizing: border-box !important; max-width: 100% !important; } .let-settings-flyout label { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: 8px !important; color: var(--tx2) !important; font-size: 11.5px !important; margin-bottom: 9px !important; } .let-settings-flyout .let-sec { display: block !important; font-family: 'Geist Mono', monospace !important; font-size: 9px !important; text-transform: uppercase !important; letter-spacing: .1em !important; color: var(--tx3) !important; margin: 0 0 7px !important; } .let-settings-flyout hr { border: none !important; border-top: 1px solid var(--line) !important; margin: 12px 0 !important; } .let-settings-flyout input[type="range"] { width: 96px !important; margin: 0 !important; accent-color: var(--ac) !important; background: none !important; border: none !important; padding: 0 !important; } .let-settings-flyout input[type="checkbox"] { accent-color: var(--ac) !important; width: 13px !important; height: 13px !important; margin: 0 !important; padding: 0 !important; background: none !important; border: none !important; flex: none !important; } .let-settings-flyout .let-size-val { font-family: 'Geist Mono', monospace !important; color: var(--tx2) !important; font-size: 10.5px !important; min-width: 32px !important; text-align: right !important; } .let-settings-flyout .let-inp { width: 100% !important; padding: 5px 8px !important; background: var(--bg3) !important; color: var(--tx) !important; border: 1px solid var(--line) !important; border-radius: 6px !important; font-size: 11.5px !important; margin-bottom: 8px !important; box-sizing: border-box !important; } .let-settings-flyout select.let-inp { cursor: pointer !important; } .let-settings-flyout .let-addrow { display: flex !important; gap: 5px !important; margin-bottom: 8px !important; } .let-settings-flyout .let-addrow .let-inp { margin-bottom: 0 !important; } .let-settings-flyout .let-addbtn { margin: 0 !important; flex-shrink: 0 !important; padding: 5px 11px !important; background: var(--bg3) !important; color: var(--tx2) !important; border: 1px solid var(--line2) !important; border-radius: 6px !important; font-size: 11px !important; cursor: pointer !important; } .let-settings-flyout .let-addbtn:hover { color: var(--tx) !important; border-color: var(--ac) !important; } .let-settings-flyout .let-settings-reset { display: block !important; margin-top: 8px !important; padding: 6px 10px !important; background: var(--bg3) !important; border: 1px solid var(--line2) !important; color: var(--tx2) !important; font-size: 11px !important; border-radius: 6px !important; cursor: pointer !important; text-align: center !important; width: 100% !important; } .let-settings-flyout .let-settings-reset:hover { color: var(--tx) !important; border-color: var(--ac) !important; } .let-settings-flyout .let-swatches { display: flex !important; gap: 7px !important; } .let-settings-flyout .let-swatches .letm-sw { width: 18px; height: 18px; } /* user lists in settings */ .let-ignored-list, .let-monitored-list { max-height: 118px; overflow-y: auto; margin: 0 0 8px 0; padding: 0; list-style: none; border: 1px solid var(--line); border-radius: 6px; } .let-ignored-list li, .let-monitored-list li { display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; font-size: 11.5px; color: var(--tx2); background: transparent; border-bottom: 1px solid var(--line); } .let-ignored-list li:last-child, .let-monitored-list li:last-child { border-bottom: none; } .let-ignored-list .let-unignore-btn, .let-monitored-list .let-unmonitor-btn { cursor: pointer; color: var(--tx3); font-size: 11px; background: none; border: none; padding: 0 3px; } .let-ignored-list .let-unignore-btn:hover, .let-monitored-list .let-unmonitor-btn:hover { color: #e06c75; } /* ignore link in the comment meta bar */ .let-ignore-wrap { display: inline !important; } .let-ignore-link { cursor: pointer !important; color: var(--tx3) !important; font-size: 11px !important; } .let-ignore-link:hover { color: #e06c75 !important; } .let-ignored-post { opacity: .75 !important; } .let-ignored-post .Message { color: var(--tx3) !important; font-style: italic !important; } /* ============================ reading pane ============================ */ #let-reading-pane { display: none; order: 3 !important; height: calc(100vh - var(--letm-chrome)) !important; position: sticky !important; top: calc(var(--letm-chrome) + 12px) !important; background: var(--bg) !important; border: 1px solid var(--line2) !important; border-radius: 10px !important; box-sizing: border-box !important; overflow-y: auto !important; overflow-x: hidden !important; } .let-reading-content { padding: 12px !important; color: var(--tx) !important; } .let-reading-content img { max-width: 100% !important; height: auto !important; } #let-reading-pane-placeholder { display: flex !important; align-items: center !important; justify-content: center !important; height: 100% !important; color: var(--tx3) !important; font-size: 13px !important; } li.let-reading-active { background: var(--bg2) !important; box-shadow: inset 2px 0 0 var(--ac) !important; } /* ============================ post counter ============================ */ #let-post-counter { position: fixed !important; bottom: 0 !important; left: 0 !important; right: 0 !important; z-index: 99999 !important; background: var(--bg2) !important; color: var(--tx2) !important; font-family: Geist, sans-serif !important; font-size: 12px !important; padding: 7px 16px !important; text-align: center !important; border-top: 1px solid var(--line2) !important; box-shadow: 0 -2px 12px var(--shadow) !important; } #let-post-counter strong { color: var(--tx) !important; font-weight: 600 !important; } #let-post-counter .let-pc-accent { color: var(--ac) !important; } #let-post-counter .let-pc-full { color: var(--ac) !important; } #let-post-counter .let-pc-close { position: absolute !important; right: 12px !important; top: 50% !important; transform: translateY(-50%) !important; cursor: pointer !important; padding: 4px 8px !important; color: var(--tx3) !important; font-size: 13px !important; } #let-post-counter .let-pc-close:hover { color: var(--tx) !important; } /* ==================== Vanilla's own bottom-left toasts ==================== */ /* "X mentioned you in ..." lands in .InformMessages at z-index 200, which puts it under the post-counter bar (99999) and the drawer scrim — only a sliver showed above the bar. Lift it over our fixed chrome and, when the counter is up, step it above rather than on top of it. */ .InformMessages { z-index: 100002 !important; left: 14px !important; bottom: 14px !important; } body:has(#let-post-counter) .InformMessages { bottom: 52px !important; } .InformMessages .InformMessage { background: var(--bg2) !important; color: var(--tx2) !important; border: 1px solid var(--line2) !important; border-left: 3px solid var(--ac) !important; border-radius: 9px !important; box-shadow: 0 10px 30px var(--shadow) !important; font: 400 12px Geist, system-ui, sans-serif !important; padding: 10px 14px !important; margin: 6px 0 0 !important; } .InformMessages .InformMessage a { color: var(--ac) !important; } .InformMessages .InformMessage strong, .InformMessages .InformMessage b { color: var(--tx) !important; } /* ============================ floating controls ============================ */ /* Bottom-right: LowEndTalk drops its own notification popups bottom-left. */ .letm-ctl { position: fixed; bottom: 14px; right: 14px; z-index: 1350; display: flex; gap: 6px; transition: transform .22s ease; } /* Slide out of the way when the sidebar drawer takes the right edge. */ html.letm-drawer-open .letm-ctl { transform: translateX(-306px); } /* Lift clear of the post-counter bar when it is showing. */ body:has(#let-post-counter) .letm-ctl { bottom: 48px; } .letm-btn { width: 30px; height: 30px; display: inline-grid; place-items: center; background: var(--bg3) !important; border: 1px solid var(--line2) !important; border-radius: 8px; color: var(--tx2) !important; font-size: 13px; cursor: pointer; line-height: 1; box-shadow: 0 2px 10px var(--shadow); padding: 0 !important; } .letm-btn:hover { color: var(--tx) !important; border-color: var(--ac) !important; } /* accent swatches (Userscript Settings › Appearance) */ .letm-sw { width: 22px; height: 22px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; padding: 0; flex: none; } .letm-sw.on { border-color: var(--tx) !important; } /* ============================ footer ============================ */ #Foot { background: var(--bg2) !important; border-top: 1px solid var(--line) !important; color: var(--tx3) !important; padding: 0 !important; margin: 0 !important; min-height: 0 !important; line-height: 1 !important; } #Foot div { background: transparent !important; color: var(--tx3) !important; border: none !important; margin: 0 !important; padding: 6px 14px !important; display: flex !important; align-items: center !important; min-height: 0 !important; width: 100% !important; } #Foot a { color: var(--tx3) !important; } #Foot a:hover { color: var(--tx) !important; } .let-foot-left, .let-foot-center, .let-foot-right { flex: 1 !important; font-size: 11px !important; } .let-foot-left { text-align: left !important; } .let-foot-center { text-align: center !important; } .let-foot-center a { margin: 0 6px !important; } .let-foot-right { text-align: right !important; } /* ============================ compact density ============================ */ /* Opt-in via Userscript Settings › Appearance. Squeezes vertical rhythm only — column positions, font sizes and alignment are left alone. */ html.letm-compact #Body > .Row { padding: 8px 10px 10px !important; gap: 8px !important; } /* discussion rows */ html.letm-compact ul.DataList.Discussions > li.ItemDiscussion { min-height: 0 !important; padding: 1px 12px !important; gap: 8px !important; } html.letm-compact .let-title a, html.letm-compact .let-meta { line-height: 1.35 !important; } html.letm-compact .let-avatar, html.letm-compact .let-avatar img { width: 15px !important; height: 15px !important; } html.letm-compact .letm-colhead { padding: 2px 12px !important; gap: 8px !important; } /* chrome around the list */ html.letm-compact .PageControls { padding: 3px 12px !important; } html.letm-compact div.Tabs, html.letm-compact div.Headings, html.letm-compact .DiscussionTabs { padding: 3px 12px !important; } html.letm-compact #let-user-monitor-banner { padding: 2px 12px !important; } html.letm-compact ul.DataList.CategoryList > li.Item { padding: 6px 12px !important; } html.letm-compact .CategoryHeading, html.letm-compact h1.H, html.letm-compact h2.H, html.letm-compact .PageTitle h1 { padding: 9px 12px 6px !important; } html.letm-compact h2.CommentHeading { padding: 2px 12px 5px !important; } /* thread view */ html.letm-compact ul.DataList.Comments > li.ItemComment, html.letm-compact .MessageList.Discussion .ItemDiscussion { margin: 0 10px 5px !important; padding: 7px 11px !important; } html.letm-compact ul.DataList.Comments { padding: 8px 0 2px !important; } html.letm-compact .MessageList.Discussion { padding: 8px 0 0 !important; } html.letm-compact .Item-Header.letm-hdr { padding: 0 0 4px !important; gap: 8px !important; } html.letm-compact .letm-hdr > a.PhotoWrap, html.letm-compact .letm-hdr > a.PhotoWrap img { width: 26px !important; height: 26px !important; } html.letm-compact .Message { line-height: 1.45 !important; } html.letm-compact .Message p { margin: 0 0 6px !important; } html.letm-compact .Message p:last-child { margin-bottom: 0 !important; } html.letm-compact blockquote, html.letm-compact .Message blockquote, html.letm-compact .UserQuote { padding: 5px 9px !important; margin: 0 0 5px !important; } html.letm-compact .CommentForm, html.letm-compact .MessageForm, html.letm-compact .DiscussionForm { margin: 6px 10px 10px !important; padding: 9px 11px !important; } /* sidebar */ html.letm-compact .Column.PanelColumn .PanelInfo li { padding: 1px 0 !important; } html.letm-compact .Column.PanelColumn .Box, html.letm-compact .Column.PanelColumn .PanelInfo, html.letm-compact .Column.PanelColumn .BoxButtons { margin-bottom: 10px !important; } /* ============================ responsive ============================ */ @media (max-width: 1360px) { .let-meta { grid-template-columns: max-content 106px 46px 52px 68px 96px 66px; } } @media (max-width: 1150px) { .let-meta { grid-template-columns: max-content 46px 52px 68px 96px 66px; } .let-meta .let-cat { display: none !important; } .let-meta .let-comments { grid-column: 2; } .let-meta .let-views { grid-column: 3; } .let-meta .let-new { grid-column: 4; } .let-meta .let-recent { grid-column: 5; } .let-meta .let-date { grid-column: 6; } } @media (max-width: 1000px) { #Body > .Row { flex-direction: column !important; } .Column.PanelColumn { width: 100% !important; min-width: 0 !important; max-width: none !important; } html.letm-drawer .Column.PanelColumn { width: 300px !important; max-width: 300px !important; } } @media (max-width: 820px) { #Head .SiteSearch input.InputBox { width: 120px !important; } /* rows stop being a grid down here, so the labels would line up with nothing */ .letm-colhead { display: none !important; } ul.DataList.Discussions > li.ItemDiscussion { flex-wrap: wrap !important; } .let-title { flex: 1 1 100% !important; min-width: 0 !important; } .let-meta { display: flex !important; flex-wrap: wrap !important; gap: 4px 10px !important; grid-template-columns: none !important; } .let-meta > * { grid-column: auto !important; } .let-meta .let-cat { display: inline-flex !important; } } `; document.documentElement.appendChild(style); // Re-append dynamic style AFTER main style so it wins the cascade document.documentElement.appendChild(dynStyle); applyDynamicStyles(); // --- Graphite chrome: wordmark, drawer, floating theme controls --------- var letmSyncSettingsUI = function () {}; // replaced once the flyout exists function setTheme(next) { letSettings.theme = next === "light" ? "light" : "dark"; applyTheme(); saveSettings(letSettings); letmSyncSettingsUI(); } function setAccent(color) { letSettings.accent = color; applyTheme(); saveSettings(letSettings); letmSyncSettingsUI(); } function toggleDrawer(force) { var root = document.documentElement; if (!root.classList.contains("letm-drawer")) return; root.classList.toggle( "letm-drawer-open", typeof force === "boolean" ? force : undefined, ); } // Sticky chrome height (header + monitor banner) drives the reading pane // and sidebar heights; remeasure whenever the banner reflows. function measureChrome() { var head = document.getElementById("Head"); if (!head) return; document.documentElement.style.setProperty( "--letm-chrome", head.offsetHeight + "px", ); } function buildGraphiteChrome() { // Wordmark: a small accent square ahead of the site title. var titleLink = document.querySelector("#Head .SiteTitle a"); if (titleLink && !titleLink.querySelector(".letm-logomark")) { var mark = document.createElement("span"); mark.className = "letm-logomark"; titleLink.insertBefore(mark, titleLink.firstChild); } // Scrim behind the slide-over sidebar. var scrim = document.createElement("div"); scrim.className = "letm-scrim"; scrim.addEventListener("click", function () { toggleDrawer(false); }); document.body.appendChild(scrim); // Floating control: sidebar drawer only. Theme and accent live in the // Userscript Settings flyout — no need to duplicate them out here. var ctl = document.createElement("div"); ctl.className = "letm-ctl"; document.body.appendChild(ctl); var drawerBtn = document.createElement("button"); drawerBtn.className = "letm-btn letm-drawerbtn"; drawerBtn.type = "button"; drawerBtn.textContent = "⧉"; drawerBtn.title = "Sidebar"; drawerBtn.addEventListener("click", function () { toggleDrawer(); }); ctl.appendChild(drawerBtn); document.addEventListener("keydown", function (e) { if (e.key === "Escape") toggleDrawer(false); // '/' jumps to search, the way every modern forum does it. if ( e.key === "/" && !/^(INPUT|TEXTAREA|SELECT)$/.test( (document.activeElement || {}).tagName || "", ) && !(document.activeElement || {}).isContentEditable ) { var box = document.querySelector( "#Head .SiteSearch input.InputBox, .SiteSearch input[type=text], input#Form_Search", ); if (box) { e.preventDefault(); box.focus(); box.select(); } } }); measureChrome(); window.addEventListener("resize", measureChrome); var head = document.getElementById("Head"); if (head && window.ResizeObserver) { new ResizeObserver(measureChrome).observe(head); } } // Post headers ship as one row: [avatar + name + role] ......... [meta]. // Rebuild them as avatar | (name + role) over (date, flag, thanks, ignore) // so the meta reads as a second line under the username instead of drifting // off to the right edge. Same markup on the OP and on every comment. function restructureCommentHeaders(root) { (root || document) .querySelectorAll(".Item-Header") .forEach(function (hdr) { if (hdr.classList.contains("letm-hdr")) return; var authorWrap = hdr.querySelector(":scope > .AuthorWrap"); if (!authorWrap) return; var meta = hdr.querySelector(":scope > .Meta"); var photo = authorWrap.querySelector("a.PhotoWrap"); var col = document.createElement("div"); col.className = "letm-hdrcol"; hdr.insertBefore(col, authorWrap); col.appendChild(authorWrap); if (meta) col.appendChild(meta); // Avatar leaves the name row so it can sit beside both lines. if (photo) hdr.insertBefore(photo, col); hdr.classList.add("letm-hdr"); }); } // Colour dot for a /categories/ link. function letmCategoryDot(href) { var slug = String(href || "").split("/categories/")[1]; slug = slug ? slug.split(/[/?#]/)[0] : ""; var dot = document.createElement("span"); dot.className = "letm-dot"; dot.style.background = LETM_CAT_COLORS[slug] || "#8b93a1"; return dot; } document.addEventListener("DOMContentLoaded", function () { if (!IS_IFRAME) { buildGraphiteChrome(); // Move MeBox icons from sidebar into header bar const headRow = document.querySelector("#Head .Row"); if (headRow && !document.getElementById("let-head-mebox")) { // Try multiple selectors to find the sidebar MeBox const sidebarMeBox = document.querySelector(".Column.PanelColumn .MeBox") || document.querySelector(".Panel .MeBox") || document.querySelector(".PanelColumn .MeBox"); if (sidebarMeBox) { const container = document.createElement("span"); container.id = "let-head-mebox"; // Grab all links and photo wraps from the MeBox sidebarMeBox.querySelectorAll("a, .PhotoWrap").forEach(function (el) { container.appendChild(el.cloneNode(true)); }); // Insert into the header headRow.appendChild(container); // Hide original sidebarMeBox.style.display = "none"; // Attach custom flyout panels to notification/inbox links var flyoutMap = [ { match: /notifications/i, popin: "/profile/notificationspopin", label: "All Notifications", full: "/profile/notifications", }, { match: /messages|inbox/i, popin: "/messages/popin", label: "All Messages", full: "/messages/inbox", }, { match: /bookmarks/i, popin: "/discussions/bookmarkedpopin", label: "All Bookmarks", full: "/discussions/bookmarked", }, ]; container.querySelectorAll("a").forEach(function (link) { var href = link.getAttribute("href") || ""; var title = link.getAttribute("title") || ""; var text = link.textContent || ""; var cls = link.className || ""; var inner = link.innerHTML || ""; var matchStr = href + " " + title + " " + text + " " + cls + " " + inner; var config = null; for (var i = 0; i < flyoutMap.length; i++) { if (flyoutMap[i].match.test(matchStr)) { config = flyoutMap[i]; break; } } if (!config) return; // Wrap link in a positioned container var wrap = document.createElement("span"); wrap.className = "let-flyout-wrap"; link.parentNode.insertBefore(wrap, link); wrap.appendChild(link); // Create flyout panel inside the wrapper var flyout = document.createElement("div"); flyout.className = "let-flyout"; flyout.innerHTML = '
Loading…
'; wrap.appendChild(flyout); var loaded = false; link.addEventListener("click", function (e) { // Allow ctrl/cmd/middle click to navigate normally if (e.ctrlKey || e.metaKey || e.button === 1) return; e.preventDefault(); e.stopPropagation(); // Close other flyouts container.querySelectorAll(".let-flyout").forEach(function (f) { if (f !== flyout) f.classList.remove("let-flyout-open"); }); // Toggle this flyout var isOpen = flyout.classList.toggle("let-flyout-open"); // Fetch content on first open (and refresh each time) if (isOpen) { flyout.innerHTML = '
Loading…
'; // gdn is a page global; with @grant active the script runs // sandboxed, so reach it through unsafeWindow. var page = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; var tk = page.gdn && page.gdn.definition ? page.gdn.definition("TransientKey") : ""; var popinUrl = config.popin + (config.popin.indexOf("?") > -1 ? "&" : "?") + "DeliveryType=VIEW" + (tk ? "&TransientKey=" + encodeURIComponent(tk) : ""); fetch(popinUrl, { credentials: "same-origin", }) .then(function (r) { return r.text(); }) .then(function (html) { flyout.innerHTML = html.trim() + '"; // Restyle the title row inline to beat theme CSS var titleLi = flyout.querySelector("li.Item.Title"); if (titleLi) { var prefLink = titleLi.querySelector("a"); var heading = titleLi.querySelector("strong"); titleLi.innerHTML = ""; titleLi.style.cssText = "display:flex!important;align-items:baseline!important;justify-content:space-between!important;padding:6px 12px!important;border-bottom:1px solid #252530!important;"; if (heading) { heading.style.cssText = "font-size:12px!important;color:#ccc!important;font-weight:700!important;order:1!important;"; titleLi.appendChild(heading); } if (prefLink) { prefLink.style.cssText = "font-size:10px!important;color:#666!important;font-style:italic!important;order:2!important;text-decoration:none!important;"; titleLi.appendChild(prefLink); } } // Remove duplicate "All Notifications" from popin (we add our own footer) var centerItem = flyout.querySelector("li.Item.Center"); if (centerItem) centerItem.style.display = "none"; }) .catch(function () { flyout.innerHTML = '
Failed to load
' + '"; }); } }); }); // The MeBox clone drags the whole account dropdown along as loose // links. Fold them back behind the avatar so the header stays a // single row of icons instead of a strip of text links. var accountLinks = container.querySelectorAll("a.dropdown-menu-link"); var photo = container.querySelector("a.PhotoWrap"); if (accountLinks.length && photo) { var uwrap = document.createElement("span"); uwrap.className = "let-flyout-wrap letm-userwrap"; container.appendChild(uwrap); uwrap.appendChild(photo); var uname = container.querySelector("a.Username"); if (uname) uwrap.appendChild(uname); var umenu = document.createElement("div"); umenu.className = "let-flyout letm-usermenu"; accountLinks.forEach(function (a) { umenu.appendChild(a); }); uwrap.appendChild(umenu); photo.addEventListener("click", function (e) { if (e.ctrlKey || e.metaKey || e.button === 1) return; e.preventDefault(); e.stopPropagation(); container.querySelectorAll(".let-flyout").forEach(function (f) { if (f !== umenu) f.classList.remove("let-flyout-open"); }); umenu.classList.toggle("let-flyout-open"); }); } // Close flyouts on outside click document.addEventListener("click", function (e) { if (!container.contains(e.target)) { container.querySelectorAll(".let-flyout").forEach(function (f) { f.classList.remove("let-flyout-open"); }); } }); } } // Search lives in the header (the sidebar may be a drawer now), pushed // right so it sits next to the MeBox icons. const headRowForSearch = document.querySelector("#Head .Row"); const search = document.querySelector("#Head .SiteSearch") || document.querySelector(".Column.PanelColumn .SiteSearch"); if (headRowForSearch && search) { var meboxEl = document.getElementById("let-head-mebox"); headRowForSearch.insertBefore(search, meboxEl || null); } // Fetch monitored users' latest posts and display in banner (function () { var banner = document.createElement("div"); banner.id = "let-user-monitor-banner"; var head = document.getElementById("Head"); if (head) { head.appendChild(banner); } var intervalId = null; var tickerId = null; var autoOn = letSettings.userMonitorAutoOn; var refreshSec = letSettings.userMonitorInterval; var lastFetchTime = null; var latestPosts = []; var lastSeenByUser = (function () { storeMigrate(LET_LAST_SEEN_KEY); try { var v = storeGet(LET_LAST_SEEN_KEY); if (v) { var parsed = JSON.parse(v); if (parsed && typeof parsed === "object") return parsed; } return {}; } catch (e) { return {}; } })(); storeWatch(LET_LAST_SEEN_KEY, function (raw) { try { var parsed = JSON.parse(raw); if (parsed && typeof parsed === "object") lastSeenByUser = parsed; } catch (e) {} }); function getMonitoredUsers() { var seen = {}; return (letSettings.monitoredUsers || []) .map(function (name) { return String(name || "").trim(); }) .filter(function (name) { var key = name.toLowerCase(); if (!key || seen[key]) return false; seen[key] = true; return true; }); } function saveLastSeen() { storeSet(LET_LAST_SEEN_KEY, JSON.stringify(lastSeenByUser)); } function profilePath(kind, username) { return "/profile/" + kind + "/" + encodeURIComponent(username); } function showNewPostToast(post) { var existing = document.querySelector(".let-um-toast"); if (existing) existing.remove(); var toast = document.createElement("div"); toast.className = "let-um-toast"; toast.innerHTML = '' + 'New post from ' + escapeHtml(post.username) + "" + '' + escapeHtml(post.prefix + post.title) + "" + '' + escapeHtml(post.time) + ""; document.body.appendChild(toast); toast .querySelector(".let-um-toast-close") .addEventListener("click", function (e) { e.stopPropagation(); toast.remove(); }); toast.addEventListener("click", function () { window.location.href = post.href; }); setTimeout(function () { if (toast.parentNode) toast.remove(); }, 15000); } function sendUserPush(post) { if (!letSettings.pushEnabled) return; var fullUrl = post.href.startsWith("http") ? post.href : window.location.origin + post.href; sendPushover({ title: post.username + " — " + post.prefix + post.title, message: post.prefix + post.title + "\n" + post.time, url: fullUrl, url_title: "View Post", }); } function getInterval() { return refreshSec * 1000; } function startInterval() { clearInterval(intervalId); intervalId = setInterval(fetchLatest, getInterval()); } function startTicker() { clearInterval(tickerId); tickerId = setInterval(updateTicker, 1000); } function updateTicker() { var el = banner.querySelector(".let-um-updated"); if (!el || !lastFetchTime) return; var ago = Math.round((Date.now() - lastFetchTime) / 1000); el.textContent = "updated " + ago + "s ago"; } function formatMonitorTime(post) { if (!post || !post.datetime) return post && post.time ? post.time : ""; var d = new Date(post.datetime); if (isNaN(d.getTime())) return post.time || ""; var months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; var hour = d.getHours(); var ampm = hour >= 12 ? "PM" : "AM"; hour = hour % 12; if (hour === 0) hour = 12; var minute = String(d.getMinutes()).padStart(2, "0"); return ( months[d.getMonth()] + " " + d.getDate() + ", " + String(hour).padStart(2, "0") + ":" + minute + " " + ampm ); } function buildPostRow(post) { if (!post) { return ( '
' + 'Monitored users:' + 'no posts found' + "
" ); } return ( '
' + 'Latest from ' + escapeHtml(post.username) + ":" + '' + escapeHtml(post.prefix + post.title) + " [" + escapeHtml(formatMonitorTime(post)) + "]" + "
" ); } function buildBanner(posts) { var users = getMonitoredUsers(); var visiblePosts = (posts || []).slice( 0, users.length > 1 ? 2 : 1, ); var postHtml = visiblePosts.length ? visiblePosts.map(buildPostRow).join("") : buildPostRow(null); banner.classList.toggle( "let-um-banner-multi", users.length > 1 && visiblePosts.length > 1, ); banner.innerHTML = '
' + postHtml + "
" + '' + '' + '' + 'Monitor (s):' + '' + '' + 'Enabled: ' + '' + "" + '' + '' + (lastFetchTime ? "updated 0s ago" : "") + "" + 'update now' + ""; banner .querySelector(".let-um-switch") .addEventListener("click", toggleAuto); banner .querySelector(".let-um-refresh") .addEventListener("click", function (e) { e.preventDefault(); fetchLatest(); }); var input = banner.querySelector(".let-um-interval"); input.addEventListener("click", function (e) { e.stopPropagation(); }); input.addEventListener("change", function () { var val = parseInt(this.value, 10); if (val && val >= 30) { refreshSec = val; letSettings.userMonitorInterval = val; saveSettings(letSettings); } else { this.value = refreshSec; } if (autoOn) startInterval(); }); } function getResultAuthor(item) { // Look ONLY in .Meta for the byline. Profile links inside the // comment body — quote attributions ("@user said:") and @mentions — // would otherwise win and make us treat someone else's reply as if // it were authored by the monitored user. var meta = item.querySelector(".Meta"); if (!meta) return ""; var links = meta.querySelectorAll("a[href*='/profile/']"); for (var i = 0; i < links.length; i++) { var link = links[i]; // Skip the discussion title link (wrapped in ) and any link // inside a quote (defensive — meta shouldn't contain quotes). if (link.closest("b") || link.closest("blockquote")) continue; var href = link.getAttribute("href") || ""; var match = href.match(/\/profile\/([^?#]+)/i); if (!match) continue; var parts = match[1].split("/").filter(Boolean); if (!parts.length) continue; return decodeURIComponent(parts[parts.length - 1]).trim(); } return ""; } function parseComment(doc, username) { var items = Array.prototype.slice.call( doc.querySelectorAll("ul.DataList.SearchResults li.Item"), ); if (!items.length) return null; var item = null; for (var i = 0; i < items.length; i++) { var author = getResultAuthor(items[i]); if (author && author.toLowerCase() === username.toLowerCase()) { item = items[i]; break; } } if (!item) return null; var titleLink = item.querySelector(".Meta .MItem b a"); var timeEl = item.querySelector(".Meta .MItem:last-child a"); // Comments page: time may be a