Guest User

OWF Necrum Theme + Fixes

a guest
Jan 1st, 2026
122
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         OddworldForums Necrum Theme + Fixes
  3. // @namespace    owforums-necrum-theme
  4. // @version      1.0
  5. // @match        https://owforums.net/*
  6. // @run-at       document-start
  7. // ==/UserScript==
  8.  
  9. (function () {
  10.     'use strict';
  11.  
  12.     // Immediately hide the page and set background
  13.     const preStyle = document.createElement('style');
  14.     preStyle.id = 'pre-theme-style';
  15.     preStyle.textContent = `
  16.         html, body {
  17.             background: linear-gradient(to bottom, #171309, #000000);  /* flash color */
  18.             visibility: hidden !important;          /* hide content until ready */
  19.         }
  20.     `;
  21.     document.head.appendChild(preStyle);
  22.  
  23.     const NEW_LOGO_URL = 'https://i.imgur.com/xYhdQy1.png';
  24.     const TOP_GRADIENT = 'https://i.imgur.com/THjrmF5.png';
  25.     const GRADIENT_TCAT = 'https://i.imgur.com/f3Y762Z.png';
  26.     const TOP_BG = 'https://i.imgur.com/vRScjRf.png';
  27.     const TOP_LEFT1 = 'https://i.imgur.com/ipJ6dw2.png';
  28.     const TOP_LEFT2 = 'https://i.imgur.com/a4XaQCn.png';
  29.     const TOP_LEFT3 = 'https://i.imgur.com/e8r6tjH.png';
  30.     const BOTTOM_BG = 'https://i.imgur.com/wV2o6Cw.png';
  31.     const BOTTOM_LEFT1 = 'https://i.imgur.com/o3VxgEE.png';
  32.     const BOTTOM_LEFT2 = 'https://i.imgur.com/wiIzKG2.png';
  33.     const BOTTOM_LEFT3 = 'https://i.imgur.com/SSVz2zQ.png';
  34.     const POSTBIT_BACK2 = 'https://i.imgur.com/a1SutZV.png';
  35.     const POSTBIT_BACKDARK = 'https://i.imgur.com/vd7mwIp.png';
  36.     const NAV_ENDLEFT = 'https://i.imgur.com/7YBxofd.png';
  37.     const NAV_ENDRIGHT = 'https://i.imgur.com/CmNLk9O.png';
  38.     const REPLYBTN = 'https://i.imgur.com/BiO562l.png';
  39.     const NEWTOPIC = 'https://i.imgur.com/WLVQJ3A.png';
  40.     const COLLAPSE_TCAT = 'https://i.imgur.com/nM8CLlT.png';
  41.     const COLLAPSE_THEAD = 'https://i.imgur.com/RpfgdPA.png';
  42.     const THREADCLOSED = 'https://i.imgur.com/f8ISLg2.png';
  43.  
  44.     function trySwapImages(sourceName, targetUrl, mirrored) {
  45.         const image = document.querySelectorAll(`img[src*="${sourceName}"]`);
  46.         if (!image.length) return false;
  47.  
  48.         image.forEach(image => {
  49.             image.src = targetUrl;
  50.  
  51.             // Mirror it horizontally
  52.             if (mirrored) {
  53.                 image.style.transformOrigin = '50% 50%'; // center of the element
  54.                 image.style.transform = 'scaleX(-1)';
  55.             }
  56.         });
  57.  
  58.         console.log('[OW Theme] Image ' + sourceName + ' swapped, using original layout size');
  59.         return true;
  60.     }
  61.  
  62.     function swapBackgroundImgs(sourceName, targetUrl, prefix) {
  63.         const tds = document.querySelectorAll(prefix + `[background*="${sourceName}"]`);
  64.         if (!tds.length) return false;
  65.  
  66.         tds.forEach(td => {
  67.             // Option 1: replace the HTML attribute directly
  68.             td.setAttribute('background', targetUrl);
  69.  
  70.             // Option 2: also set inline style to be safe
  71.             td.style.backgroundImage = `url(${targetUrl})`;
  72.         });
  73.  
  74.         console.log(`[OW Theme] Swapped ${tds.length} background gif(s)`);
  75.         return true;
  76.     }
  77.  
  78.     function trySwapLogo() {
  79.         return trySwapImages("native_05.jpg", NEW_LOGO_URL);
  80.     }
  81.  
  82.     function trySwapGradients() {
  83.         swapBackgroundImgs("top-bg.gif", TOP_BG, 'td');
  84.  
  85.         trySwapImages("top-left1.gif", TOP_LEFT1);
  86.         trySwapImages("top-left2.gif", TOP_LEFT2);
  87.         trySwapImages("top-left3.gif", TOP_LEFT3);
  88.         trySwapImages("top-right1.gif", TOP_LEFT1, true);
  89.         trySwapImages("top-right2.gif", TOP_LEFT2, true);
  90.         trySwapImages("top-right3.gif", TOP_LEFT3, true);
  91.  
  92.         swapBackgroundImgs("bottom-bg.gif", BOTTOM_BG, 'td');
  93.         trySwapImages("bottom-left1.gif", BOTTOM_LEFT1);
  94.         trySwapImages("bottom-left2.gif", BOTTOM_LEFT2);
  95.         trySwapImages("bottom-left3.gif", BOTTOM_LEFT3);
  96.  
  97.         trySwapImages("bottom-right1.gif", BOTTOM_LEFT1, true);
  98.         trySwapImages("bottom-right2.gif", BOTTOM_LEFT2, true);
  99.         trySwapImages("bottom-right3.gif", BOTTOM_LEFT3, true);
  100.  
  101.         const images = document.querySelectorAll(
  102.             `img[src*="${BOTTOM_LEFT3}"], img[src*="${BOTTOM_LEFT2}"]`
  103.         );
  104.  
  105.         images.forEach(image => {
  106.             if (image.style.transform.includes('scaleX(-1)')) {
  107.                 image.style.transform = 'scaleX(-1) translateX(1px)';
  108.             }
  109.         });
  110.  
  111.         trySwapImages("nav_endleft.gif", NAV_ENDLEFT);
  112.         trySwapImages("nav_endright.gif", NAV_ENDRIGHT);
  113.         swapBackgroundImgs("postbit-back2.gif", POSTBIT_BACK2, 'table');
  114.  
  115.         trySwapImages("collapse_tcat.gif", COLLAPSE_TCAT);
  116.         trySwapImages("collapse_tcat_collapsed.gif", BOTTOM_LEFT1); //todo fix - doesn't work
  117.  
  118.         trySwapImages("collapse_thead.gif", COLLAPSE_THEAD);
  119.         trySwapImages("reply.gif", REPLYBTN);
  120.         trySwapImages("threadclosed.gif", THREADCLOSED);
  121.         trySwapImages("newthread.gif", NEWTOPIC);
  122.  
  123.         return true;
  124.     }
  125.  
  126.     function removeHeaderRows() {
  127.         const headerLogo = document.querySelector(
  128.             'img[src*="native_05.jpg"]'
  129.         );
  130.         if (!headerLogo) return false;
  131.  
  132.         const tbody = headerLogo.closest('tbody');
  133.         if (!tbody) return false;
  134.  
  135.         const rows = Array.from(tbody.querySelectorAll('tr'));
  136.         if (rows.length < 4) return false;
  137.  
  138.         const logoRowIndex = rows.findIndex(row =>
  139.             row.contains(headerLogo)
  140.         );
  141.  
  142.         if (logoRowIndex === -1) return false;
  143.  
  144.         // Remove the row ABOVE the logo row
  145.         if (rows[logoRowIndex - 1]) {
  146.             rows[logoRowIndex - 1].remove();
  147.         }
  148.  
  149.         // Remove the row BELOW the logo row
  150.         if (rows[logoRowIndex + 1]) {
  151.             rows[logoRowIndex + 1].remove();
  152.         }
  153.  
  154.         console.log('[OW Theme] Header rows removed');
  155.         return true;
  156.     }
  157.  
  158.     // Expand page container width
  159.     function expandPageWidth() {
  160.         const pageDiv = document.querySelector('div.page');
  161.         if (!pageDiv) return false;
  162.  
  163.         pageDiv.style.width = '870px';
  164.         console.log('[OW Theme] Page width set to 870px');
  165.         return true;
  166.     }
  167.  
  168.     function expandCenteredTable() {
  169.         // Target the table by width + alignment (unique enough)
  170.         const table = document.querySelector('table[width="796"][align="center"]');
  171.         if (!table) return false;
  172.  
  173.         // Set new width
  174.         table.style.width = '870px';
  175.  
  176.         // Optional: adjust left/right border if you want (keep black)
  177.         table.style.borderLeft = '1px solid #000000';
  178.         table.style.borderRight = '1px solid #000000';
  179.  
  180.         console.log('[OW Theme] Centered table width set to 870px');
  181.         return true;
  182.     }
  183.  
  184.     function expandPostsTable() {
  185.         // Select all divs with class "page"
  186.         const tables = document.querySelectorAll('div.page');
  187.  
  188.         tables.forEach(table => {
  189.             // Only target divs with inline width of 794px
  190.             if (table.style.width === '794px') {
  191.                 // Set new width
  192.                 table.style.width = '870px';
  193.  
  194.                 // Optional: adjust left/right border if you want (keep black)
  195.                 table.style.borderLeft = '1px solid #000000';
  196.                 table.style.borderRight = '1px solid #000000';
  197.  
  198.                 console.log('[OW Theme] Centered table width set to 870px for one div.page');
  199.             }
  200.         });
  201.     }
  202.  
  203.     function recolorTables() {
  204.         return true;
  205.         // All tables in the page
  206.         document.querySelectorAll('table, td, tr').forEach(el => {
  207.             if (el.hasAttribute('background') || el.style.background || el.style.backgroundImage) {
  208.                 el.style.background = '#6a5931';
  209.                 el.style.backgroundImage = 'none';
  210.             }
  211.         });
  212.         console.log('[OW Theme] Table colors updated');
  213.     }
  214.  
  215.     function addStyle(css) {
  216.         const style = document.createElement('style');
  217.         style.textContent = css;
  218.         document.head.appendChild(style);
  219.     }
  220.  
  221.  
  222.     const COLOR_THREAD_LIGHT = "#413920";
  223.     const COLOR_THREAD_DARK = "#3a321b";
  224.     const COLOR_BODY_DARK = "#251e0a";
  225.  
  226.     function injectThemeCSS() {
  227.         // Only inject if NOT in /archive/
  228.         const hostname = window.location.hostname;
  229.         const path = window.location.pathname;
  230.  
  231.         if (hostname.includes('owforums.net') && path.startsWith('/rules.php')) {
  232.             addStyle(`
  233.             /* Force page background to be visible */
  234.             body .page,
  235.  
  236.             page {
  237.             background: `+ COLOR_BODY_DARK + ` !important;;
  238.             }
  239.  
  240.            
  241.             body {
  242.                 /* Vertical gradient from dark gray at top to black at bottom */
  243.                 background: linear-gradient(to bottom, #171309, #000000);
  244.                 background-color: #FFFFFF;
  245.                 /* Ensure gradient covers full height */
  246.                 min-height: 100vh;
  247.             }
  248.  
  249.             body > div {
  250.                 background-color: `+ COLOR_BODY_DARK + `!important; /* replace white with dark background */
  251.  
  252.             }
  253.            
  254.             `);
  255.         } else if (!(hostname.includes('owforums.net') && (path.startsWith('/archive/') || path.startsWith('/printthread')))) {
  256.             addStyle(`
  257.             /* Force page background to be visible */
  258.             body .page,
  259.  
  260.             page {
  261.             background: `+ COLOR_BODY_DARK + `;
  262.             }
  263.  
  264.  
  265.             /* alt1 */
  266.             td.alt1,
  267.             td.alt1Active {
  268.                 background-color: ` + COLOR_THREAD_DARK + ` !important;
  269.                 color: #B7B7B7 !important;
  270.             }
  271.  
  272.             /* alt2 */
  273.             td.alt2,
  274.             td.alt2Active {
  275.                 background-color: ` + COLOR_THREAD_LIGHT + ` !important;
  276.                 color: #B7B7B7 !important;
  277.             }
  278.  
  279.             .tcat
  280.             {
  281.                 background: #3E453C url(` + GRADIENT_TCAT + `);
  282.                 background-size: auto 100%;
  283.             }
  284.             .vbmenu_control
  285.             {
  286.                 background: #2F362D url(` + GRADIENT_TCAT + `);
  287.                                 background-size: auto 100%;
  288.             }
  289.  
  290.             .vbmenu_option
  291.             {
  292.                 background: #31382F url(` + POSTBIT_BACKDARK + `);
  293.             }
  294.  
  295.             .vbmenu_hilite
  296.             {
  297.                 background: #31382F url(` + COLOR_THREAD_LIGHT + `);
  298.             }
  299.  
  300.             .thead
  301.             {
  302.                 background: none !important; /* removes the image */
  303.                 background-color: ` + COLOR_THREAD_DARK + ` !important;
  304.             }
  305.  
  306.             .tfoot
  307.             {
  308.                 background: #31382F url(` + POSTBIT_BACKDARK + `);
  309.             }
  310.            
  311.             .div.tborder
  312.             {
  313.                 background: #31382F url(` + POSTBIT_BACKDARK + `);
  314.             }
  315.            
  316.             body {
  317.                 /* Vertical gradient from dark gray at top to black at bottom */
  318.                 background: linear-gradient(to bottom, #171309, #000000);
  319.                
  320.                 /* Ensure gradient covers full height */
  321.                 min-height: 100vh;
  322.             }
  323.  
  324.             .info2 {
  325.             background-color: ` + COLOR_THREAD_DARK + `;
  326.             } /* user info top right of their post */
  327.            
  328.            
  329.             .alt1, .alt1Active
  330.             {
  331.                 background: ` + COLOR_THREAD_LIGHT + `;
  332.             }
  333.             .alt2, .alt2Active
  334.             {
  335.                 background: ` + COLOR_THREAD_DARK + `;
  336.             }
  337.  
  338.             #content_container {
  339.                 background: `+ COLOR_BODY_DARK + `;
  340.             }
  341.  
  342.             #sidebar_container {
  343.                 background: `+ COLOR_BODY_DARK + `;
  344.             }
  345.  
  346.  
  347.             #content {
  348.                 background: `+ COLOR_BODY_DARK + `;
  349.             }
  350.  
  351.             .panelsurround {
  352.                 background: `+ COLOR_THREAD_DARK + `;
  353.             }
  354.             .panel {
  355.                 background: `+ COLOR_THREAD_LIGHT + `;
  356.             }
  357.  
  358.             .wysiwyg {
  359.                 background: `+ COLOR_THREAD_LIGHT + `; !important
  360.             };
  361.  
  362.             #wysiwyg {
  363.                 background: `+ COLOR_THREAD_LIGHT + `; !important
  364.             };
  365.  
  366.             `);
  367.         }
  368.     }
  369.  
  370.     function stripInlineBackgrounds() {
  371.         document.querySelectorAll('[style]').forEach(el => {
  372.             if (el.style.backgroundColor || el.style.background) {
  373.                 el.style.backgroundColor = '';
  374.                 el.style.background = '';
  375.             }
  376.         });
  377.         console.log('[OW Theme] Inline backgrounds stripped');
  378.     }
  379.  
  380.     function fillMissingInputs() {
  381.         const usernameInput = document.querySelector('#navbar_username');
  382.         if (usernameInput && usernameInput.value.trim() === '') {
  383.             usernameInput.placeholder = 'Login';
  384.         }
  385.  
  386.         const passwordInput = document.querySelector('#navbar_password');
  387.         if (passwordInput && passwordInput.value.trim() === '') {
  388.             passwordInput.placeholder = 'Password';
  389.         }
  390.  
  391.  
  392.         // "Remember Me" checkbox label
  393.         const checkboxLabel = document.querySelector('label[for="cb_cookieuser_navbar"]');
  394.         if (checkboxLabel && !checkboxLabel.querySelector('span')) {
  395.             // Add a span inside the label so checkbox remains intact
  396.             const span = document.createElement('span');
  397.             span.textContent = ' Remember Me?';
  398.             span.style.marginRight = '4px';
  399.             checkboxLabel.appendChild(span);
  400.         }
  401.  
  402.         // Login button
  403.         const loginButton = document.querySelector('input[type="submit"].button');
  404.         if (loginButton && !loginButton.value.trim()) {
  405.             loginButton.value = 'Login';
  406.             loginButton.title = 'Click to login';
  407.         }
  408.     }
  409.  
  410.  
  411.     function fillForumLegendTexts() {
  412.         // Map of image filenames → legend text
  413.         const legendMap = {
  414.             'forum_new.gif': 'New Posts (Not a big chance you\'ll see this one in the wild anymore)',
  415.             'forum_old.gif': 'No New Posts',
  416.             'forum_old_lock.gif': 'Locked Forum'
  417.         };
  418.  
  419.         // Loop over all relevant table cells
  420.         document.querySelectorAll('tr td img[src*="statusicon/native/forum_"]').forEach(img => {
  421.             const filename = img.src.split('/').pop(); // get last part of src
  422.             const text = legendMap[filename];
  423.             if (text) {
  424.                 // The next <td> cell is where the text should go
  425.                 const textTd = img.closest('tr').querySelector('td.smallfont:nth-child(2)');
  426.                 if (textTd && (!textTd.textContent || textTd.textContent.trim() === '')) {
  427.                     textTd.textContent = text;
  428.                     textTd.style.paddingLeft = '5px';
  429.                     console.log(`[OW Theme] Added legend text: "${text}"`);
  430.                 }
  431.             }
  432.         });
  433.     }
  434.  
  435.     function fillForumHeaders() {
  436.         // The default header texts
  437.         const headers = ['', 'Forum', 'Last Post', 'Threads', 'Posts'];
  438.  
  439.         // Find all tbody elements whose id starts with collapseobj_forumbit_
  440.         document.querySelectorAll('tbody[id^="collapseobj_forumbit_"]').forEach(tbody => {
  441.             const headerRow = tbody.querySelector('tr:first-child');
  442.             if (headerRow) {
  443.                 const tds = headerRow.querySelectorAll('td.thead');
  444.                 tds.forEach((td, index) => {
  445.                     if (td && (!td.textContent || td.textContent.trim() === '')) {
  446.                         td.textContent = headers[index] || '';
  447.                     }
  448.                 });
  449.                 console.log('[OW Theme] Forum header restored for', tbody.id);
  450.             }
  451.         });
  452.     }
  453.  
  454.     function fillWelcomeText() {
  455.         const tables = document.querySelectorAll('table.tborder');
  456.  
  457.         for (const table of tables) {
  458.             const rows = table.querySelectorAll('tr');
  459.             if (rows.length < 2) continue;
  460.  
  461.             const firstTd = rows[0].querySelector('td.tcat');
  462.             const secondTd = rows[1].querySelector('td.alt1');
  463.  
  464.             if (
  465.                 firstTd && secondTd &&
  466.                 (!firstTd.textContent.trim() || firstTd.textContent.trim() === '\u00a0') &&
  467.                 (!secondTd.textContent.trim() || secondTd.textContent.trim() === '\u00a0')
  468.             ) {
  469.                 // Fill tcat header
  470.                 firstTd.textContent = 'This is Necrum. The mostly read-only version of Oddworld Forums.';
  471.                 console.log('[OW Theme] Added tcat welcome text');
  472.  
  473.                 // Fill alt1 info row
  474.                 secondTd.innerHTML = `
  475.                     If this is your first visit, be sure to check out the
  476.                     <a href="faq.php"><strong>FAQ</strong></a> by clicking the link above.
  477.                     You may have to <a href="register.php"><strong>register</strong></a> before you can post.
  478.                     To start viewing messages, select the forum that you want to visit from the selection below.
  479.                     <div style="height: 1em;"></div> <!-- empty line -->
  480.                     If you have any issues with the new theme, or maybe requests for it, reach me at <a href="https://discord.gg/GwK5EQAX"><strong>OWF Discord</strong></a> (Varrok). No promises though.
  481.                 `;
  482.                 console.log('[OW Theme] Added alt1 welcome info text');
  483.  
  484.                 break; // stop after first match
  485.             }
  486.         }
  487.     }
  488.  
  489.     function fillWhatsGoingOn() {
  490.         // 1. Fill the main tcat header
  491.         const tcat = document.querySelector('thead tr td.tcat[colspan="2"]');
  492.         if (tcat && (!tcat.textContent.trim() || tcat.textContent.trim() === '\u00a0')) {
  493.             tcat.textContent = "What's Going On?";
  494.             console.log('[OW Theme] Added tcat: What\'s Going On?');
  495.         }
  496.  
  497.         const td = document.querySelector('tbody tr td.thead[colspan="2"] a[href*="online.php"]');
  498.         if (!td) return;
  499.  
  500.         // Add label to the <a>
  501.         td.textContent = "Currently Active Users (likely all non human agents)";
  502.  
  503.         // Find the next text node after the <a>
  504.         const textNode = td.nextSibling;
  505.         if (textNode && textNode.nodeType === Node.TEXT_NODE) {
  506.             // Extract the number from the text (ignore brackets)
  507.             const match = textNode.textContent.match(/[\d,]+/);
  508.             if (match) {
  509.                 textNode.textContent = ': ' + match[0]; // keep only the number
  510.             } else {
  511.                 textNode.textContent = ''; // fallback
  512.             }
  513.         }
  514.  
  515.         console.log('[OW Theme] Fixed Currently Active Users label and number');
  516.  
  517.  
  518.         // 3. Fill Stats header
  519.         const statsHeader = document.querySelectorAll('tbody tr td.thead[colspan="2"]')[1];
  520.         if (statsHeader && (!statsHeader.textContent.trim() || statsHeader.textContent.trim() === '\u00a0')) {
  521.             statsHeader.textContent = "Oddworld Forums Statistics";
  522.             console.log('[OW Theme] Added Stats header');
  523.         }
  524.     }
  525.  
  526.     function labelForumStats() {
  527.         const statsDiv = document.querySelector('#collapseobj_forumhome_stats .smallfont div:first-child');
  528.         if (!statsDiv || statsDiv.dataset.labeled) return;
  529.  
  530.         // Match numbers that may have commas
  531.         const numbers = statsDiv.textContent.match(/[\d,]+/g);
  532.         if (!numbers) return;
  533.  
  534.         // Build labeled text dynamically
  535.         const labels = ['Threads', 'Posts', 'Members', 'Active Members'];
  536.         const labeledText = numbers.map((num, i) => labels[i] + ': ' + num).join(' ');
  537.  
  538.         statsDiv.textContent = labeledText;
  539.         statsDiv.dataset.labeled = '1';
  540.         console.log('[OW Theme] Forum stats labeled correctly');
  541.     }
  542.  
  543.     function labelStatsAndUsers() {
  544.         // 1. Most users ever online
  545.         const onlineDiv = document.querySelector('#collapseobj_forumhome_activeusers .smallfont div:first-child');
  546.         if (onlineDiv && !onlineDiv.dataset.labeled) {
  547.             onlineDiv.textContent = 'Most users ever online was 4,307, 10-18-2015 at 08:35 PM (from archive.org as of 2020)' + onlineDiv.textContent.trim();
  548.             onlineDiv.dataset.labeled = '1'; // mark as done
  549.             console.log('[OW Theme] Labeled Most users ever online');
  550.         }
  551.  
  552.         // 2. Active users list
  553.         const activeUsersDiv = document.querySelector('#collapseobj_forumhome_activeusers .smallfont div:nth-child(2)');
  554.         if (activeUsersDiv && !activeUsersDiv.dataset.labeled) {
  555.             activeUsersDiv.innerHTML = 'Currently online: ' + activeUsersDiv.innerHTML.trim();
  556.             activeUsersDiv.dataset.labeled = '1';
  557.             console.log('[OW Theme] Labeled currently online members');
  558.         }
  559.  
  560.         labelForumStats();
  561.  
  562.         // 4. Newest member
  563.         const newestMemberDiv = document.querySelector('#collapseobj_forumhome_stats .smallfont div:nth-child(2)');
  564.         if (newestMemberDiv && !newestMemberDiv.dataset.labeled) {
  565.             newestMemberDiv.innerHTML = 'Welcome to our newest member: <i>We can\'t have any new members anymore.</i>' + newestMemberDiv.innerHTML.trim();
  566.             newestMemberDiv.dataset.labeled = '1';
  567.             console.log('[OW Theme] Labeled newest member');
  568.         }
  569.     }
  570.  
  571.     function fillUserCpMenuLabels() {
  572.         const menu = document.getElementById('usercpoptions_menu');
  573.         if (!menu) return;
  574.  
  575.         const labelMap = {
  576.             'usercp.php': 'User Control Panel',
  577.             'private.php': 'Private Messages',
  578.             'subscription.php': 'Subscriptions',
  579.             'profile.php?do=editsignature': 'Edit Signature',
  580.             'profile.php?do=editprofile': 'Edit Profile',
  581.             'profile.php?do=editoptions': 'Edit Options',
  582.             '#': 'Log Out',
  583.             'member.php?u=5866': 'View Profile'
  584.         };
  585.  
  586.         menu.querySelectorAll('a').forEach(a => {
  587.             const href = a.getAttribute('href');
  588.             if (href && labelMap[href]) {
  589.                 a.textContent = labelMap[href];
  590.             }
  591.         });
  592.  
  593.         console.log('[OW Theme] User CP menu labels filled');
  594.     }
  595.  
  596.     function showPage() {
  597.         // Remove the pre-theme style
  598.         const preStyle = document.getElementById('pre-theme-style');
  599.         if (preStyle) preStyle.remove();
  600.  
  601.         // Make the page visible
  602.         document.documentElement.style.visibility = 'visible';
  603.     }
  604.  
  605.     function fillUserCpPanelLabels() {
  606.         // --- 1. Panel Headers ---
  607.         const headerMap = {
  608.             'pmpanel': 'Private Messages',
  609.             'subscribepanel': 'Subscriptions',
  610.             'settingspanel': 'Settings & Options',
  611.             'miscpanel': 'Miscellaneous'
  612.         };
  613.  
  614.         Object.entries(headerMap).forEach(([id, text]) => {
  615.             const td = document.querySelector(`#collapseimg_${id}`)?.closest('td');
  616.             if (td && (!td.textContent.trim() || td.textContent.trim() === '\u00a0')) {
  617.                 td.appendChild(document.createTextNode(text));
  618.             }
  619.         });
  620.  
  621.         const labelMap = {
  622.  
  623.             // Private Messages panel
  624.             'private.php?folderid=0': 'Inbox',
  625.             'private.php?folderid=-1': 'Sent Items',
  626.             'private.php?do=newpm': 'Compose Message',
  627.             'private.php?do=trackpm': 'Track Messages',
  628.             'private.php?do=editfolders': 'Edit Folders',
  629.  
  630.             // Subscriptions panel
  631.             'subscription.php?do=viewsubscription': 'Your Subscriptions',
  632.             'subscription.php?do=viewsubscription&daysprune=-1&folderid=all': 'View All',
  633.             'subscription.php?do=editfolders': 'Edit Subscription Folders',
  634.  
  635.             // Settings & Options panel
  636.             'profile.php?do=editsignature': 'Edit Signature',
  637.             'profile.php?do=editpassword': 'Edit Email & Password',
  638.             'profile.php?do=editprofile': 'Edit Profile',
  639.             'profile.php?do=editoptions': 'Edit Options',
  640.             'profile.php?do=editavatar': 'Edit Avatar',
  641.             'profile.php?do=editprofilepic': 'Edit Profile Picture',
  642.  
  643.             // Misc panel
  644.             'calendar.php?do=viewreminder': 'Calendar Reminders',
  645.             'profile.php?do=editlist': 'Edit Friend & Ignore List',
  646.             'profile.php?do=editattachments': 'Edit Attachments'
  647.         };
  648.  
  649.         document.querySelectorAll('#usercpoptions_menu a, tbody a').forEach(a => {
  650.             const href = a.getAttribute('href');
  651.             if (href && labelMap[href] && !a.textContent.trim()) {
  652.                 a.textContent = labelMap[href];
  653.             }
  654.         });
  655.  
  656.         console.log('[OW Theme] Filled User CP panel labels');
  657.     }
  658.  
  659.     function fillSubscriptionsPanel() {
  660.         // 1. Fill the header tcat
  661.         const tcat = document.querySelector('#collapseimg_subslistinfo')?.closest('td.tcat');
  662.         if (tcat) {
  663.             // Only fill if text is empty
  664.             if (!tcat.querySelector('span.normal')?.textContent.trim()) {
  665.                 let span = tcat.querySelector('span.normal');
  666.                 if (!span) {
  667.                     span = document.createElement('span');
  668.                     span.className = 'normal';
  669.                     tcat.appendChild(span);
  670.                 }
  671.                 span.textContent = "Subscription Info:";
  672.             }
  673.         }
  674.  
  675.         // 2. Fill legend
  676.         const legend = document.querySelector('#collapseobj_subslistinfo legend');
  677.         if (legend && !legend.textContent.trim()) {
  678.             legend.textContent = "Subscription List";
  679.         }
  680.  
  681.         // 3. Fill select option text
  682.         document.querySelectorAll('#collapseobj_subslistinfo select option').forEach(option => {
  683.             if (!option.textContent.trim()) {
  684.                 // Keep the number but add label
  685.                 const num = option.textContent.replace(/[^\d]/g, '') || '0';
  686.                 option.textContent = `Inbox (${num})`;
  687.             }
  688.         });
  689.  
  690.         // 4. Fill submit button
  691.         const submitBtn = document.querySelector('#collapseobj_subslistinfo input[type="submit"]');
  692.         if (submitBtn && !submitBtn.value.trim()) {
  693.             submitBtn.value = "Go";
  694.         }
  695.  
  696.         console.log('[OW Theme] Filled Subscription List panel labels');
  697.     }
  698.  
  699.     function fillPrivateMessagesPanel() {
  700.         // 1. Fill legend
  701.         const legend = document.querySelector('#collapseobj_pmlistinfo legend');
  702.         if (legend && !legend.textContent.trim()) {
  703.             legend.textContent = "Folder Info";
  704.         }
  705.  
  706.         // 2. Fill folder name in <strong>
  707.         const folderStrong = document.querySelector('#collapseobj_pmlistinfo td > strong');
  708.         if (folderStrong && !folderStrong.textContent.trim()) {
  709.             folderStrong.textContent = "Inbox"; // Default folder name
  710.         }
  711.  
  712.         // 3. Update select options
  713.         document.querySelectorAll('#collapseobj_pmlistinfo select option').forEach(option => {
  714.             if (!option.textContent.trim()) {
  715.                 const num = option.textContent.replace(/[^\d]/g, '') || '0';
  716.                 const folderName = option.value === "-1" ? "Inbox" : "Other Folder";
  717.                 option.textContent = `${folderName} (${num})`;
  718.             }
  719.         });
  720.  
  721.         // 4. Fill submit button
  722.         const submitBtn = document.querySelector('#collapseobj_pmlistinfo input[type="submit"]');
  723.         if (submitBtn && !submitBtn.value.trim()) {
  724.             submitBtn.value = "Go";
  725.         }
  726.  
  727.         console.log('[OW Theme] Filled Private Messages panel labels');
  728.     }
  729.  
  730.     function fillSearchPanel() {
  731.         const table = document.querySelector('table[cellpadding="4"][cellspacing="1"]');
  732.         if (!table) return;
  733.  
  734.         // 1. Fill empty thead cells with titles
  735.         table.querySelectorAll('td.thead').forEach((thead, idx) => {
  736.             if (!thead.textContent.trim()) {
  737.                 thead.textContent = idx === 0 ? "Quick Search" : "Search by Title";
  738.             }
  739.         });
  740.  
  741.         // 2. Fill search form submit buttons
  742.         const submitButtons = table.querySelectorAll('input[type="submit"]');
  743.         submitButtons.forEach(btn => {
  744.             if (!btn.value.trim()) {
  745.                 btn.value = "Search";
  746.             }
  747.         });
  748.  
  749.         // 3. Fill empty <a> links with text
  750.         table.querySelectorAll('a').forEach(a => {
  751.             if (!a.textContent.trim()) {
  752.                 a.textContent = a.href.includes("blog_search.php") ? "Search Blogs" : "Advanced Search";
  753.             }
  754.         });
  755.  
  756.         // 4. Fill radio label descriptions
  757.         const radios = [
  758.             { id: "rb_nb_sp0", label: "Show All Posts" },
  759.             { id: "rb_nb_sp1", label: "Show Only Your Posts" }
  760.         ];
  761.         radios.forEach(r => {
  762.             const input = document.getElementById(r.id);
  763.             if (input) {
  764.                 const label = table.querySelector(`label[for="${r.id}"]`);
  765.                 if (label && !label.textContent.trim()) {
  766.                     label.insertAdjacentText('beforeend', r.label);
  767.                 }
  768.             }
  769.         });
  770.  
  771.         console.log('[OW Theme] Filled Search Panel labels');
  772.     }
  773.  
  774.     function cleanSearchPanel() { //not working buttons
  775.         const table = document.querySelector('table[cellpadding="4"][cellspacing="1"]');
  776.         if (!table) return;
  777.  
  778.         const tbody = table.querySelector('tbody');
  779.         if (!tbody) return;
  780.  
  781.         // Collect rows with visible links
  782.         const rowsToKeep = Array.from(tbody.querySelectorAll('tr')).filter(tr => {
  783.             const link = tr.querySelector('a');
  784.             return link && link.textContent.trim() !== '';
  785.         });
  786.  
  787.         // Clear the tbody
  788.         tbody.innerHTML = '';
  789.  
  790.         // Re-add only the rows we want
  791.         rowsToKeep.forEach(tr => tbody.appendChild(tr));
  792.  
  793.         // Optional: add small padding to links
  794.         tbody.querySelectorAll('a').forEach(a => {
  795.             a.style.display = 'block';  // ensure vertical stacking
  796.             a.style.paddingLeft = '5px'; // subtle left padding
  797.             a.style.paddingRight = '5px'; // subtle left padding
  798.         });
  799.  
  800.         console.log('[OW Theme] Search panel cleaned and fixed');
  801.     }
  802.  
  803.     function fixFooterLinks() {
  804.         // Find all links in the footer area (smallfont strong inside a td)
  805.         const links = document.querySelectorAll('td div.smallfont strong a');
  806.  
  807.         links.forEach(link => {
  808.             if (!link.textContent.trim()) {
  809.                 const href = link.getAttribute('href') || '';
  810.                 if (href.includes('sendmessage.php')) { //todo button does nothing
  811.                     link.textContent = 'Send Message';
  812.                 } else if (href.includes('archive/index.php')) {
  813.                     link.textContent = 'Archive';
  814.                 } else if (href === '#top') {
  815.                     link.textContent = 'Top';
  816.                 }
  817.             }
  818.         });
  819.  
  820.         console.log('[OW Theme] Footer links fixed');
  821.     }
  822.  
  823.     function replaceOldDomainLinks() {
  824.         const links = document.querySelectorAll('a[href*="oddworldforums.net"]');
  825.  
  826.         links.forEach(link => {
  827.             const oldHref = link.getAttribute('href');
  828.             const newHref = oldHref.replace('oddworldforums.net', 'owforums.net');
  829.             link.setAttribute('href', newHref);
  830.         });
  831.  
  832.         console.log('[OW Theme] All oddworldforums.net links replaced with owforums.net');
  833.     }
  834.  
  835.     function fixPostUserInfoLabels() {
  836.         // Select all post user info blocks
  837.         const infoBlocks = document.querySelectorAll('td.alt2 div.info2');
  838.  
  839.         infoBlocks.forEach(block => {
  840.             const divs = block.querySelectorAll('div');
  841.  
  842.             // Safety check: divs.length should be at least 3 for Joined, Location, Posts
  843.             if (divs.length >= 3) {
  844.                 // Add labels before existing ": ..."
  845.                 if (divs[0].textContent.trim().startsWith(':')) {
  846.                     divs[0].textContent = 'Joined: ' + divs[0].textContent.replace(':', '').trim();
  847.                 }
  848.                 if (divs[1].textContent.trim().startsWith(':')) {
  849.                     divs[1].textContent = 'Location: ' + divs[1].textContent.replace(':', '').trim();
  850.                 }
  851.                 if (divs[2].textContent.trim().startsWith(':')) {
  852.                     divs[2].textContent = 'Posts: ' + divs[2].textContent.replace(':', '').trim();
  853.                 }
  854.             }
  855.         });
  856.  
  857.         console.log('[OW Theme] Added labels to post user info');
  858.     }
  859.  
  860.     async function labelQuotedPostsWithAuthor() {
  861.         const quoteSections = Array.from(
  862.             document.querySelectorAll('div.smallfont + table')
  863.         ).filter(table => {
  864.             const label = table.previousElementSibling;
  865.  
  866.             return (
  867.                 label &&
  868.                 label.classList.contains('smallfont') &&
  869.                 label.textContent.trim() === ':' &&
  870.                 table.querySelector('td.alt2') &&
  871.                 table.querySelector('a[href*="showthread.php?p="]')
  872.             );
  873.         });
  874.  
  875.         // Cache to avoid fetching the same post multiple times
  876.         const fetchedPosts = {};
  877.  
  878.         const promises = quoteSections.map(async (section) => {
  879.             const link = section.querySelector('a[href*="showthread.php?p="]');
  880.             let username = '';
  881.  
  882.             if (link) {
  883.                 const postUrl = link.href;
  884.  
  885.                 // Skip fetch if we already fetched this post
  886.                 if (fetchedPosts[postUrl]) {
  887.                     username = fetchedPosts[postUrl];
  888.                 } else {
  889.                     try {
  890.                         const response = await fetch(postUrl, { credentials: 'include' });
  891.                         if (response.ok) {
  892.                             const htmlText = await response.text();
  893.                             const parser = new DOMParser();
  894.                             const doc = parser.parseFromString(htmlText, 'text/html');
  895.  
  896.                             const postIdMatch = postUrl.match(/p=(\d+)/);
  897.                             if (postIdMatch) {
  898.                                 const postId = postIdMatch[1];
  899.                                 const postDiv = doc.querySelector(`#post${postId}`);
  900.                                 if (postDiv) {
  901.                                     const usernameLink = postDiv.querySelector('.bigusername, .normalusername');
  902.                                     username = usernameLink ? usernameLink.textContent.trim() : '';
  903.                                 }
  904.                             }
  905.                         }
  906.                         fetchedPosts[postUrl] = username; // Cache the result
  907.                     } catch (err) {
  908.                         console.error('Error fetching quote post:', err);
  909.                     }
  910.                 }
  911.             }
  912.  
  913.             const labelText = username ? `Quote (from ${username}):` : 'Quote:';
  914.  
  915.             const labelDiv = section.previousElementSibling;
  916.             if (labelDiv && labelDiv.classList.contains('smallfont')) {
  917.                 labelDiv.textContent = labelText;
  918.             } else {
  919.                 const newLabelDiv = document.createElement('div');
  920.                 newLabelDiv.className = 'smallfont';
  921.                 newLabelDiv.style.marginBottom = '2px';
  922.                 newLabelDiv.textContent = labelText;
  923.                 section.parentNode.insertBefore(newLabelDiv, section);
  924.             }
  925.         });
  926.  
  927.         await Promise.allSettled(promises); // ensures all promises run without stopping on error
  928.         console.log('[OW Theme] Labeled all quote sections in parallel.');
  929.     }
  930.  
  931.     function disableSendMessage() {
  932.         const links = Array.from(
  933.             document.querySelectorAll('a[href="sendmessage.php"]')
  934.         );
  935.  
  936.         if (links.length === 0) return;
  937.  
  938.         // Last occurrence
  939.         const last = links[links.length - 1];
  940.         last.removeAttribute('href');
  941.         last.removeAttribute('rel');
  942.         last.removeAttribute('accesskey');
  943.         last.textContent = '🧸';
  944.         last.style.fontSize = '20px';
  945.         last.title = "I'm watching you...";
  946.  
  947.         // Previous occurrence (if it exists)
  948.         if (links.length > 1) {
  949.             const prev = links[links.length - 2];
  950.             prev.removeAttribute('href');
  951.             prev.removeAttribute('rel');
  952.             prev.removeAttribute('accesskey');
  953.             prev.textContent = 'inform the administrator (disabled)'; //doesn't work
  954.             prev.title = 'Messaging disabled';
  955.             prev.style.whiteSpace = 'pre-line'; // Only applies to this link
  956.         }
  957.  
  958.         console.log('[OW Theme] Modified last and previous Send Message links');
  959.     }
  960.  
  961.     function replaceThemeOptions() {
  962.         // Find the select containing the optgroup
  963.         const select = document.querySelector('select[name="styleid"]');
  964.         if (!select) return;
  965.  
  966.         // Define the custom captions you want
  967.         const newCaptions = {
  968.             "18": "Necrum",
  969.             "13": "---- Industrial (Disabled)",
  970.             "1": "---- vBulletin 3 (Disabled)",
  971.             "11": "---- Munch's Oddysee (Disabled)",
  972.             "12": "---- HoneyBee Theme (Disabled)"
  973.         };
  974.  
  975.         // Move all options out of any optgroup
  976.         select.querySelectorAll('optgroup').forEach(group => {
  977.             const parent = group.parentNode;
  978.             group.querySelectorAll('option').forEach(option => parent.appendChild(option));
  979.             group.remove(); // remove the empty optgroup
  980.         });
  981.  
  982.         // Replace text for each option
  983.         select.querySelectorAll('option').forEach(option => {
  984.             if (newCaptions[option.value]) {
  985.                 option.textContent = newCaptions[option.value];
  986.             }
  987.  
  988.             // Enable only Native (value 18), disable the rest
  989.             if (option.value !== "18") {
  990.                 option.disabled = true;
  991.             } else {
  992.                 option.selected = true; // make sure Native is selected
  993.             }
  994.         });
  995.  
  996.         console.log('[OW Theme] Updated select option captions');
  997.     }
  998.  
  999.     function labelSidebarButtons() {
  1000.         const buttonMap = {
  1001.             'search.php?do=getdaily': 'Daily Posts',
  1002.             'forumdisplay.php?do=markread': 'Mark Forums As Read',
  1003.             'online.php': "Who's Online"
  1004.         };
  1005.  
  1006.         const links = document.querySelectorAll('.vbmenu_option a[href]');
  1007.         links.forEach(link => {
  1008.             const href = link.getAttribute('href');
  1009.             for (const key in buttonMap) {
  1010.                 if (href.includes(key)) {
  1011.                     link.textContent = buttonMap[key];
  1012.                     // remove cursor: default on parent to make it look clickable
  1013.                     if (link.parentElement) link.parentElement.style.cursor = 'pointer';
  1014.                 }
  1015.             }
  1016.         });
  1017.         console.log('[OW Theme] Sidebar buttons labeled.');
  1018.     }
  1019.  
  1020.     function removeMenuLinks() {
  1021.         // Remove "New Posts"
  1022.         const newPostsTd = document.querySelector('td.vbmenu_control2 a[href*="search.php?do=getnew"]');
  1023.         if (newPostsTd) {
  1024.             newPostsTd.closest('td.vbmenu_control2').remove();
  1025.         }
  1026.  
  1027.         // Remove "Quick Links" (usercptools)
  1028.         const quickLinksTd = document.querySelector('td#usercptools');
  1029.         if (quickLinksTd) {
  1030.             quickLinksTd.remove();
  1031.         }
  1032.  
  1033.         // Remove "Today's posts" (getdaily)
  1034.         const getDailyTd = document.querySelector('td.vbmenu_control2 a[href*="search.php?do=getdaily"]');
  1035.         if (getDailyTd) {
  1036.             getDailyTd.closest('td.vbmenu_control2').remove();
  1037.         }
  1038.  
  1039.  
  1040.         console.log('[OW Theme] Removed sections.');
  1041.     }
  1042.  
  1043.     function fixForumNavigationLabels() {
  1044.         const links = [
  1045.             { hrefContains: 'do=markread', text: 'Mark Forums As Read' },
  1046.             { hrefContains: 'showgroups.php', text: 'View Forum Leaders' },
  1047.             { hrefContains: 'memberlist.php', text: 'Members List' },
  1048.         ];
  1049.  
  1050.         links.forEach(linkInfo => {
  1051.             const a = document.querySelector(`div.smallfont a[href*="${linkInfo.hrefContains}"]`);
  1052.             if (a) {
  1053.                 if (!a.textContent.trim()) {
  1054.                     a.textContent = linkInfo.text;
  1055.                 }
  1056.             }
  1057.         });
  1058.  
  1059.         console.log('[OW Theme] Fixed missing forum nav labels.');
  1060.     }
  1061.  
  1062.     function fixDropdownLabels() {
  1063.         const labelMap = {
  1064.             cp: 'Control Panel',
  1065.             pm: 'Private Messages',
  1066.             subs: 'Subscriptions',
  1067.             wol: "Who's Online",
  1068.             search: 'Search',
  1069.             home: 'Forum Home'
  1070.         };
  1071.  
  1072.         const select = document.querySelector('select[name="f"]');
  1073.         if (!select) return;
  1074.  
  1075.         const groups = select.querySelectorAll('optgroup');
  1076.  
  1077.         if (groups[0] && (!groups[0].label || !groups[0].label.trim())) {
  1078.             groups[0].label = 'User Tools';
  1079.         }
  1080.  
  1081.         if (groups[1] && (!groups[1].label || !groups[1].label.trim())) {
  1082.             groups[1].label = 'Forums';
  1083.         }
  1084.  
  1085.         // 1. Fill empty option labels
  1086.         select.querySelectorAll('option').forEach(option => {
  1087.             const value = option.value?.trim();
  1088.             const text = option.textContent.trim();
  1089.  
  1090.             if (!text && labelMap[value]) {
  1091.                 option.textContent = labelMap[value];
  1092.             }
  1093.         });
  1094.  
  1095.         // 2. Remove empty optgroup labels (but keep grouping behavior)
  1096.         select.querySelectorAll('optgroup').forEach(group => {
  1097.             if (!group.label || !group.label.trim()) {
  1098.                 group.removeAttribute('label');
  1099.             }
  1100.         });
  1101.  
  1102.         console.log('[OW Theme] Fixed forum jump missing labels');
  1103.     }
  1104.  
  1105.     function fixSelectedMessagesLabels() {
  1106.         const select = document.querySelector('select[name="dowhat"]');
  1107.         if (!select) return;
  1108.  
  1109.         const labelMap = {
  1110.             move: 'Move to Folder',
  1111.             delete: 'Delete'
  1112.         };
  1113.  
  1114.         Array.from(select.options).forEach(option => {
  1115.             const text = option.textContent.trim();
  1116.  
  1117.             // Replace empty OR placeholder labels
  1118.             if (
  1119.                 labelMap[option.value] &&
  1120.                 (text === '' || text === '...')
  1121.             ) {
  1122.                 option.textContent = labelMap[option.value];
  1123.             }
  1124.         });
  1125.         console.log('[OW Theme] Fixed Selected Messages action labels');
  1126.     }
  1127.  
  1128.     function fixBlogQuickSearchLabels() {
  1129.         const checkboxLabel = document.querySelector('label[for="cb_titleonly"]');
  1130.         if (checkboxLabel && checkboxLabel.textContent.trim() === '') {
  1131.             checkboxLabel.appendChild(
  1132.                 document.createTextNode(' Search titles only')
  1133.             );
  1134.         }
  1135.  
  1136.         const button = document.querySelector(
  1137.             'input.button[tabindex="53"]'
  1138.         );
  1139.  
  1140.         if (!button) return;
  1141.  
  1142.         // Fix missing label
  1143.         if (!button.value.trim()) {
  1144.             button.value = 'Search Blogs';
  1145.         }
  1146.  
  1147.         // Fix broken float layout
  1148.         button.style.float = 'none';
  1149.         button.style.display = 'block';
  1150.         button.style.marginLeft = 'auto';
  1151.  
  1152.         console.log('[OW Theme] Fixed blog quicksearch checkbox label');
  1153.     }
  1154.  
  1155.     function fixMemberlistHeaderLabels() {
  1156.         const headerCells = document.querySelectorAll('tr[align="center"] td.thead');
  1157.  
  1158.         headerCells.forEach(td => {
  1159.             const link = td.querySelector('a');
  1160.             // Only if the <a> exists but has no text
  1161.             if (link && !link.textContent.trim()) {
  1162.                 if (td.innerHTML.includes('sortasc')) {
  1163.                     link.textContent = 'Username ↑';
  1164.                 } else if (td.innerHTML.includes('joindate')) {
  1165.                     link.textContent = 'Join Date';
  1166.                 } else if (td.innerHTML.includes('posts')) {
  1167.                     link.textContent = 'Posts';
  1168.                 } else if (td.innerHTML.includes('reputation')) {
  1169.                     link.textContent = 'Reputation';
  1170.                 } else {
  1171.                     // Fallback for other empty links
  1172.                     link.textContent = 'Sort';
  1173.                 }
  1174.             }
  1175.         });
  1176.  
  1177.         console.log('[OW Theme] Fixed missing memberlist header labels.');
  1178.     }
  1179.  
  1180.     function fixProfilePanelLabels() {
  1181.         // Private Message link
  1182.         const pmLink = document.querySelector('td.panelsurround a[href*="private.php?do=newpm"]');
  1183.         if (pmLink && !pmLink.textContent.trim()) {
  1184.             pmLink.textContent = 'Send PM';
  1185.         }
  1186.  
  1187.         // Forum Info fieldset legend
  1188.         document.querySelectorAll('fieldset.fieldset > legend').forEach(legend => {
  1189.             if (!legend.textContent.trim()) {
  1190.                 legend.textContent = 'Statistics';
  1191.             }
  1192.         });
  1193.  
  1194.  
  1195.         // Additional empty user links
  1196.         document.querySelectorAll('td.panelsurround a[href*="search.php?do=finduser"]').forEach(link => {
  1197.             if (!link.textContent.trim()) {
  1198.                 // Look for the username in the next table row
  1199.                 const currentTr = link.closest('tr');
  1200.                 let username = null;
  1201.  
  1202.                 if (currentTr && currentTr.nextElementSibling) {
  1203.                     const nextTrText = currentTr.nextElementSibling.textContent;
  1204.                     const match = nextTrText.match(/by\s+(.+)/i);
  1205.                     if (match) username = match[1].trim();
  1206.                 }
  1207.  
  1208.                 if (username) {
  1209.                     link.textContent = `Find all posts made by ${username}`;
  1210.                 } else {
  1211.                     link.textContent = 'Find all posts made by the user';
  1212.                 }
  1213.             }
  1214.         });
  1215.  
  1216.         console.log('[OW Theme] Filled missing profile panel labels.');
  1217.     }
  1218.  
  1219.     function removeForumRulesSection() {
  1220.         const forumRules = document.getElementById('collapseobj_forumrules');
  1221.         if (forumRules) {
  1222.             const table = forumRules.closest('table'); // get the parent table
  1223.             if (table) {
  1224.                 table.remove();
  1225.                 console.log('[OW Theme] Removed forum rules section bottom left (empty).');
  1226.             }
  1227.         }
  1228.     }
  1229.  
  1230.     function fixNavigationButton() {
  1231.         const form = document.querySelector('form[action="forumdisplay.php"]');
  1232.         if (!form) return;
  1233.  
  1234.         const select = form.querySelector('select[name="f"]');
  1235.         const button = form.querySelector('input[type="submit"]');
  1236.  
  1237.         if (!select || !button) return;
  1238.  
  1239.         // Wrap the select and button in a flex container
  1240.         const wrapper = document.createElement('div');
  1241.         wrapper.style.display = 'flex';
  1242.         wrapper.style.alignItems = 'center';
  1243.         wrapper.style.gap = '4px';
  1244.  
  1245.         select.parentNode.insertBefore(wrapper, select);
  1246.         wrapper.appendChild(select);
  1247.         wrapper.appendChild(button);
  1248.  
  1249.         // Set button label and style
  1250.         button.value = 'Go';
  1251.         button.style.padding = '1px 10px';
  1252.         button.style.fontSize = '1em';
  1253.     }
  1254.  
  1255.     function removeEmptyForumViewers() {
  1256.         document.querySelectorAll('td.alt1Active').forEach(td => {
  1257.             // Look for the forum link
  1258.             const forumLink = td.querySelector('a[href^="forumdisplay.php?f="]');
  1259.             if (!forumLink) return;
  1260.  
  1261.             // Check the next sibling span for empty brackets
  1262.             const nextSpan = forumLink.parentElement.querySelector('span.smallfont');
  1263.             if (nextSpan && nextSpan.textContent.trim() === '()') {
  1264.                 nextSpan.remove(); // remove only the empty brackets
  1265.             }
  1266.         });
  1267.     }
  1268.  
  1269.     function fixCalendarForms() {
  1270.         // Map for month numbers → names
  1271.         const monthNames = [
  1272.             '', 'January', 'February', 'March', 'April', 'May', 'June',
  1273.             'July', 'August', 'September', 'October', 'November', 'December'
  1274.         ];
  1275.  
  1276.         // Fix first form: Jump to month
  1277.         const jumpForm = document.querySelector('form[action="calendar.php"]:first-of-type');
  1278.         if (jumpForm) {
  1279.             // Add month names
  1280.             jumpForm.querySelectorAll('select[name="month"] option').forEach((opt, idx) => {
  1281.                 if (monthNames[idx]) opt.textContent = monthNames[idx];
  1282.             });
  1283.             // Add label for year dropdown
  1284.             jumpForm.querySelector('strong').textContent = 'Jump to Month';
  1285.             // Add submit button label
  1286.             const submitBtn = jumpForm.querySelector('input[type="submit"]');
  1287.             if (submitBtn) submitBtn.value = 'Go';
  1288.             // Align button with dropdown
  1289.             submitBtn.style.marginLeft = '6px';
  1290.             submitBtn.style.verticalAlign = 'top';
  1291.         }
  1292.  
  1293.         // Fix second form: Calendar Jump
  1294.         const calForm = document.querySelectorAll('form[action="calendar.php"]')[1];
  1295.         if (calForm) {
  1296.             const optGroup = calForm.querySelector('optgroup');
  1297.             if (optGroup) optGroup.label = 'Calendar Type';
  1298.             // Add submit button label
  1299.             const submitBtn = calForm.querySelector('input[type="submit"]');
  1300.             if (submitBtn) submitBtn.value = 'Jump';
  1301.             // Align button
  1302.             submitBtn.style.marginLeft = '6px';
  1303.             submitBtn.style.verticalAlign = 'top';
  1304.         }
  1305.     }
  1306.  
  1307.     function fixShowPostsLabels() {
  1308.         const labels = [
  1309.             { id: 'rb_showposts_0', text: 'Show Threads' },
  1310.             { id: 'rb_showposts_1', text: 'Show Posts' }
  1311.         ];
  1312.  
  1313.         labels.forEach(l => {
  1314.             const label = document.querySelector(`label[for="${l.id}"]`);
  1315.             if (label) {
  1316.                 // remove any existing text nodes
  1317.                 Array.from(label.childNodes)
  1318.                     .filter(n => n.nodeType === Node.TEXT_NODE)
  1319.                     .forEach(n => n.remove());
  1320.  
  1321.                 // append a new text node so input stays intact
  1322.                 label.appendChild(document.createTextNode(' ' + l.text));
  1323.             }
  1324.         });
  1325.     }
  1326.  
  1327.     function fixResetSearchBtn() {
  1328.         // Select the reset button
  1329.         const resetBtn = document.querySelector('input[type="reset"][onclick*="search.php?type=advanced"]');
  1330.         if (resetBtn) {
  1331.             resetBtn.value = "Reset";                // Add a caption
  1332.             resetBtn.style.position = "relative";    // Allow offset
  1333.             resetBtn.style.top = "0px";             // Nudge it 1px up
  1334.         }
  1335.     }
  1336.  
  1337.     function fixEmptyOption() {
  1338.         const selects = document.querySelectorAll('select');
  1339.         selects.forEach(select => {
  1340.             const options = select.options;
  1341.             if (options.length >= 2) {
  1342.                 const secondOption = options[1];
  1343.                 if (secondOption.value === "1" && secondOption.text.trim() === "Find Threads Started by User") {
  1344.                     const firstOption = options[0];
  1345.                     if (firstOption.value === "0" && firstOption.text.trim() === "") {
  1346.                         firstOption.textContent = "Find Posts Made By User";
  1347.                         console.log("First option label fixed!");
  1348.                     }
  1349.                 }
  1350.             }
  1351.         });
  1352.  
  1353.         const divs = document.querySelectorAll('div');
  1354.  
  1355.         divs.forEach(div => {
  1356.             if (div.textContent.trim() === ":") {
  1357.                 div.textContent = "Username:";
  1358.                 console.log("Username label fixed!");
  1359.             }
  1360.         });
  1361.     }
  1362.  
  1363.     function fixProfileDropdownList() {
  1364.         // Loop through all menu links
  1365.         const menuLinks = document.querySelectorAll('td.vbmenu_option a');
  1366.  
  1367.         menuLinks.forEach(a => {
  1368.             if (!a.textContent.trim()) {
  1369.                 const href = a.getAttribute('href');
  1370.                 let label = "No Label";
  1371.  
  1372.                 if (href.includes('do=newpm')) label = "Send Private Message";
  1373.                 else if (href.includes('do=finduser')) label = "Find User Posts";
  1374.                 else if (href.includes('addlist&userlist=buddy')) label = "Add to Buddy List";
  1375.  
  1376.                 a.textContent = label;
  1377.                 console.log(`Added label "${label}" to ${href}`);
  1378.             }
  1379.         });
  1380.     }
  1381.  
  1382.     function fixAddBuddyBtnLabels() {
  1383.         // Confirm button
  1384.         const confirmBtn = document.querySelector('input[type="submit"][name="confirm"]');
  1385.         if (confirmBtn && !confirmBtn.value.trim()) {
  1386.             confirmBtn.value = "Confirm";
  1387.             console.log("Set Confirm button label");
  1388.         }
  1389.  
  1390.         // Deny button
  1391.         const denyBtn = document.querySelector('input[type="submit"][name="deny"]');
  1392.         if (denyBtn && !denyBtn.value.trim()) {
  1393.             denyBtn.value = "Deny";
  1394.             console.log("Set Deny button label");
  1395.         }
  1396.     }
  1397.  
  1398.     function fixBlogSideBarProfile() {
  1399.         const blogUserMenu = document.getElementById('blogusermenu');
  1400.         if (!blogUserMenu) return;
  1401.  
  1402.         // Find the info block directly under the same tborder
  1403.         const infoList = blogUserMenu
  1404.             .closest('.tborder')
  1405.             .querySelector('.alt1 .smallfont ul.nobullets');
  1406.  
  1407.         if (!infoList) return;
  1408.  
  1409.         const labels = [
  1410.             'Date Joined',
  1411.             'Location',
  1412.             'Posts',
  1413.             'Blog Entries'
  1414.         ];
  1415.  
  1416.         const items = infoList.querySelectorAll('li');
  1417.  
  1418.         items.forEach((li, index) => {
  1419.             if (!labels[index]) return;
  1420.  
  1421.             const shade = li.querySelector('.shade');
  1422.             if (!shade) return;
  1423.  
  1424.             let text = shade.textContent.trim();
  1425.  
  1426.             // Insert missing label
  1427.             if (!text) {
  1428.                 text = labels[index];
  1429.             }
  1430.  
  1431.             // Ensure colon
  1432.             if (!text.endsWith(':')) {
  1433.                 text += ':';
  1434.             }
  1435.  
  1436.             shade.textContent = text;
  1437.         });
  1438.  
  1439.  
  1440.         console.log('[OW Theme] Blog sidebar labels fixed');
  1441.     }
  1442.  
  1443.     function fixBlogNavigationBtn() {
  1444.         const filterForm = document.querySelector('form[action*="blog.php"][action*="do=list"]');
  1445.         if (!filterForm) return;
  1446.  
  1447.         const submitBtn = filterForm.querySelector('input[type="submit"]');
  1448.         if (!submitBtn) return;
  1449.  
  1450.         // Get rid of a pointless button
  1451.         if (submitBtn) {
  1452.             submitBtn.remove();
  1453.             console.log('[OW Theme] Blog filter submit button removed');
  1454.         }
  1455.     }
  1456.  
  1457.     function timeAgo(fromDate) {
  1458.         const seconds = Math.floor((Date.now() - fromDate.getTime()) / 1000);
  1459.         const units = [
  1460.             ['year', 31536000],
  1461.             ['month', 2592000],
  1462.             ['day', 86400],
  1463.         ];
  1464.  
  1465.         for (const [unit, value] of units) {
  1466.             const amount = Math.floor(seconds / value);
  1467.             if (amount >= 1) {
  1468.                 return `${amount} ${unit}${amount !== 1 ? 's' : ''} ago`;
  1469.             }
  1470.         }
  1471.  
  1472.         return 'today';
  1473.     }
  1474.  
  1475.     function fixUserProfileLabels() {
  1476.         // Locate the Forum Info panel (left column)
  1477.         const forumInfoPanel = document.querySelector(
  1478.             'td.panelsurround .panel div > .fieldset'
  1479.         );
  1480.         if (!forumInfoPanel) return;
  1481.  
  1482.         /* ---- Date Joined ---- */
  1483.         const joinedDiv = [...document.querySelectorAll('div')]
  1484.             .find(div =>
  1485.                 div.firstChild?.nodeType === Node.TEXT_NODE &&
  1486.                 div.firstChild.textContent.trim().startsWith(':') &&
  1487.                 div.querySelector('strong')
  1488.             );
  1489.  
  1490.         if (!joinedDiv) return;
  1491.  
  1492.         const strong = joinedDiv.querySelector('strong');
  1493.         const dateText = strong.textContent.trim();
  1494.  
  1495.         const parsedDate = new Date(dateText.replace(/-/g, '/'));
  1496.         if (isNaN(parsedDate)) return;
  1497.  
  1498.         const ageText = timeAgo(parsedDate);
  1499.  
  1500.         /* ---- Fix leading colon ---- */
  1501.         // Find the text node that contains the leading colon
  1502.         for (const node of joinedDiv.childNodes) {
  1503.             if (node.nodeType === Node.TEXT_NODE && node.textContent.includes(':')) {
  1504.                 // Remove the colon and any leading spaces
  1505.                 node.textContent = node.textContent.replace(/^\s*:\s*/, '');
  1506.                 break; // only remove the first colon
  1507.             }
  1508.         }
  1509.         /* ---- Insert label before the date ---- */
  1510.         const label = document.createElement('strong');
  1511.         label.textContent = 'Date Joined: ';
  1512.  
  1513.         joinedDiv.insertBefore(label, strong);
  1514.  
  1515.         /* ---- Replace (0 ) only ---- */
  1516.         joinedDiv.childNodes.forEach(node => {
  1517.             if (
  1518.                 node.nodeType === Node.TEXT_NODE &&
  1519.                 /\(\s*0\s*\)/.test(node.textContent)
  1520.             ) {
  1521.                 node.textContent = ` (${ageText})`;
  1522.             }
  1523.         });
  1524.  
  1525.         /* ---- Last Post ---- */
  1526.         // Find the Statistics fieldset
  1527.         const statsFieldset = Array.from(document.querySelectorAll('fieldset'))
  1528.             .find(fs => fs.querySelector('legend')?.textContent.trim() === 'Statistics');
  1529.  
  1530.         if (!statsFieldset) return;
  1531.  
  1532.         // Find the row that contains the last post link
  1533.         const lastPostTd = Array.from(statsFieldset.querySelectorAll('td'))
  1534.             .find(td =>
  1535.                 td.querySelector('a[href*="showthread.php"]') &&
  1536.                 td.textContent.trim().startsWith(':')
  1537.             );
  1538.  
  1539.         if (!lastPostTd) return;
  1540.  
  1541.         for (const node of lastPostTd.childNodes) {
  1542.             if (node.nodeType === Node.TEXT_NODE && node.textContent.includes(':')) {
  1543.                 // Remove colon and leading/trailing spaces
  1544.                 node.textContent = node.textContent.replace(/^\s*:\s*/, '');
  1545.                 break; // Only fix the first colon
  1546.             }
  1547.         }
  1548.  
  1549.         // Add the label
  1550.         lastPostTd.insertAdjacentText('afterbegin', 'Last Post: ');
  1551.  
  1552.     }
  1553.  
  1554.     function fixBlogCalendarHeader() {
  1555.         // Locate the blog calendar container
  1556.         const calendar = document.getElementById('vb_blogcalendar');
  1557.         if (!calendar) return;
  1558.  
  1559.         // Find the thead header cell
  1560.         const headerTh = calendar.querySelector('thead th.thead');
  1561.         if (!headerTh) return;
  1562.  
  1563.         // Avoid duplicating the label
  1564.         if (headerTh.textContent.trim()) return;
  1565.  
  1566.         // Create label text node
  1567.         const label = document.createElement('span');
  1568.         label.textContent = 'Blog Calendar';
  1569.  
  1570.         // Insert label before the collapse button (first child)
  1571.         headerTh.insertBefore(label, headerTh.firstChild);
  1572.  
  1573.         console.log('[OW Theme] Blog Calendar header fixed');
  1574.     }
  1575.  
  1576.     function removeAdditionalInformationPanelProfile() {
  1577.         // Remove Additional Information header
  1578.         const addInfoHeader = [...document.querySelectorAll('td.tcat')]
  1579.             .find(td => td.textContent.trim() === "Additional Information");
  1580.         if (addInfoHeader) addInfoHeader.remove();
  1581.  
  1582.         // Remove the Additional Information panel
  1583.         const addInfoPanel = document.querySelector('#additionalinfo_list')?.closest('td.panelsurround');
  1584.         if (addInfoPanel) addInfoPanel.remove();
  1585.  
  1586.         // Stretch Group Memberships header
  1587.         const groupHeader = [...document.querySelectorAll('td.tcat')]
  1588.             .find(td => td.textContent.trim() === "Group Memberships");
  1589.         if (groupHeader) groupHeader.colSpan = 2;
  1590.  
  1591.         // Stretch Group Memberships panel
  1592.         const groupPanel = document.querySelector('.panelsurround .smallfont')?.closest('td.panelsurround');
  1593.         if (groupPanel) groupPanel.colSpan = 2;
  1594.     }
  1595.  
  1596.     function fixMissingProfileAvatar() {
  1597.         const urlParams = new URLSearchParams(window.location.search);
  1598.         const userId = urlParams.get('u');
  1599.         if (!userId) return;
  1600.  
  1601.         const lastPostTd = [...document.querySelectorAll('td')].find(td =>
  1602.             td.textContent.trim().startsWith('Last Post:')
  1603.         );
  1604.         if (!lastPostTd) return;
  1605.  
  1606.         const lastPostLink = lastPostTd.querySelector('a');
  1607.         if (!lastPostLink) return;
  1608.  
  1609.         // Replace showthread.php with showpost.php to get the single post
  1610.         const postUrl = lastPostLink.href.replace('showthread.php', 'showpost.php');
  1611.  
  1612.         fetch(postUrl)
  1613.             .then(response => response.text())
  1614.             .then(htmlText => {
  1615.                 const parser = new DOMParser();
  1616.                 const doc = parser.parseFromString(htmlText, 'text/html');
  1617.  
  1618.                 // Try customavatars first
  1619.                 let avatarImg = doc.querySelector('img[src*="customavatars/avatar"]');
  1620.  
  1621.                 // Fallback to avatars/ pattern if no customavatars found
  1622.                 if (!avatarImg) {
  1623.                     avatarImg = doc.querySelector('img[src*="avatars/"]');
  1624.                 }
  1625.  
  1626.                 if (!avatarImg) return; // no avatar found
  1627.  
  1628.                 const avatarUrl = avatarImg.src;
  1629.  
  1630.                 // Panel containing username and stars
  1631.                 const panel = document.querySelector('.bigusername')?.parentElement;
  1632.                 if (!panel) return;
  1633.  
  1634.                 const img = document.createElement('img');
  1635.                 img.src = avatarUrl;
  1636.                 img.style.cssText = 'float: left; margin: 10px; margin-top: 4px';
  1637.  
  1638.                 panel.insertBefore(img, panel.firstChild);
  1639.             })
  1640.             .catch(err => console.error('Failed to fetch last post:', err));
  1641.     }
  1642.  
  1643.     function cleanContactInfoPanel() {
  1644.         // Find the Contact Info header
  1645.         const contactHeader = [...document.querySelectorAll('td.tcat')]
  1646.             .find(td => td.textContent.trim() === 'Contact Info');
  1647.  
  1648.         if (!contactHeader) return;
  1649.  
  1650.         // The Contact Info panel is the next row's second <td>
  1651.         const contactPanelTd = contactHeader.closest('tr')?.nextElementSibling?.children[1];
  1652.         if (!contactPanelTd) return;
  1653.  
  1654.         // Remove any <td> inside the Contact Info panel that contains only a colon
  1655.         [...contactPanelTd.querySelectorAll('td')].forEach(td => {
  1656.             if (/^\s*:\s*$/.test(td.textContent)) {
  1657.                 td.remove();
  1658.             }
  1659.         });
  1660.     }
  1661.  
  1662.     function fixContactInfoSubforumsLabel() {
  1663.         // Select all elements in the document
  1664.         const allNodes = [...document.querySelectorAll('div, td, span, p')];
  1665.  
  1666.         allNodes.forEach(node => {
  1667.             if (!node.innerHTML) return;
  1668.  
  1669.             // Match empty <strong></strong> followed by colon and subforum links
  1670.             if (/^<strong><\/strong>:\s*(<img[^>]+>\s*<a href="forumdisplay\.php\?f=\d+">[^<]+<\/a>)/i.test(node.innerHTML)) {
  1671.                 node.innerHTML = node.innerHTML.replace(
  1672.                     /^<strong><\/strong>:/,
  1673.                     '<strong>Subforums:</strong>'
  1674.                 );
  1675.             }
  1676.         });
  1677.     }
  1678.  
  1679.     function removeEmptyContactStatistics() {
  1680.         // Find all fieldsets with legend "Statistics"
  1681.         document.querySelectorAll('fieldset.fieldset').forEach(fieldset => {
  1682.             const legend = fieldset.querySelector('legend');
  1683.             if (legend && legend.textContent.trim() === "Statistics") {
  1684.                 // Check if the fieldset is inside a Contact Info panel
  1685.                 const contactPanel = fieldset.closest('td.panelsurround');
  1686.                 const headerTd = contactPanel?.closest('tr')?.previousElementSibling?.querySelector('td.tcat:nth-child(2)');
  1687.                 if (headerTd && headerTd.textContent.trim() === "Contact Info") {
  1688.                     // Check if all <tr> are empty
  1689.                     const rows = fieldset.querySelectorAll('tr');
  1690.                     const hasContent = [...rows].some(tr => tr.textContent.trim() !== '');
  1691.                     if (!hasContent) {
  1692.                         fieldset.remove(); // remove the entire empty fieldset
  1693.                     }
  1694.                 }
  1695.             }
  1696.         });
  1697.     }
  1698.  
  1699.     function replaceLegacyTopPanelOnce() {
  1700.         const td = document.querySelector(
  1701.             'td[colspan="3"][background*="gradient_tcat.gif"] img[src*="native_01.gif"]'
  1702.         )?.closest('td');
  1703.  
  1704.         if (!td) return false;
  1705.  
  1706.         // Clear old content
  1707.         td.innerHTML = '';
  1708.  
  1709.         // Preserve gradient background
  1710.         td.style.backgroundImage =
  1711.             'url(' + TOP_GRADIENT + ')';
  1712.         td.style.backgroundRepeat = 'repeat-x';
  1713.         td.style.padding = '0';
  1714.  
  1715.         // Create panel container
  1716.         const panel = document.createElement('div');
  1717.         panel.style.display = 'flex';
  1718.         panel.style.justifyContent = 'space-evenly';
  1719.         panel.style.alignItems = 'center';
  1720.         panel.style.height = '34px';
  1721.         panel.style.width = '100%';
  1722.  
  1723.         // Embedded button data
  1724.         const buttons = [
  1725.             {
  1726.                 href: 'https://discord.gg/GwK5EQAX',
  1727.                 img: 'https://i.imgur.com/8bunCWy.png',
  1728.                 alt: 'OWF Discord',
  1729.                 hoverImg: 'https://i.imgur.com/c37oT49.png'
  1730.             },
  1731.             {
  1732.                 href: 'https://magogonthemarch.wordpress.com/',
  1733.                 img: 'https://i.imgur.com/hmGyBm4.png',
  1734.                 alt: 'Magog on the March'
  1735.             },
  1736.             {
  1737.                 href: 'http://oddworldlibrary.net/',
  1738.                 img: 'https://i.imgur.com/NU48IiC.png',
  1739.                 alt: 'Oddworld Library'
  1740.             },
  1741.             {
  1742.                 href: 'https://oddwords.hu/',
  1743.                 img: 'https://i.imgur.com/Sz035L9.png',
  1744.                 alt: 'Oddwords'
  1745.             },
  1746.             {
  1747.                 href: 'http://www.oddworld.com/',
  1748.                 img: 'https://i.imgur.com/oR7iGLx.png',
  1749.                 alt: 'Oddworld'
  1750.             }
  1751.         ];
  1752.  
  1753.         // Build buttons
  1754.         buttons.forEach(b => {
  1755.             const a = document.createElement('a');
  1756.             a.href = b.href;
  1757.  
  1758.             const img = document.createElement('img');
  1759.             img.src = b.img;
  1760.             img.alt = b.alt;
  1761.             img.style.display = 'block';
  1762.             img.style.position = 'relative';
  1763.             img.style.top = '1px';
  1764.  
  1765.             // Swap image on hover
  1766.             if (b.hoverImg) {
  1767.                 img.addEventListener('mouseenter', () => {
  1768.                     img.src = b.hoverImg;
  1769.                 });
  1770.                 img.addEventListener('mouseleave', () => {
  1771.                     img.src = b.img;
  1772.                 });
  1773.             }
  1774.  
  1775.             a.appendChild(img);
  1776.             panel.appendChild(a);
  1777.         });
  1778.  
  1779.         td.appendChild(panel);
  1780.         return true;
  1781.     }
  1782.  
  1783.     function replaceLegacyNavbarOnce() {
  1784.         // Remove all top/bottom divs around navbar
  1785.         document.querySelectorAll('.div-top, .div-bot').forEach(div => div.remove());
  1786.         const navImg = document.querySelector('img[src*="nav_calendar.gif"]');
  1787.         if (!navImg) return false;
  1788.         const navTable = navImg.closest('table');
  1789.         if (!navTable) return false;
  1790.  
  1791.         // Remove div-top / div-bot that are siblings or in wrapping container
  1792.         const container = navTable.parentElement;
  1793.         if (container) {
  1794.             container.querySelectorAll('.div-top, .div-bot').forEach(div => div.remove());
  1795.         }
  1796.         [navTable.previousElementSibling, navTable.nextElementSibling].forEach(sib => {
  1797.             if (!sib) return;
  1798.             if (sib.classList.contains('div-top') || sib.classList.contains('div-bot')) {
  1799.                 sib.remove();
  1800.             }
  1801.         });
  1802.  
  1803.         const buttonImages = {
  1804.             usercp: 'https://i.imgur.com/PJhQLxK.png',
  1805.             blogs: 'https://i.imgur.com/DQBrc5e.png',
  1806.             faq: 'https://i.imgur.com/xOwVVCe.png',
  1807.             rules: 'https://i.imgur.com/uKurJ06.png',
  1808.             calendar: 'https://i.imgur.com/6DnnYXM.png',
  1809.             search: 'https://i.imgur.com/4YwdFOb.png',
  1810.             logout: 'https://i.imgur.com/EFd3JmG.png'
  1811.         };
  1812.  
  1813.         // Apply gradient to the full table so it stretches full width
  1814.         navTable.style.background = `url(${TOP_GRADIENT}) repeat-x`;
  1815.         navTable.style.width = '100%';
  1816.  
  1817.         // Normalize all td cells and unify class
  1818.         navTable.querySelectorAll('td.vbmenu_control, td.vbmenu_control2').forEach(td => {
  1819.             td.className = 'vbmenu_control2';
  1820.  
  1821.             td.style.background = 'transparent';
  1822.  
  1823.             td.style.height = '34px';
  1824.  
  1825.  
  1826.             td.querySelectorAll('img[src*="nav_div"]').forEach(i => i.remove());
  1827.         });
  1828.  
  1829.         // Replace / insert button images & handle flex alignment
  1830.         navTable.querySelectorAll('td').forEach(td => {
  1831.             const a = td.querySelector('a');
  1832.             if (!a) return;
  1833.  
  1834.             const href = a.getAttribute('href') || '';
  1835.             let key = null;
  1836.  
  1837.             if (href.includes('usercp')) key = 'usercp';
  1838.             else if (href.includes('blog')) key = 'blogs';
  1839.             else if (href.includes('faq')) key = 'faq';
  1840.             else if (href.includes('rules')) key = 'rules';
  1841.             else if (href.includes('calendar')) key = 'calendar';
  1842.             else if (href.includes('search')) key = 'search';
  1843.             else if (href.includes('logout')) key = 'logout';
  1844.  
  1845.             if (!key || !buttonImages[key]) return;
  1846.  
  1847.             // Remove old button images (except arrow)
  1848.             a.querySelectorAll('img:not([src*="menu_open"])').forEach(img => img.remove());
  1849.  
  1850.             // Remove all text nodes safely
  1851.             Array.from(a.childNodes).forEach(node => {
  1852.                 if (node.nodeType === Node.TEXT_NODE) node.remove();
  1853.             });
  1854.  
  1855.  
  1856.             // Insert main button image
  1857.             const img = document.createElement('img');
  1858.             img.src = buttonImages[key];
  1859.             img.alt = key;
  1860.             img.style.display = 'inline-block';
  1861.             img.style.verticalAlign = 'middle';
  1862.             img.style.marginTop = '1px';
  1863.             a.prepend(img);
  1864.  
  1865.             // Move arrow inside <a> without flex
  1866.             const arrow = td.querySelector('img[src*="menu_open"]');
  1867.             if (arrow) {
  1868.                 arrow.remove();
  1869.                 arrow.style.display = 'inline-block';
  1870.                 arrow.style.verticalAlign = 'middle';
  1871.                 arrow.style.marginLeft = '-5px';
  1872.                 a.appendChild(arrow);
  1873.             }
  1874.         });
  1875.  
  1876.  
  1877.         return true;
  1878.     }
  1879.  
  1880.     function labelThreadSortSubmitButton() {
  1881.         const tables = Array.from(document.querySelectorAll('table'));
  1882.  
  1883.         const targetTable = tables.find(table =>
  1884.             table.querySelector('select[name="sort"]') &&
  1885.             table.querySelector('select[name="order"]') &&
  1886.             table.querySelector('select[name="daysprune"]')
  1887.         );
  1888.  
  1889.         if (!targetTable) {
  1890.             console.warn('[OW Theme] Sort table not found.');
  1891.             return;
  1892.         }
  1893.  
  1894.         const submitButton = targetTable.querySelector('input[type="submit"].button');
  1895.  
  1896.         if (!submitButton) {
  1897.             console.warn('[OW Theme] Submit button not found.');
  1898.             return;
  1899.         }
  1900.  
  1901.         // Only label if it's currently blank
  1902.         if (!submitButton.value.trim()) {
  1903.             submitButton.value = 'Apply';
  1904.         }
  1905.  
  1906.         console.log('[OW Theme] Labeled thread sort submit button.');
  1907.     }
  1908.  
  1909.     function fixForumJumpButton() {
  1910.         const form = document.querySelector(
  1911.             'form[action="forumdisplay.php"] select[name="f"]'
  1912.         )?.closest('form');
  1913.  
  1914.         if (!form) {
  1915.             console.warn('[OW Theme] Forum jump form not found.');
  1916.             return;
  1917.         }
  1918.  
  1919.         const submitButton = form.querySelector('input[type="submit"].button');
  1920.  
  1921.         if (!submitButton) {
  1922.             console.warn('[OW Theme] Forum jump submit button not found.');
  1923.             return;
  1924.         }
  1925.  
  1926.         submitButton.value = 'Go';
  1927.  
  1928.         submitButton.style.verticalAlign = 'top';
  1929.         submitButton.style.marginLeft = '6px';
  1930.  
  1931.         // Optional: normalize height to match the select
  1932.         const select = form.querySelector('select[name="f"]');
  1933.         if (select) {
  1934.             submitButton.style.height = `${select.offsetHeight}px`;
  1935.         }
  1936.  
  1937.         console.log('[OW Theme] Forum jump button fixed.');
  1938.     }
  1939.  
  1940.     function fixForumToolsTextAlignment() {
  1941.         const forumTools = document.getElementById('forumtools');
  1942.         const forumSearch = document.getElementById('forumsearch');
  1943.  
  1944.         if (!forumTools || !forumSearch) {
  1945.             console.warn('[OW Theme] Forum header cells not found.');
  1946.             return;
  1947.         }
  1948.  
  1949.         // Align the link text itself
  1950.         const link = forumTools.querySelector('a');
  1951.         if (link) {
  1952.             link.style.verticalAlign = 'middle';
  1953.             link.style.lineHeight = '1';
  1954.         }
  1955.  
  1956.         // Prevent the icon from dragging the baseline down
  1957.         const icon = forumTools.querySelector('img');
  1958.         if (icon) {
  1959.             icon.style.verticalAlign = 'middle';
  1960.             icon.style.marginLeft = '4px';
  1961.         }
  1962.  
  1963.         console.log('[OW Theme] Forum Tools text alignment nudged up.');
  1964.     }
  1965.  
  1966.     function fixPageNavPopupLabels() {
  1967.         const form = document.getElementById('pagenav_form');
  1968.         if (!form) {
  1969.             console.warn('[OW Theme] Page navigation form not found.');
  1970.             return;
  1971.         }
  1972.  
  1973.         const input = form.querySelector('#pagenav_itxt');
  1974.         const button = form.querySelector('#pagenav_ibtn');
  1975.  
  1976.         if (!input || !button) {
  1977.             console.warn('[OW Theme] Page navigation controls not found.');
  1978.             return;
  1979.         }
  1980.  
  1981.         // Label the page number input
  1982.         if (!input.placeholder) {
  1983.             input.placeholder = 'Page';
  1984.         }
  1985.  
  1986.         input.setAttribute('aria-label', 'Page number');
  1987.  
  1988.         // Label the button
  1989.         if (!button.value.trim()) {
  1990.             button.value = 'Go';
  1991.         }
  1992.  
  1993.         button.setAttribute('aria-label', 'Go to page');
  1994.  
  1995.         console.log('[OW Theme] Page navigation popup labels fixed.');
  1996.     }
  1997.  
  1998.     function getReputationTier(rep) {
  1999.         //Todo to be filled later when I actually obtain the rep messages
  2000.         if (rep < 0) return 'Uhhhh this guy is not liked at all!';
  2001.         if (rep < 50) return 'Wallflower';
  2002.         if (rep < 200) return 'Well Known';
  2003.         if (rep < 500) return 'Respected';
  2004.         if (rep < 2000) return 'Imma need me an ass the size of a truck to fit this rep';
  2005.         if (rep < 10000) return 'is a sacred manifestation of  the hippy-dippy spirit of Great Mother OWF';
  2006.         return 'is Chuck Norris';
  2007.     }
  2008.  
  2009.     function fixReputationTooltips() {
  2010.         const repImages = document.querySelectorAll(
  2011.             'img.inlineimg[src*="images/reputation/"]'
  2012.         );
  2013.  
  2014.         repImages.forEach(img => {
  2015.             const text = img.title || img.alt;
  2016.             if (!text) return;
  2017.  
  2018.             // Expected format: "Username  (1234)"
  2019.             const match = text.match(/^(.*?)\s*\((\-?\d+)\)$/);
  2020.             if (!match) return;
  2021.  
  2022.             const username = match[1].trim();
  2023.             const rep = parseInt(match[2], 10);
  2024.  
  2025.             if (Number.isNaN(rep)) return;
  2026.  
  2027.             const tier = getReputationTier(rep);
  2028.             const newText = `${username} ${tier} (${rep})`;
  2029.  
  2030.             img.title = newText;
  2031.             img.alt = newText;
  2032.         });
  2033.  
  2034.         console.log('[OW Theme] Reputation tooltips enhanced.');
  2035.     }
  2036.  
  2037.     function fixSearchTitleOnlyLabel() {
  2038.         const select = document.querySelector(
  2039.             'select[name="titleonly"], select[name="titlesonly"]'
  2040.         );
  2041.  
  2042.         if (!select) {
  2043.             console.warn('[OW Theme] titleonly select not found.');
  2044.             return;
  2045.         }
  2046.  
  2047.         const option = Array.from(select.options).find(opt => opt.value === '1');
  2048.  
  2049.         if (!option) {
  2050.             console.warn('[OW Theme] titleonly=1 option not found.');
  2051.             return;
  2052.         }
  2053.  
  2054.         // Only fix if the label is missing or blank
  2055.         if (!option.textContent.trim()) {
  2056.             option.textContent = 'Search Titles Only';
  2057.         }
  2058.  
  2059.         console.log('[OW Theme] Search Titles Only option label fixed.');
  2060.     }
  2061.  
  2062.     function fixThreadSearchPopup() {
  2063.         // Locate the text input
  2064.         const input = document.querySelector('td.vbmenu_option input[name="query"]');
  2065.         if (!input) {
  2066.             console.warn('[OW Theme] Thread search input not found.');
  2067.             return;
  2068.         }
  2069.  
  2070.         // Find the submit button in the same td
  2071.         const td = input.closest('td.vbmenu_option');
  2072.         if (!td) return;
  2073.  
  2074.         const submitButton = td.querySelector('input[type="submit"].button');
  2075.         if (!submitButton) {
  2076.             console.warn('[OW Theme] Thread search submit button not found.');
  2077.             return;
  2078.         }
  2079.  
  2080.         if (!submitButton.value.trim()) {
  2081.             submitButton.value = 'Go';
  2082.         }
  2083.         submitButton.style.verticalAlign = 'top';
  2084.         submitButton.style.marginLeft = '4px';
  2085.         submitButton.style.height = `24px`;
  2086.  
  2087.         console.log('[OW Theme] Thread search popup fixed.');
  2088.     }
  2089.  
  2090.     function fixFAQResetButton() {
  2091.         // Find the reset button
  2092.         const resetBtn = document.querySelector('input[type="reset"].button');
  2093.         if (!resetBtn) {
  2094.             console.warn('[OW Theme] FAQ reset button not found.');
  2095.             return;
  2096.         }
  2097.  
  2098.         // Only set value if blank
  2099.         if (!resetBtn.value.trim()) {
  2100.             resetBtn.value = 'Reset';
  2101.         }
  2102.  
  2103.         // Align with other button
  2104.         resetBtn.style.verticalAlign = 'top';
  2105.         resetBtn.style.marginLeft = '4px';
  2106.  
  2107.         // Match height of the first submit button if available
  2108.         const submitBtn = document.querySelector('input[type="submit"].button');
  2109.         if (submitBtn) {
  2110.             resetBtn.style.height = `${submitBtn.offsetHeight}px`;
  2111.         }
  2112.  
  2113.         console.log('[OW Theme] FAQ reset button fixed.');
  2114.     }
  2115.  
  2116.     function addThreadLabels() {
  2117.         const labelMap = {
  2118.             'thread_new.gif': 'Thread (New Messages)',
  2119.             'thread.gif': 'Thread (No New Messages)',
  2120.             'thread_lock.gif': 'Thread is closed'
  2121.         };
  2122.  
  2123.         // Loop through all table rows
  2124.         document.querySelectorAll('tr').forEach(tr => {
  2125.             // Find the first <td> with a thread icon
  2126.             const tdWithIcon = Array.from(tr.children).find(td => td.querySelector('img[src*="thread"]'));
  2127.             if (!tdWithIcon) return; // skip rows without a thread icon
  2128.  
  2129.             const img = tdWithIcon.querySelector('img[src*="thread"]');
  2130.             if (!img) return;
  2131.  
  2132.             // Determine the label
  2133.             const label = Object.entries(labelMap).find(([key]) => img.src.includes(key))?.[1];
  2134.             if (!label) return;
  2135.  
  2136.             // Only consider **direct child <td>s after the icon <td>**
  2137.             const tdsAfterIcon = Array.from(tr.children).slice(Array.from(tr.children).indexOf(tdWithIcon) + 1);
  2138.  
  2139.             // Find the first empty td (no images, only whitespace or &nbsp;)
  2140.             for (const td of tdsAfterIcon) {
  2141.                 if (td.querySelector('img')) continue;
  2142.                 const text = td.textContent.replace(/\u00A0/g, '').trim();
  2143.                 if (text === '') {
  2144.                     td.textContent = label;
  2145.                     break; // fill only one td per row
  2146.                 }
  2147.             }
  2148.         });
  2149.     }
  2150.  
  2151.     function labelModerators() {
  2152.         // Find the thread header row (contains "Showing threads")
  2153.         const headerRow = Array.from(document.querySelectorAll('tr')).find(tr => {
  2154.             return Array.from(tr.children).some(td =>
  2155.                 td.classList.contains('thead') && td.textContent.includes('Showing threads')
  2156.             );
  2157.         });
  2158.  
  2159.         if (!headerRow) return; // exit if not found
  2160.  
  2161.         // Look at sibling <td> in the same row
  2162.         Array.from(headerRow.children).forEach(td => {
  2163.             if (!td.classList.contains('thead')) return;
  2164.  
  2165.             const text = td.textContent.trim();
  2166.             const match = text.match(/^:\s*(\d+)$/); // ": 4"
  2167.             if (match) {
  2168.                 td.textContent = `Moderators: ${match[1]}`;
  2169.             }
  2170.         });
  2171.     }
  2172.  
  2173.     function runOnceDOMReady() {
  2174.         removeHeaderRows();
  2175.         trySwapLogo();
  2176.         trySwapGradients();
  2177.         expandPageWidth();
  2178.         recolorTables();
  2179.         expandCenteredTable();
  2180.         injectThemeCSS();
  2181.         stripInlineBackgrounds();
  2182.         showPage();
  2183.         fillMissingInputs();
  2184.         fillForumLegendTexts();
  2185.         fillForumHeaders();
  2186.         fillWelcomeText();
  2187.         fillWhatsGoingOn();
  2188.         labelStatsAndUsers();
  2189.         fillUserCpMenuLabels();
  2190.         fillUserCpPanelLabels();
  2191.         fillSubscriptionsPanel();
  2192.         fillPrivateMessagesPanel();
  2193.         fillSearchPanel();
  2194.         cleanSearchPanel();
  2195.         fixFooterLinks();
  2196.         replaceOldDomainLinks();
  2197.         expandPostsTable();
  2198.         fixPostUserInfoLabels();
  2199.         labelQuotedPostsWithAuthor();
  2200.         disableSendMessage();
  2201.         replaceThemeOptions();
  2202.         labelSidebarButtons();
  2203.         removeMenuLinks();
  2204.         fixForumNavigationLabels();
  2205.         fixDropdownLabels();
  2206.         fixSelectedMessagesLabels();
  2207.         fixBlogQuickSearchLabels();
  2208.         fixMemberlistHeaderLabels();
  2209.         fixProfilePanelLabels();
  2210.         removeForumRulesSection();
  2211.         fixNavigationButton();
  2212.         removeEmptyForumViewers();
  2213.         fixCalendarForms();
  2214.         fixShowPostsLabels();
  2215.         fixResetSearchBtn();
  2216.         fixEmptyOption();
  2217.         fixProfileDropdownList();
  2218.         fixAddBuddyBtnLabels();
  2219.         fixBlogSideBarProfile();
  2220.         fixBlogNavigationBtn();
  2221.         fixBlogCalendarHeader();
  2222.         fixUserProfileLabels();
  2223.         fixMissingProfileAvatar();
  2224.         removeAdditionalInformationPanelProfile();
  2225.         cleanContactInfoPanel();
  2226.         fixContactInfoSubforumsLabel();
  2227.         removeEmptyContactStatistics();
  2228.         replaceLegacyTopPanelOnce();
  2229.         replaceLegacyNavbarOnce();
  2230.         labelThreadSortSubmitButton();
  2231.         fixForumJumpButton();
  2232.         fixForumToolsTextAlignment();
  2233.         fixPageNavPopupLabels();
  2234.         fixReputationTooltips();
  2235.         fixSearchTitleOnlyLabel();
  2236.         fixThreadSearchPopup();
  2237.         fixFAQResetButton();
  2238.         addThreadLabels();
  2239.         labelModerators();
  2240.         // Show the page after theme applied
  2241.         document.documentElement.style.display = '';
  2242.     }
  2243.  
  2244.     if (document.body) {
  2245.         // DOM exists, run immediately
  2246.         runOnceDOMReady();
  2247.     } else {
  2248.         // Wait for DOMContentLoaded
  2249.         document.addEventListener('DOMContentLoaded', runOnceDOMReady);
  2250.     }
  2251. })();
  2252.  
Advertisement
Add Comment
Please, Sign In to add comment