A community-maintained directory of Bluesky Personal Data Servers (PDS). pdslist.wisp.place

Compare changes

Choose any two refs to compare.

+3
README.md
··· 11 11 "url": "https://ypds.example.com", 12 12 "supportedHandles": ["*.example.com", "*.example.net"], 13 13 "maintainer": "@your-handle.example.com", 14 + "serverLocation": "United States", 14 15 "tosUrl": "https://pds.example.com/terms", 15 16 "privacyUrl": "https://pds.example.com/privacy", 16 17 "inviteCodeRequired": false ··· 24 25 | `url` | string | ✅ Yes | The base URL of your PDS server (must include https://) | 25 26 | `supportedHandles` | array of strings | ✅ Yes | Domain patterns for handles your PDS supports (e.g., `*.example.com`) | 26 27 | `maintainer` | string | ✅ Yes | Bluesky handle of the server maintainer (format: `@handle.domain.com`) | 28 + | `serverLocation` | string | ⚠️ Optional | Geographic location of the server (e.g., `United States`, `European Union`, `Canada`) | 27 29 | `contactEmail` | string | ⚠️ Optional | Contact email for the PDS administrator | 28 30 | `tosUrl` | string | ⚠️ Optional | URL to your Terms of Service page | 29 31 | `privacyUrl` | string | ⚠️ Optional | URL to your Privacy Policy page | ··· 39 41 "*.myserver.org" 40 42 ], 41 43 "maintainer": "@admin.myserver.com", 44 + "serverLocation": "Canada", 42 45 "contactEmail": "admin@myserver.com", 43 46 "tosUrl": "https://pds.myserver.com/terms-of-service", 44 47 "privacyUrl": "https://pds.myserver.com/privacy-policy",
+81 -5
app.js
··· 1 1 // Load PDS data and populate table 2 2 document.addEventListener('DOMContentLoaded', function() { 3 + // Check if mobile and show popup 4 + if (window.innerWidth < 768) { 5 + document.getElementById('mobile-popup').style.display = 'flex'; 6 + } 7 + 8 + // Close mobile popup button 9 + document.getElementById('close-mobile-popup').addEventListener('click', function() { 10 + document.getElementById('mobile-popup').style.display = 'none'; 11 + }); 12 + 13 + // Search input 14 + document.getElementById('search-input').addEventListener('input', filterData); 15 + 16 + // Invite filter radios 17 + document.querySelectorAll('input[name="invite-filter"]').forEach(radio => { 18 + radio.addEventListener('change', filterData); 19 + }); 20 + 3 21 loadPDSData(); 4 22 }); 23 + 24 + // Store all PDS data globally for filtering 25 + let allPDSData = []; 5 26 6 27 async function loadPDSData() { 7 28 const loading = document.getElementById('loading'); ··· 13 34 try { 14 35 const response = await fetch('./pdslist.json'); 15 36 const pdsData = await response.json(); 37 + allPDSData = pdsData; // Store for filtering 16 38 17 39 // Hide loading 18 40 loading.style.display = 'none'; ··· 31 53 const row = createTableRow(pds); 32 54 tableBody.appendChild(row); 33 55 }); 56 + 57 + // Set initial filtered count to total 58 + document.getElementById('filtered-count').textContent = pdsData.length; 34 59 35 60 } catch (error) { 36 61 console.error('Error loading PDS data:', error); ··· 38 63 } 39 64 } 40 65 66 + function filterData() { 67 + const searchTerm = document.getElementById('search-input').value.toLowerCase(); 68 + const inviteFilter = document.querySelector('input[name="invite-filter"]:checked').value; 69 + 70 + const tableBody = document.getElementById('table-body'); 71 + const rows = tableBody.querySelectorAll('tr'); 72 + let visibleCount = 0; 73 + 74 + rows.forEach((row, index) => { 75 + const pds = allPDSData[index]; 76 + if (shouldShowRow(pds, searchTerm, inviteFilter)) { 77 + row.style.display = ''; 78 + visibleCount++; 79 + } else { 80 + row.style.display = 'none'; 81 + } 82 + }); 83 + 84 + // Update the filtered count 85 + document.getElementById('filtered-count').textContent = visibleCount; 86 + } 87 + 88 + function shouldShowRow(pds, searchTerm, inviteFilter) { 89 + // Apply search filter 90 + const searchMatch = !searchTerm || 91 + pds.url.toLowerCase().includes(searchTerm) || 92 + (pds.supportedHandles && pds.supportedHandles.some(h => h.toLowerCase().includes(searchTerm))) || 93 + (pds.maintainer && pds.maintainer.toLowerCase().includes(searchTerm)) || 94 + (pds.contactEmail && pds.contactEmail.toLowerCase().includes(searchTerm)); 95 + 96 + // Apply invite filter 97 + const inviteMatch = inviteFilter === 'all' || 98 + (inviteFilter === 'yes' && pds.inviteCodeRequired) || 99 + (inviteFilter === 'no' && !pds.inviteCodeRequired); 100 + 101 + return searchMatch && inviteMatch; 102 + } 103 + 41 104 function createTableRow(pds) { 42 105 const row = document.createElement('tr'); 43 106 44 107 // URL 45 108 const urlCell = document.createElement('td'); 46 - const urlLink = document.createElement('a'); 47 - urlLink.href = pds.url; 109 + const urlLink = document.createElement('p'); 110 + // sanitize URL by removing any '@' characters 111 + const safeUrl = pds.url ? pds.url.replace(/@/g, '') : ''; 112 + urlLink.href = safeUrl; 48 113 urlLink.target = '_blank'; 49 - urlLink.textContent = pds.url; 114 + urlLink.textContent = safeUrl; 50 115 urlCell.appendChild(urlLink); 51 116 row.appendChild(urlCell); 52 117 ··· 63 128 const maintainerCell = document.createElement('td'); 64 129 if (pds.maintainer) { 65 130 const link = document.createElement('a'); 66 - link.href = `https://madebydanny.uk/followonbsky?did=${pds.maintainer}`; 131 + // sanitize maintainer by removing any '@' characters before using in URL and display 132 + const safeMaintainer = pds.maintainer.replace(/@/g, ''); 133 + link.href = `https://madebydanny.uk/followonbsky.html?did=${encodeURIComponent(safeMaintainer)}`; 67 134 link.target = '_blank'; 68 - link.textContent = pds.maintainer; 135 + link.textContent = safeMaintainer; 69 136 maintainerCell.appendChild(link); 70 137 } else { 71 138 maintainerCell.textContent = '—'; 72 139 } 73 140 row.appendChild(maintainerCell); 141 + 142 + // Server Location 143 + const locationCell = document.createElement('td'); 144 + if (pds.serverLocation) { 145 + locationCell.textContent = pds.serverLocation; 146 + } else { 147 + locationCell.textContent = '—'; 148 + } 149 + row.appendChild(locationCell); 74 150 75 151 // Contact Email 76 152 const emailCell = document.createElement('td');
+77 -28
index.html
··· 3 3 <head> 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <title>Bluesky PDS Directory</title> 6 + <title>PDSList - A database of Bluesky PDSes </title> 7 + <link rel="stylesheet" href="styles.css"> 8 + <meta name="description" content="A database of Bluesky PDSes that are 'open' - by madebydanny.uk"> 9 + <meta property="og:image" content="/media/ogimg.png"> 10 + <link rel="icon" href="/media/database-solid-full.svg" type="image/x-icon"/> 11 + <link rel="shortcut icon" href="/media/database-solid-full.svg" type="image/x-icon"/> 7 12 </head> 8 13 <body> 9 - <h1>Bluesky PDS Directory</h1> 10 - <p>Community-maintained database of Personal Data Servers</p> 11 - <p><b>NOTE: Data on this site is sourced from <a href="https://pdsls.dev">PDSls</a></b></p> 12 - <p><a href="https://tangled.org/madebydanny.uk/pdslist#adding-your-pds">Add a PDS</a></p> 14 + <div class="container"> 15 + <h1>PDSList - A database of Bluesky PDSes </h1> 16 + <p>A database of Bluesky PDSes that are "open"</p> 17 + <p><b>NOTE: Data on this site is sourced from the server's</b> <i>/xrpc/com.atproto.server.describeServer</i> <b>endpoint</b></p> 18 + <p><a href="https://tangled.org/madebydanny.uk/pdslist#adding-your-pds">→ Add Your PDS</a></p> 19 + 20 + <div id="loading"> 21 + <p>Loading database...</p> 22 + </div> 13 23 14 - <div id="loading"> 15 - <p>Loading database...</p> 16 - </div> 24 + <div id="content" style="display: none;"> 25 + <div class="stats"> 26 + <div class="stat"> 27 + <div class="stat-label">Total Servers</div> 28 + <div class="stat-value" id="server-count">0</div> 29 + </div> 30 + <div class="stat"> 31 + <div class="stat-label">Showing</div> 32 + <div class="stat-value" id="filtered-count">0</div> 33 + </div> 34 + </div> 17 35 18 - <div id="content" style="display: none;"> 19 - <p>Total Servers: <strong id="server-count">0</strong></p> 20 - 21 - <table id="pds-table" border="1" cellpadding="10" cellspacing="0"> 22 - <thead> 23 - <tr> 24 - <th>URL</th> 25 - <th>Handles</th> 26 - <th>Maintainer</th> 27 - <th>Email</th> 28 - <th>Invite Code Required</th> 29 - <th>Terms</th> 30 - <th>Privacy</th> 31 - </tr> 32 - </thead> 33 - <tbody id="table-body"> 34 - </tbody> 35 - </table> 36 + <!-- Mobile Popup --> 37 + <div id="mobile-popup" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center;"> 38 + <div style="background: white; padding: 30px; border-radius: 8px; max-width: 400px; text-align: center; box-shadow: 0 4px 20px rgba(0,0,0,0.2);"> 39 + <h2 style="margin-top: 0; color: #0066cc;">Mobile Notice</h2> 40 + <p>This site is best viewed on a desktop for the best experience.</p> 41 + <p style="font-size: 14px; color: #666;">The database table is optimized for larger screens.</p> 42 + <button id="close-mobile-popup" style="padding: 10px 20px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">Got it, continue anyway</button> 43 + </div> 44 + </div> 45 + 46 + <!-- Search and Filters --> 47 + <div style="margin-bottom: 20px; padding: 15px; background: #f0f4f8; border-radius: 4px; border: 1px solid #ddd;"> 48 + <input type="text" id="search-input" placeholder="Search by URL, handles, maintainer, or email..." style="width: 100%; padding: 10px 12px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;"> 49 + 50 + <div style="display: flex; gap: 15px; flex-wrap: wrap; align-items: center;"> 51 + <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> 52 + <input type="radio" name="invite-filter" value="all" checked> 53 + <span>All Servers</span> 54 + </label> 55 + <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> 56 + <input type="radio" name="invite-filter" value="yes"> 57 + <span>Invite Required</span> 58 + </label> 59 + <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> 60 + <input type="radio" name="invite-filter" value="no"> 61 + <span>No Invite Required</span> 62 + </label> 63 + </div> 64 + </div> 65 + 66 + <div style="overflow-x: auto; -webkit-overflow-scrolling: touch;"> 67 + <table id="pds-table" border="1" cellpadding="10" cellspacing="0"> 68 + <thead> 69 + <tr> 70 + <th>URL</th> 71 + <th>Handles</th> 72 + <th>Maintainer</th> 73 + <th>Server Location</th> 74 + <th>Email</th> 75 + <th>Invite Code Needed</th> 76 + <th>Terms</th> 77 + <th>Privacy</th> 78 + </tr> 79 + </thead> 80 + <tbody id="table-body"> 81 + </tbody> 82 + </table> 83 + </div> 36 84 37 - <div id="no-data" style="display: none;"> 38 - <p>No PDS servers found.</p> 85 + <div id="no-data" style="display: none;"> 86 + <p>No PDS servers found.</p> 87 + </div> 39 88 </div> 40 89 </div> 41 90
+1
media/database-solid-full.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#74C0FC" d="M544 269.8C529.2 279.6 512.2 287.5 494.5 293.8C447.5 310.6 385.8 320 320 320C254.2 320 192.4 310.5 145.5 293.8C127.9 287.5 110.8 279.6 96 269.8L96 352C96 396.2 196.3 432 320 432C443.7 432 544 396.2 544 352L544 269.8zM544 192L544 144C544 99.8 443.7 64 320 64C196.3 64 96 99.8 96 144L96 192C96 236.2 196.3 272 320 272C443.7 272 544 236.2 544 192zM494.5 453.8C447.6 470.5 385.9 480 320 480C254.1 480 192.4 470.5 145.5 453.8C127.9 447.5 110.8 439.6 96 429.8L96 496C96 540.2 196.3 576 320 576C443.7 576 544 540.2 544 496L544 429.8C529.2 439.6 512.2 447.5 494.5 453.8z"/></svg>
media/ogimg.png

This is a binary file and will not be displayed.

+49 -29
pdslist.json
··· 1 1 [ 2 2 { 3 - "url": "https://bsky.social", 4 - "supportedHandles": [ 5 - "*.bsky.social" 6 - ], 7 - "maintainer": "@bsky.app", 8 - "tosUrl": "https://bsky.social/about/support/tos", 9 - "privacyUrl": "https://bsky.social/about/support/privacy-policy", 10 - "inviteCodeRequired": false 11 - }, 12 - { 13 - "url": "https://pds.madebydanny.uk", 3 + "url": "pds.madebydanny.uk", 14 4 "supportedHandles": [ 15 5 ".pds.madebydanny.uk", 16 6 ".pds.danielmorrisey.com", 17 7 ".good-example.com", 18 - ".mbdio.uk.", 19 - "certifiedshitposter.com" 8 + ".mbdio.uk", 9 + ".certifiedshitposter.com" 20 10 ], 11 + "serverLocation": "New York, US", 21 12 "contactEmail": "danielmorrisey@pm.me", 22 13 "maintainer": "@madebydanny.uk", 23 14 "tosUrl": "https://pds.madebydanny.uk/about/terms.html", ··· 25 16 "inviteCodeRequired": false 26 17 }, 27 18 { 28 - "url": "https://blacksky.app", 19 + "url": "blacksky.app", 29 20 "supportedHandles": [ 30 21 ".myatproto.social", 31 22 ".blacksky.app", ··· 38 29 "inviteCodeRequired": false 39 30 }, 40 31 { 41 - "url": "https://northsky.social", 32 + "url": "northsky.social", 42 33 "supportedHandles": [ 43 34 ".northsky.social" 44 35 ], ··· 48 39 "inviteCodeRequired": true 49 40 }, 50 41 { 51 - "url": "https://selfhosted.social", 42 + "url": "selfhosted.social", 52 43 "supportedHandles": [ 53 44 ".selfhosted.social" 54 45 ], ··· 59 50 "inviteCodeRequired": false 60 51 }, 61 52 { 62 - "url": "https://altq.net", 53 + "url": "altq.net", 63 54 "supportedHandles": [ 64 55 ".altq.net" 65 56 ], ··· 67 58 "inviteCodeRequired": "true" 68 59 }, 69 60 { 70 - "url": "https://tngl.sh", 61 + "url": "tngl.sh", 71 62 "supportedHandles": [ 72 63 ".tngl.sh" 73 64 ], 65 + "contactEmail": "team@tangled.org", 74 66 "maintainer": "@tangled.org", 67 + "privacyUrl": "https://tangled.org/privacy", 68 + "tosUrl": "https://tangled.org/terms", 75 69 "inviteCodeRequired": "true" 76 70 }, 77 71 { 78 - "url": "https://pds.tgirl.cloud", 72 + "url": "pds.tgirl.cloud", 79 73 "supportedHandles": [ 80 74 ".tgirl.beauty" 81 75 ], 82 - "maintainer": "tgirl.cloud", 76 + "maintainer": "@tgirl.cloud", 83 77 "inviteCodeRequired": "true" 84 78 }, 85 79 { 86 - "url": "https://pds.witchcraft.systems", 80 + "url": "pds.witchcraft.systems", 87 81 "supportedHandles": [ 88 82 ".pds.witchcraft.systems" 89 83 ], ··· 91 85 "inviteCodeRequired": "true" 92 86 }, 93 87 { 94 - "url": "https://evil.gay", 88 + "url": "evil.gay", 95 89 "supportedHandles": [ 96 90 ".evil.gay", 97 91 ".pds.mmatt.net" ··· 100 94 "inviteCodeRequired": "true" 101 95 }, 102 96 { 103 - "url": "https://totallynotseth.dev", 97 + "url": "totallynotseth.dev", 104 98 "supportedHandles": [ 105 99 ".totallynotseth.dev" 106 100 ], ··· 108 102 "inviteCodeRequired": "true" 109 103 }, 110 104 { 111 - "url": "https://pds.atpota.to", 105 + "url": "pds.atpota.to", 112 106 "supportedHandles": [ 113 107 ".flush.es", 114 108 ".on.anisota.net" ··· 117 111 "inviteCodeRequired": "true" 118 112 }, 119 113 { 120 - "url": "https://katproto.girlonthemoon.xyz", 114 + "url": "katproto.girlonthemoon.xyz", 121 115 "supportedHandles": [ 122 116 ".katproto.girlonthemoon.xyz" 123 117 ], ··· 126 120 "inviteCodeRequired": "true" 127 121 }, 128 122 { 129 - "url": "https://pds.tophhie.cloud", 123 + "url": "pds.tophhie.cloud", 130 124 "supportedHandles": [ 131 125 ".tophhie.social" 132 126 ], ··· 137 131 "inviteCodeRequired": false 138 132 }, 139 133 { 140 - "url": "https://pds.sprk.so", 134 + "url": "pds.sprk.so", 141 135 "supportedHandles": [ 142 136 ".sprk.so" 143 137 ], ··· 148 142 "inviteCodeRequired": false 149 143 }, 150 144 { 151 - "url": "https://zio.blue", 145 + "url": "zio.blue", 152 146 "supportedHandles": [ 153 147 ".zio.blue" 154 148 ], ··· 156 150 "inviteCodeRequired": false 157 151 }, 158 152 { 159 - "url": "https://peedee.es", 153 + "url": "peedee.es", 160 154 "supportedHandles": [ 161 155 ".peedee.es" 162 156 ], ··· 164 158 "maintainer": "@angrydutchman.peedee.es", 165 159 "privacyUrl": "https://peedee.es/@peedee/md/privacy.html", 166 160 "tosUrl": "https://peedee.es/@peedee/md/terms.html", 161 + "inviteCodeRequired": true 162 + }, 163 + { 164 + "url": "caramelo.social.br", 165 + "supportedHandles": [ 166 + ".caramelo.social.br" 167 + ], 168 + "contactEmail": "atlas@caramelo.social.br", 169 + "maintainer": "@luan.caramelo.social.br", 170 + "inviteCodeRequired": true 171 + }, 172 + { 173 + "url": "micro.blog.br", 174 + "supportedHandles": [ 175 + ".micro.blog.br" 176 + ], 177 + "maintainer": "@joseli.to", 178 + "inviteCodeRequired": true 179 + }, 180 + { 181 + "url": "at.app.wafrn.net", 182 + "supportedHandles": [ 183 + ".at.app.wafrn.net" 184 + ], 185 + "contactEmail": "info@wafrn.net", 186 + "privacyUrl": "https://app.wafrn.net/article/system.privacy-policy", 167 187 "inviteCodeRequired": true 168 188 } 169 189 ]
+326 -118
styles.css
··· 1 1 * { 2 - margin: 0; 3 - padding: 0; 4 - box-sizing: border-box; 2 + margin: 0; 3 + padding: 0; 4 + box-sizing: border-box; 5 5 } 6 6 7 7 body { 8 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 9 - background-color: #f5f5f5; 10 - color: #333; 11 - line-height: 1.6; 8 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 9 + padding: 20px; 10 + background-color: #f8f9fa; 11 + color: #333; 12 + line-height: 1.6; 12 13 } 13 14 14 15 .container { 15 - max-width: 1200px; 16 - margin: 0 auto; 17 - padding: 20px; 16 + max-width: 1200px; 17 + margin: 0 auto; 18 + background-color: white; 19 + padding: 30px; 20 + border-radius: 8px; 21 + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); 18 22 } 19 23 20 - header { 21 - background-color: white; 22 - padding: 30px 0; 23 - margin-bottom: 30px; 24 - border-bottom: 2px solid #e0e0e0; 24 + h1 { 25 + color: #1a1a1a; 26 + margin-bottom: 10px; 27 + font-size: 28px; 25 28 } 26 29 27 - h1 { 28 - font-size: 32px; 29 - margin-bottom: 10px; 30 + h2 { 31 + color: #0066cc; 32 + margin-top: 20px; 33 + margin-bottom: 15px; 34 + font-size: 22px; 30 35 } 31 36 32 - .subtitle { 33 - color: #666; 34 - font-size: 16px; 35 - margin-bottom: 20px; 37 + p { 38 + margin-bottom: 15px; 39 + color: #555; 36 40 } 37 41 38 - .header-links { 39 - display: flex; 40 - gap: 15px; 42 + a { 43 + color: #0066cc; 44 + text-decoration: none; 45 + transition: color 0.2s; 41 46 } 42 47 43 - .header-link { 44 - color: #0066cc; 45 - text-decoration: none; 46 - font-size: 14px; 48 + a:hover { 49 + color: #0052a3; 50 + text-decoration: underline; 47 51 } 48 52 49 - .header-link:hover { 50 - text-decoration: underline; 53 + #loading { 54 + text-align: center; 55 + padding: 40px; 56 + font-size: 18px; 57 + color: #666; 51 58 } 52 59 53 - .loading { 54 - text-align: center; 55 - padding: 40px; 60 + #content { 61 + padding: 20px 0; 56 62 } 57 63 58 - .spinner { 59 - border: 4px solid #e0e0e0; 60 - border-top: 4px solid #0066cc; 61 - border-radius: 50%; 62 - width: 40px; 63 - height: 40px; 64 - animation: spin 1s linear infinite; 65 - margin: 0 auto 20px; 64 + /* Tables */ 65 + table { 66 + width: 100%; 67 + border-collapse: collapse; 68 + margin: 20px 0; 69 + font-size: 14px; 70 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 71 + overflow-x: auto; 72 + display: block; 66 73 } 67 74 68 - @keyframes spin { 69 - 0% { transform: rotate(0deg); } 70 - 100% { transform: rotate(360deg); } 75 + table th, 76 + table td { 77 + padding: 12px; 78 + text-align: left; 79 + border-bottom: 1px solid #e0e0e0; 71 80 } 72 81 73 - .content { 74 - background-color: white; 75 - padding: 20px; 76 - border-radius: 4px; 82 + table th { 83 + background-color: #0066cc; 84 + color: white; 85 + font-weight: 600; 86 + border-bottom: 2px solid #0052a3; 87 + position: sticky; 88 + top: 0; 77 89 } 78 90 79 - .content.hidden, 80 - .no-data.hidden, 81 - #loading.hidden { 82 - display: none; 91 + table tr { 92 + display: table; 93 + width: 100%; 94 + table-layout: fixed; 83 95 } 84 96 85 - .stats { 86 - margin-bottom: 20px; 87 - padding-bottom: 20px; 88 - border-bottom: 1px solid #e0e0e0; 97 + table tr:hover { 98 + background-color: #f5f5f5; 89 99 } 90 100 91 - .stat-item { 92 - font-size: 16px; 101 + table a { 102 + color: #0066cc; 103 + word-break: break-all; 93 104 } 94 105 95 - .stat-item strong { 96 - font-weight: 600; 106 + table a:hover { 107 + text-decoration: underline; 97 108 } 98 109 99 - .pds-table { 100 - width: 100%; 101 - border-collapse: collapse; 102 - margin-top: 20px; 110 + /* Make Terms and Privacy columns smaller */ 111 + table th:nth-child(7), 112 + table th:nth-child(8), 113 + table td:nth-child(7), 114 + table td:nth-child(8) { 115 + padding: 4px 2px; 116 + font-size: 12px; 117 + text-align: center; 118 + width: 60px; 119 + } 120 + table th:nth-child(6), 121 + table td:nth-child(6), 122 + table th:nth-child(4), 123 + table td:nth-child(4) 124 + { 125 + padding: 4px 2px; 126 + font-size: 12px; 127 + text-align: center; 128 + width: 90px; 129 + } 130 + /* Responsive Table */ 131 + @media (max-width: 1024px) { 132 + table { 133 + font-size: 13px; 134 + } 135 + 136 + table th, 137 + table td { 138 + padding: 10px 8px; 139 + } 140 + table th:nth-child(6), 141 + table td:nth-child(6), 142 + table th:nth-child(4), 143 + table td:nth-child(4), 144 + table th:nth-child(7), 145 + table th:nth-child(8), 146 + table td:nth-child(7), 147 + table td:nth-child(8) { 148 + padding: 8px 4px; 149 + width: 50px; 150 + } 103 151 } 104 152 105 - .pds-table thead { 106 - background-color: #f9f9f9; 107 - border-bottom: 2px solid #e0e0e0; 153 + @media (max-width: 768px) { 154 + table { 155 + font-size: 12px; 156 + } 157 + 158 + table th, 159 + table td { 160 + padding: 8px 6px; 161 + word-break: break-word; 162 + } 163 + 164 + /* Hide certain columns on mobile */ 165 + table th:nth-child(4), 166 + table th:nth-child(5), 167 + table th:nth-child(7), 168 + table th:nth-child(8), 169 + table td:nth-child(4), 170 + table td:nth-child(5), 171 + table td:nth-child(7), 172 + table td:nth-child(8) { 173 + display: none; 174 + } 175 + 176 + /* Make URL and Maintainer wider on mobile */ 177 + table th:nth-child(1), 178 + table th:nth-child(3), 179 + table td:nth-child(1), 180 + table td:nth-child(3) { 181 + min-width: 100px; 182 + } 183 + 184 + .container { 185 + padding: 15px; 186 + } 187 + 188 + h1 { 189 + font-size: 20px; 190 + } 108 191 } 109 192 110 - .pds-table th { 111 - text-align: left; 112 - padding: 15px; 113 - font-weight: 600; 114 - font-size: 14px; 115 - text-transform: uppercase; 116 - color: #666; 193 + @media (max-width: 480px) { 194 + table { 195 + font-size: 11px; 196 + } 197 + 198 + table th, 199 + table td { 200 + padding: 6px 4px; 201 + } 202 + 203 + /* On very small screens, show only essential columns */ 204 + table th:nth-child(2), 205 + table th:nth-child(6), 206 + table td:nth-child(2), 207 + table td:nth-child(6) { 208 + display: none; 209 + } 210 + 211 + /* Make columns more compact */ 212 + table th:nth-child(1), 213 + table th:nth-child(3), 214 + table td:nth-child(1), 215 + table td:nth-child(3) { 216 + min-width: 80px; 217 + max-width: 150px; 218 + } 219 + 220 + .container { 221 + padding: 10px; 222 + } 223 + 224 + h1 { 225 + font-size: 18px; 226 + } 227 + 228 + p { 229 + font-size: 13px; 230 + } 117 231 } 118 232 119 - .pds-table td { 120 - padding: 12px 15px; 121 - border-bottom: 1px solid #e0e0e0; 122 - font-size: 14px; 123 - vertical-align: top; 233 + /* Buttons */ 234 + button { 235 + padding: 12px 16px; 236 + border: none; 237 + border-radius: 4px; 238 + cursor: pointer; 239 + font-size: 15px; 240 + font-weight: 600; 241 + transition: all 0.3s ease; 242 + width: 100%; 243 + margin-bottom: 10px; 124 244 } 125 245 126 - .pds-table tbody tr:hover { 127 - background-color: #f9f9f9; 246 + button:hover { 247 + transform: translateY(-2px); 248 + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); 128 249 } 129 250 130 - .pds-table a { 131 - color: #0066cc; 132 - text-decoration: none; 133 - word-break: break-all; 251 + #toggle-pds { 252 + background: #0066cc; 253 + color: white; 134 254 } 135 255 136 - .pds-table a:hover { 137 - text-decoration: underline; 256 + #toggle-pds:hover { 257 + background: #0052a3; 138 258 } 139 259 140 - .handles-container { 141 - display: flex; 142 - flex-wrap: wrap; 143 - gap: 6px; 260 + #toggle-bsky { 261 + background: #0066cc; 262 + color: white; 144 263 } 145 264 146 - .handle-tag { 147 - background-color: #e8f0ff; 148 - color: #0066cc; 149 - padding: 4px 8px; 150 - border-radius: 3px; 151 - font-size: 12px; 152 - white-space: nowrap; 153 - border: 1px solid #cce0ff; 265 + #toggle-bsky:hover { 266 + background: #0052a3; 154 267 } 155 268 156 - .link-button { 157 - display: inline-block; 158 - background-color: #0066cc; 159 - color: white; 160 - padding: 6px 12px; 161 - border-radius: 3px; 162 - font-size: 12px; 163 - text-decoration: none; 164 - transition: background-color 0.2s; 269 + /* Sections */ 270 + #pds-section, 271 + #bsky-section, 272 + #cool-section { 273 + margin-top: 15px; 274 + padding: 15px; 275 + background: #f9f9f9; 276 + border-radius: 4px; 277 + border-left: 4px solid #0066cc; 165 278 } 166 279 167 - .link-button:hover { 168 - background-color: #0052a3; 169 - text-decoration: none; 280 + /* Search and Filters */ 281 + #search-input { 282 + width: 100%; 283 + padding: 10px 12px; 284 + margin-bottom: 10px; 285 + border: 1px solid #ddd; 286 + border-radius: 4px; 287 + font-size: 14px; 288 + } 289 + 290 + #search-input:focus { 291 + outline: none; 292 + border-color: #0066cc; 293 + box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1); 294 + } 295 + 296 + #filters { 297 + margin-bottom: 20px; 298 + padding: 15px; 299 + background: #f0f4f8; 300 + border-radius: 4px; 301 + border: 1px solid #ddd; 302 + } 303 + 304 + #filters label { 305 + display: inline-flex; 306 + align-items: center; 307 + gap: 8px; 308 + margin-right: 15px; 309 + cursor: pointer; 310 + color: #333; 311 + } 312 + 313 + #filters input[type="radio"] { 314 + cursor: pointer; 315 + width: 18px; 316 + height: 18px; 317 + } 318 + 319 + /* Mobile Popup Styles */ 320 + #mobile-popup { 321 + backdrop-filter: blur(4px); 322 + } 323 + 324 + #mobile-popup button { 325 + padding: 10px 20px !important; 326 + width: auto !important; 327 + margin: 0 !important; 328 + } 329 + 330 + /* Info boxes */ 331 + #no-data { 332 + text-align: center; 333 + padding: 40px 20px; 334 + color: #999; 335 + font-size: 16px; 170 336 } 171 337 172 - .no-data { 173 - text-align: center; 174 - padding: 40px; 175 - color: #999; 338 + .stats { 339 + display: flex; 340 + gap: 20px; 341 + margin-bottom: 20px; 342 + flex-wrap: wrap; 343 + } 344 + 345 + .stat { 346 + padding: 15px; 347 + background: linear-gradient(135deg, #0066cc 0%, #0052a3 100%); 348 + color: white; 349 + border-radius: 4px; 350 + font-weight: 600; 351 + min-width: 200px; 352 + } 353 + 354 + .stat-label { 355 + font-size: 12px; 356 + opacity: 0.9; 357 + margin-bottom: 5px; 358 + } 359 + 360 + .stat-value { 361 + font-size: 24px; 176 362 } 177 363 178 - .hidden { 179 - display: none; 364 + @media (max-width: 768px) { 365 + .stat { 366 + min-width: 150px; 367 + flex: 1; 368 + } 369 + 370 + .stat-value { 371 + font-size: 20px; 372 + } 180 373 } 374 + 375 + @media (max-width: 480px) { 376 + .stats { 377 + gap: 10px; 378 + } 379 + 380 + .stat { 381 + min-width: 120px; 382 + padding: 12px; 383 + } 384 + 385 + .stat-value { 386 + font-size: 18px; 387 + } 388 + }