diff --git a/src/js/background.js b/src/js/background.js index b3d560d..28cf693 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -34,6 +34,8 @@ const safeListURLs = [ ]; const fmhyFilterListURL = "https://raw.githubusercontent.com/fmhy/FMHY-SafeGuard/refs/heads/main/fmhy-filterlist.txt"; +const unsafeReasonsURL = + "https://raw.githubusercontent.com/fmhy/FMHYFilterlist/refs/heads/main/filterlists-reasons.json"; const notesBaseURL = "https://raw.githubusercontent.com/fmhy/edit/main/docs/.vitepress/notes/"; @@ -43,6 +45,7 @@ let potentiallyUnsafeSitesRegex = null; let fmhySitesRegex = null; let safeSites = []; let starredSites = []; +let unsafeReasons = {}; // Object to store reasons for unsafe sites const approvedUrls = new Map(); // Map to store approved URLs per tab const notesCache = new Map(); // Cache for fetched notes @@ -234,6 +237,39 @@ function getNoteSlugForDomain(hostname) { return null; } +// Get reason for an unsafe domain +async function getReasonForDomain(hostname) { + const domain = hostname.replace(/^www\./, "").toLowerCase(); + + // If in-memory unsafeReasons is empty, try loading from storage + if (!unsafeReasons || Object.keys(unsafeReasons).length === 0) { + try { + const stored = await browserAPI.storage.local.get("unsafeReasons"); + if (stored.unsafeReasons && Object.keys(stored.unsafeReasons).length > 0) { + unsafeReasons = stored.unsafeReasons; + console.log(`getReasonForDomain: Loaded ${Object.keys(unsafeReasons).length} unsafe reasons from storage`); + } else { + // Storage is also empty, fetch from URL + console.log("getReasonForDomain: Storage empty, fetching from URL..."); + const response = await fetch(unsafeReasonsURL); + if (response.ok) { + unsafeReasons = await response.json(); + await browserAPI.storage.local.set({ unsafeReasons }); + console.log(`getReasonForDomain: Fetched and stored ${Object.keys(unsafeReasons).length} unsafe reasons`); + } + } + } catch (e) { + console.error("Error loading unsafeReasons:", e); + } + } + + // Try exact match first + if (unsafeReasons && unsafeReasons[domain]) return unsafeReasons[domain]; + // Try with www prefix + if (unsafeReasons && unsafeReasons["www." + domain]) return unsafeReasons["www." + domain]; + return null; +} + // Fetch note content from GitHub async function fetchNoteContent(noteSlug) { // Check cache first @@ -368,11 +404,12 @@ function isSearchEngine(url) { async function fetchFilterLists() { console.log("Fetching filter lists..."); try { - const [unsafeResponse, potentiallyUnsafeResponse, fmhyResponse] = + const [unsafeResponse, potentiallyUnsafeResponse, fmhyResponse, reasonsResponse] = await Promise.all([ fetch(filterListURLUnsafe), fetch(filterListURLPotentiallyUnsafe), fetch(fmhyFilterListURL), + fetch(unsafeReasonsURL), ]); let unsafeSites = []; @@ -399,10 +436,25 @@ async function fetchFilterLists() { fmhySitesRegex = generateRegexFromList(fmhySites); } + // Fetch unsafe site reasons + if (reasonsResponse.ok) { + try { + unsafeReasons = await reasonsResponse.json(); + console.log(`Loaded ${Object.keys(unsafeReasons).length} unsafe site reasons`); + } catch (e) { + console.error("Error parsing unsafe reasons JSON:", e); + unsafeReasons = {}; + } + } else { + console.warn("Failed to fetch unsafe reasons, status:", reasonsResponse.status); + unsafeReasons = {}; + } + await browserAPI.storage.local.set({ unsafeSites, potentiallyUnsafeSites, fmhySites, + unsafeReasons, unsafeFilterCount: unsafeSites.length, potentiallyUnsafeFilterCount: potentiallyUnsafeSites.length, fmhyFilterCount: fmhySites.length, @@ -680,133 +732,151 @@ browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { matchedUrl = rootUrl; } - sendResponse({ status, matchedUrl }); + // Get reason if unsafe + let reason = null; + if (status === "unsafe" || status === "potentially_unsafe") { + try { + const urlObj = new URL(matchedUrl); + reason = getReasonForDomain(urlObj.hostname); + } catch (e) { + console.error("Error getting reason:", e); + } + } + + sendResponse({ status, matchedUrl, reason }); return true; } if (message.action === "getSiteStatus") { - try { - // Get the URL from the message - const url = message.url; - if (!url) { - sendResponse({ status: "no_data", matchedUrl: null }); - return true; - } + (async () => { + try { + // Get the URL from the message + const url = message.url; + if (!url) { + sendResponse({ status: "no_data", matchedUrl: null }); + return; + } - console.log(`getSiteStatus: checking status for ${url}`); + console.log(`getSiteStatus: checking status for ${url}`); - // Normalize the URL - const normalizedUrl = normalizeUrl(url); - if (!normalizedUrl) { - sendResponse({ status: "no_data", matchedUrl: null }); - return true; - } + // Normalize the URL + const normalizedUrl = normalizeUrl(url); + if (!normalizedUrl) { + sendResponse({ status: "no_data", matchedUrl: null }); + return; + } - // Extract domain for domain-level checking - const urlObj = new URL(normalizedUrl); - const domain = urlObj.hostname; + // Extract domain for domain-level checking + const urlObj = new URL(normalizedUrl); + const domain = urlObj.hostname; - // First check if it's an extension page - if (url.startsWith(browserAPI.runtime.getURL(""))) { - sendResponse({ status: "extension_page", matchedUrl: url }); - return true; - } + // First check if it's an extension page + if (url.startsWith(browserAPI.runtime.getURL(""))) { + sendResponse({ status: "extension_page", matchedUrl: url }); + return; + } - // Special handling for repository sites - const isRepoSite = ["github.com", "gitlab.com", "sourceforge.net"].some( - (domain) => - urlObj.hostname === domain || urlObj.hostname.endsWith("." + domain) - ); + // Special handling for repository sites + const isRepoSite = ["github.com", "gitlab.com", "sourceforge.net"].some( + (d) => urlObj.hostname === d || urlObj.hostname.endsWith("." + d) + ); - // Variables to track status and matched URL - let status = "no_data"; - let matchedUrl = null; + // Variables to track status and matched URL + let status = "no_data"; + let matchedUrl = null; - // Check full URL first - if (unsafeSitesRegex?.test(normalizedUrl)) { - status = "unsafe"; - matchedUrl = normalizedUrl; - } else if (potentiallyUnsafeSitesRegex?.test(normalizedUrl)) { - status = "potentially_unsafe"; - matchedUrl = normalizedUrl; - } else if (fmhySitesRegex?.test(normalizedUrl)) { - status = "fmhy"; - matchedUrl = normalizedUrl; - } else if (starredSites.includes(normalizedUrl)) { - status = "starred"; - matchedUrl = normalizedUrl; - } else if (safeSites.includes(normalizedUrl)) { - status = "safe"; - matchedUrl = normalizedUrl; - } - - // If no match for full URL and it's a repository site, don't try domain matching - if (status === "no_data" && isRepoSite) { - console.log(`No match for repository URL: ${normalizedUrl}`); - sendResponse({ status: "no_data", matchedUrl: normalizedUrl }); - return true; - } - - // If no match for full URL and it's a regular site, try domain-level matching - if (status === "no_data" && !isRepoSite) { - console.log(`No match for full URL, trying domain: ${domain}`); - - // Check domain against regex patterns - if (unsafeSitesRegex?.test(domain)) { + // Check full URL first + if (unsafeSitesRegex?.test(normalizedUrl)) { status = "unsafe"; - matchedUrl = `https://${domain}`; - } else if (potentiallyUnsafeSitesRegex?.test(domain)) { + matchedUrl = normalizedUrl; + } else if (potentiallyUnsafeSitesRegex?.test(normalizedUrl)) { status = "potentially_unsafe"; - matchedUrl = `https://${domain}`; - } else if (fmhySitesRegex?.test(domain)) { + matchedUrl = normalizedUrl; + } else if (fmhySitesRegex?.test(normalizedUrl)) { status = "fmhy"; - matchedUrl = `https://${domain}`; + matchedUrl = normalizedUrl; + } else if (starredSites.includes(normalizedUrl)) { + status = "starred"; + matchedUrl = normalizedUrl; + } else if (safeSites.includes(normalizedUrl)) { + status = "safe"; + matchedUrl = normalizedUrl; } - // Check domain against starred and safe lists - if (status === "no_data") { - for (const starredUrl of starredSites) { - try { - const starredUrlObj = new URL(starredUrl); - if (starredUrlObj.hostname === domain) { - status = "starred"; - matchedUrl = starredUrl; - break; + // If no match for full URL and it's a repository site, don't try domain matching + if (status === "no_data" && isRepoSite) { + console.log(`No match for repository URL: ${normalizedUrl}`); + sendResponse({ status: "no_data", matchedUrl: normalizedUrl }); + return; + } + + // If no match for full URL and it's a regular site, try domain-level matching + if (status === "no_data" && !isRepoSite) { + console.log(`No match for full URL, trying domain: ${domain}`); + + // Check domain against regex patterns + if (unsafeSitesRegex?.test(domain)) { + status = "unsafe"; + matchedUrl = `https://${domain}`; + } else if (potentiallyUnsafeSitesRegex?.test(domain)) { + status = "potentially_unsafe"; + matchedUrl = `https://${domain}`; + } else if (fmhySitesRegex?.test(domain)) { + status = "fmhy"; + matchedUrl = `https://${domain}`; + } + + // Check domain against starred and safe lists + if (status === "no_data") { + for (const starredUrl of starredSites) { + try { + const starredUrlObj = new URL(starredUrl); + if (starredUrlObj.hostname === domain) { + status = "starred"; + matchedUrl = starredUrl; + break; + } + } catch (e) { + continue; + } + } + } + + if (status === "no_data") { + for (const safeUrl of safeSites) { + try { + const safeUrlObj = new URL(safeUrl); + if (safeUrlObj.hostname === domain) { + status = "safe"; + matchedUrl = safeUrl; + break; + } + } catch (e) { + continue; } - } catch (e) { - continue; } } } - if (status === "no_data") { - for (const safeUrl of safeSites) { - try { - const safeUrlObj = new URL(safeUrl); - if (safeUrlObj.hostname === domain) { - status = "safe"; - matchedUrl = safeUrl; - break; - } - } catch (e) { - continue; - } - } + // Get reason if unsafe + let reason = null; + if (status === "unsafe" || status === "potentially_unsafe") { + reason = await getReasonForDomain(domain); } + + console.log( + `getSiteStatus result for ${url}: ${status}, matched: ${matchedUrl}` + ); + sendResponse({ status: status, matchedUrl: matchedUrl, reason: reason }); + } catch (error) { + console.error("Error in getSiteStatus handler:", error); + sendResponse({ + status: "no_data", + matchedUrl: null, + error: error.message, + }); } - - console.log( - `getSiteStatus result for ${url}: ${status}, matched: ${matchedUrl}` - ); - sendResponse({ status: status, matchedUrl: matchedUrl }); - } catch (error) { - console.error("Error in getSiteStatus handler:", error); - sendResponse({ - status: "no_data", - matchedUrl: null, - error: error.message, - }); - } + })(); return true; // Keep the message channel open for async response } @@ -938,10 +1008,24 @@ async function openWarningPage(tabId, unsafeUrl) { tabApprovedUrls.push(normalizedUrl); approvedUrls.set(tabId, tabApprovedUrls); + // Get the reason for this unsafe site + let hostname; + try { + hostname = new URL(unsafeUrl).hostname; + } catch (e) { + hostname = unsafeUrl.replace(/^https?:\/\//, "").split("/")[0]; + } + const reason = await getReasonForDomain(hostname); + console.log(`openWarningPage: hostname=${hostname}, reason=${reason ? "found" : "not found"}`); + // Redirect to the warning page if it is enabled in settings - const warningPageUrl = browserAPI.runtime.getURL( + let warningPageUrl = browserAPI.runtime.getURL( `../pub/warning-page.html?url=${encodeURIComponent(unsafeUrl)}` ); + if (reason) { + warningPageUrl += `&reason=${encodeURIComponent(reason)}`; + } + console.log(`openWarningPage: redirecting to ${warningPageUrl}`); browserAPI.tabs.update(tabId, { url: warningPageUrl }); } @@ -1034,6 +1118,7 @@ async function initializeExtension() { "fmhySites", "starredSites", "safeSiteList", + "unsafeReasons", ]); if (storedData.unsafeSites && storedData.unsafeSites.length > 0) { @@ -1053,6 +1138,24 @@ async function initializeExtension() { fmhySitesRegex = generateRegexFromList(storedData.fmhySites); } + if (storedData.unsafeReasons && Object.keys(storedData.unsafeReasons).length > 0) { + unsafeReasons = storedData.unsafeReasons; + console.log(`Loaded ${Object.keys(unsafeReasons).length} unsafe reasons from storage`); + } else { + // If no unsafe reasons in storage, fetch them now + console.log("No unsafeReasons in storage, fetching..."); + try { + const reasonsResponse = await fetch(unsafeReasonsURL); + if (reasonsResponse.ok) { + unsafeReasons = await reasonsResponse.json(); + await browserAPI.storage.local.set({ unsafeReasons }); + console.log(`Fetched and stored ${Object.keys(unsafeReasons).length} unsafe reasons`); + } + } catch (e) { + console.error("Error fetching unsafeReasons:", e); + } + } + // Load starred sites from storage if (storedData.starredSites && storedData.starredSites.length > 0) { starredSites = storedData.starredSites; @@ -1098,6 +1201,21 @@ browserAPI.runtime.onMessage.addListener((message, sender, sendResponse) => { return true; } + if (message.action === "forceUpdate") { + console.log("Force update triggered manually"); + Promise.all([ + fetchFilterLists(), + fetchSafeSites(), + fetchStarredSites() + ]).then(() => { + sendResponse({ status: "updated" }); + }).catch((error) => { + console.error("Force update failed:", error); + sendResponse({ status: "error", error: error.message }); + }); + return true; + } + if (message.action === "refreshAllTabs") { // Get all tabs and refresh browserAPI.tabs.query({}).then(async (tabs) => { diff --git a/src/js/content.js b/src/js/content.js index e84f455..0da4545 100644 --- a/src/js/content.js +++ b/src/js/content.js @@ -46,6 +46,7 @@ let unsafeDomains = new Set(); let safeDomains = new Set(); let userTrusted = new Set(); let userUntrusted = new Set(); +let unsafeReasons = {}; // Search engines where highlighting should be applied const searchEngines = [ @@ -177,9 +178,10 @@ async function loadSettings() { // Load domain lists from extension storage async function loadDomainLists() { try { - const { unsafeSites, safeSiteList } = await browserAPI.storage.local.get([ + const { unsafeSites, safeSiteList, unsafeReasons: storedReasons } = await browserAPI.storage.local.get([ "unsafeSites", "safeSiteList", + "unsafeReasons", ]); if (unsafeSites && Array.isArray(unsafeSites)) { @@ -194,6 +196,10 @@ async function loadDomainLists() { ); } + if (storedReasons) { + unsafeReasons = storedReasons; + } + // Apply user overrides applyUserOverrides(); @@ -246,12 +252,12 @@ function setupObserver() { if (!isSupportedSite(currentDomain)) { return; } - + // Special handling for Brave Search - continuously recheck and re-add badges/highlighting for all site types if (currentDomain.includes("brave")) { // Track domains we've processed to avoid repetitive work const braveProcessedDomains = new Set(); - + // Set up a more frequent interval specifically for Brave Search braveSearchBadgeInterval = setInterval(() => { // Process all links in Brave Search results @@ -260,48 +266,50 @@ function setupObserver() { if (!link.href || link.href.startsWith('javascript:') || link.href.startsWith('#')) { return; } - + const linkDomain = normalizeDomain(new URL(link.href).hostname); - + // Skip internal Brave links if (linkDomain.includes('brave.com')) { return; } - + // Always check all links, even if we've seen them before // This ensures styling is reapplied if Brave removes it - + // CASE 1: Unsafe sites (user untrusted or in unsafe list) - if (userUntrusted.has(linkDomain) || - (!userTrusted.has(linkDomain) && unsafeDomains.has(linkDomain))) { - + if (userUntrusted.has(linkDomain) || + (!userTrusted.has(linkDomain) && unsafeDomains.has(linkDomain))) { + // Mark as unsafe link.setAttribute('data-fmhy-unsafe', 'true'); - + // Apply highlighting with !important to ensure it's not overridden link.style.setProperty('text-shadow', `0 0 4px ${settings.untrustedColor}`, 'important'); link.style.setProperty('font-weight', 'bold', 'important'); - + // Force style recalculation by toggling a property link.style.display = 'inline'; link.style.display = ''; - + // Also apply a class for redundancy link.classList.add('fmhy-highlighted-unsafe'); - + // Add badge for unsafe sites - const resultContainer = link.closest('article') || - link.closest('li') || - link.closest('.snippet') || - findParentByTag(link, 5, ['ARTICLE', 'LI', 'DIV']); - + const resultContainer = link.closest('article') || + link.closest('li') || + link.closest('.snippet') || + findParentByTag(link, 5, ['ARTICLE', 'LI', 'DIV']); + if (resultContainer) { const siteDiv = resultContainer.querySelector('div[class^="site svelte-"]'); - + if (siteDiv && !siteDiv.querySelector('.fmhy-unsafe-badge')) { const badge = document.createElement('span'); badge.className = 'fmhy-unsafe-badge'; - badge.innerHTML = '⚠️ FMHY Unsafe Site'; + const reason = getReasonForDomain(linkDomain); + const reasonText = reason ? `: ${reason}` : ""; + badge.innerHTML = `⚠️ FMHY Unsafe Site${reasonText}`; badge.dataset.domain = linkDomain; siteDiv.appendChild(badge); } @@ -311,36 +319,36 @@ function setupObserver() { else if (userTrusted.has(linkDomain) || safeDomains.has(linkDomain)) { // Mark as safe link.setAttribute('data-fmhy-safe', 'true'); - + // Only add highlighting if the setting is enabled if (settings.highlightTrusted) { link.style.setProperty('text-shadow', `0 0 4px ${settings.trustedColor}`, 'important'); link.style.setProperty('font-weight', 'bold', 'important'); } } - } catch (e) {} + } catch (e) { } }); }, 50); // Check more frequently to ensure styling persists } const observer = new MutationObserver((mutations) => { let needsReprocess = false; - + for (const mutation of mutations) { // Check if existing badges were removed if (mutation.removedNodes && mutation.removedNodes.length) { for (const node of mutation.removedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { - if (node.classList && - (node.classList.contains('fmhy-badge-wrapper') || - node.textContent && node.textContent.includes('FMHY Unsafe Site'))) { + if (node.classList && + (node.classList.contains('fmhy-badge-wrapper') || + node.textContent && node.textContent.includes('FMHY Unsafe Site'))) { needsReprocess = true; break; } } } } - + // Process added nodes if (mutation.addedNodes && mutation.addedNodes.length) { for (const node of mutation.addedNodes) { @@ -349,7 +357,7 @@ function setupObserver() { if (node.tagName === "A" && node.href) { processLink(node, currentDomain); } - + // Process any links inside the added node if (node.querySelectorAll) { node @@ -360,7 +368,7 @@ function setupObserver() { } } } - + // If badges were removed, reprocess the page if (needsReprocess) { // Use setTimeout to avoid too frequent reprocessing @@ -375,8 +383,8 @@ function setupObserver() { }); // Watch for both childList and attributes changes, and subtree modifications - observer.observe(document.body, { - childList: true, + observer.observe(document.body, { + childList: true, subtree: true, attributes: true, attributeFilter: ['href'] @@ -388,7 +396,7 @@ function processLink(link, currentDomain) { // Skip if already processed if (processedLinks.has(link)) return; processedLinks.add(link); - + // Add a class to mark as processed for easier detection link.classList.add("fmhy-processed"); @@ -426,7 +434,8 @@ function processLink(link, currentDomain) { } if (settings.showWarningBanners && !processedDomains.has(linkDomain)) { - addWarningBanner(link); + const reason = getReasonForDomain(linkDomain); + addWarningBanner(link, reason); processedDomains.add(linkDomain); } } @@ -454,19 +463,19 @@ function highlightLink(link, type) { } // Add a warning banner after an unsafe link -function addWarningBanner(link) { +function addWarningBanner(link, reason = null) { const currentDomain = normalizeDomain(window.location.hostname); - + // Special handling for Brave Search - use original highlighting style if (currentDomain.includes("brave")) { try { // Mark the link with attribute for CSS styling link.setAttribute('data-fmhy-unsafe', 'true'); - + // Use the original highlighting approach with text shadow link.style.setProperty('text-shadow', `0 0 4px ${settings.untrustedColor}`, 'important'); link.style.setProperty('font-weight', 'bold', 'important'); - + // Add CSS for the badge styling only if (!document.getElementById('fmhy-brave-style')) { const style = document.createElement('style'); @@ -492,10 +501,10 @@ function addWarningBanner(link) { `; document.head.appendChild(style); } - + // Find the closest search result container let resultContainer = link.closest('article') || link.closest('li') || link.closest('.snippet'); - + if (!resultContainer) { // If no direct container, traverse up a few levels resultContainer = link; @@ -508,26 +517,27 @@ function addWarningBanner(link) { } } } - + // Look for the site div specifically (what the user requested) const siteDiv = resultContainer.querySelector('div[class^="site svelte-"]'); - + if (siteDiv) { // Check if we already added a badge to this site div if (!siteDiv.querySelector('.fmhy-unsafe-badge')) { const badge = document.createElement('span'); badge.className = 'fmhy-unsafe-badge'; - badge.innerHTML = '⚠️ FMHY Unsafe Site'; + const reasonText = reason ? `: ${reason}` : ""; + badge.innerHTML = `⚠️ FMHY Unsafe Site${reasonText}`; siteDiv.appendChild(badge); } } - + return; // Exit early } catch (e) { console.error("[FMHY SafeGuard] Error styling Brave Search link:", e); } } - + // For all other search engines, create a badge element const badge = document.createElement("span"); Object.assign(badge.style, { @@ -538,37 +548,38 @@ function addWarningBanner(link) { borderRadius: "4px", fontSize: "12px", display: "inline-block", - transform: "rotate(180deg) scaleX(-1) !important", + transform: "rotate(180deg) scaleX(-1) !important", WebkitTransform: "rotate(180deg) scaleX(-1) !important", msTransform: "rotate(180deg) scaleX(-1) !important", position: "relative", zIndex: "9999" }); - + // Add the warning icon and text - badge.innerHTML = '⚠️ FMHY Unsafe Site'; - + const reasonText = reason ? `: ${reason}` : ""; + badge.innerHTML = `⚠️ FMHY Unsafe Site${reasonText}`; + // Google-specific margin adjustment if (currentDomain.includes("google")) { badge.style.margin = "0 15px"; } - + // Different insertion strategy for Google vs other engines if (currentDomain.includes("google")) { // For Google, find a suitable container let container = link; let parent = link.parentElement; - + // Look for a suitable container for (let i = 0; i < 3 && parent; i++) { - if (parent.tagName === "DIV" || parent.tagName === "LI" || - parent.querySelector("cite") || parent.querySelector(".link")) { + if (parent.tagName === "DIV" || parent.tagName === "LI" || + parent.querySelector("cite") || parent.querySelector(".link")) { container = parent; break; } parent = parent.parentElement; } - + // Try to place after cite element if it exists const citeElement = container.querySelector("cite"); if (citeElement) { @@ -589,6 +600,15 @@ function addWarningBanner(link) { } // Helper functions +function getReasonForDomain(hostname) { + const domain = hostname.replace(/^www\./, "").toLowerCase(); + // Try exact match first + if (unsafeReasons[domain]) return unsafeReasons[domain]; + // Try with www prefix + if (unsafeReasons["www." + domain]) return unsafeReasons["www." + domain]; + return null; +} + function normalizeDomain(hostname) { return hostname.replace(/^www\./, "").toLowerCase(); } @@ -646,13 +666,13 @@ function refreshPage() { processedDomains.clear(); highlightCountTrusted.clear(); highlightCountUntrusted.clear(); - + // Clear any existing Brave Search interval and restart the observer if (braveSearchBadgeInterval) { clearInterval(braveSearchBadgeInterval); braveSearchBadgeInterval = null; } - + // Restart the processing processPage(); setupObserver(); diff --git a/src/pub/index.html b/src/pub/index.html index 1551669..ec925ce 100644 --- a/src/pub/index.html +++ b/src/pub/index.html @@ -182,6 +182,50 @@ transform: scale(1.1); } + /* Reason Section Styles */ + #reason-container { + margin-top: 15px; + padding: 10px; + background: var(--hover-bg); + border-radius: 8px; + text-align: left; + display: none; + } + + #reason-container.visible { + display: block; + } + + #reason-title { + font-size: 13px; + font-weight: 600; + color: #ff6b6b; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; + } + + #reason-title svg { + stroke: #ff6b6b; + } + + #reason-content { + font-size: 12px; + line-height: 1.5; + color: var(--text-color-light); + } + + #reason-content a { + color: #ff6b6b; + text-decoration: none; + word-break: break-all; + } + + #reason-content a:hover { + text-decoration: underline; + } + /* Note Section Styles */ #note-container { margin-top: 15px; @@ -262,6 +306,17 @@
Checking site status...
+