diff --git a/pytorrent/static/js/mobileTorrentDetails.js b/pytorrent/static/js/mobileTorrentDetails.js
index f046fd4..bdb4987 100644
--- a/pytorrent/static/js/mobileTorrentDetails.js
+++ b/pytorrent/static/js/mobileTorrentDetails.js
@@ -1 +1 @@
-export const mobileTorrentDetailsSource = " function mobileDetailValue(value, fallback='-'){\n const text = value === null || value === undefined || value === '' ? fallback : String(value);\n return esc(text);\n }\n function mobileDetailsStatCards(t){\n const stats = [\n ['Status', t.status || '-'],\n ['Progress', `${Number(t.progress || 0)}%`],\n ['Size', t.size_h || '-'],\n ['Downloaded', t.down_total_h || '-'],\n ['Uploaded', t.up_total_h || '-'],\n ['DL / UL', `${t.down_rate_h || '-'} / ${t.up_rate_h || '-'}`],\n ['Seeds / Peers', `${t.seeds ?? 0} / ${t.peers ?? 0}`],\n ['Ratio', t.ratio ?? '-'],\n ['ETA', t.eta_h || '-'],\n ['Added', formatDateTime(t.created)],\n ];\n return stats.map(([label,value]) => `
${esc(label)}${mobileDetailValue(value)}
`).join('');\n }\n function mobileDetailsPeerRows(peers){\n // Note: Mobile peers use the same responsive table wrapper as desktop details for consistent spacing and scrolling.\n return (peers || []).slice(0, 40).map(p => {\n const location = [p.country, p.city].filter(Boolean).join(', ') || '-';\n const ip = `${esc(p.ip || '-')}`;\n const row = [flag(p.country_iso), ip];\n if(reverseDnsEnabled) row.push(peerHostCell(p));\n row.push(esc(location), esc(p.client || '-'), progressBar(p.completed || 0, 'peer-progress peer-progress-wide'), esc(p.down_rate_h || '-'), esc(p.up_rate_h || '-'), esc(p.port || '-'), peerBadges(p));\n return row;\n });\n }\n function scheduleMobileReverseDnsRefresh(hash, torrent, payload){\n // Note: Mobile details refresh only the peers payload while preserving already-loaded files and trackers.\n clearTimeout(mobileReverseDnsRefreshTimer);\n if(!hasPendingReverseDns(payload.peers?.value?.peers || [])){ mobileReverseDnsRefreshAttempts=0; return; }\n if(!hash || mobileReverseDnsRefreshAttempts>=REVERSE_DNS_REFRESH_MAX_ATTEMPTS) return;\n mobileReverseDnsRefreshAttempts+=1;\n mobileReverseDnsRefreshTimer=setTimeout(async()=>{\n if(selectedHash!==hash || !$('mobileDetailsModal')?.classList.contains('show')) return;\n try{\n const peersJson=await fetchMobileDetailsJson(hash, 'peers');\n const nextPayload={...payload, peers:{status:'fulfilled', value:peersJson}};\n const body=$('mobileDetailsBody');\n if(body && selectedHash===hash){\n body.innerHTML=renderMobileDetailsContent(torrent, nextPayload);\n scheduleMobileReverseDnsRefresh(hash, torrent, nextPayload);\n }\n }catch(_){ }\n }, REVERSE_DNS_REFRESH_SECONDS*1000);\n }\n function mobileDetailsPeerTable(peers){\n const headers = ['Flag', 'IP'];\n if(reverseDnsEnabled) headers.push('Host');\n headers.push('Location', 'Client', '%', 'DL', 'UL', 'Port', 'Flags');\n const rows = mobileDetailsPeerRows(peers);\n if(!rows.length) return 'No peers returned by rTorrent.
';\n return responsiveTable(headers, rows, reverseDnsEnabled ? 'peers-table mobile-details-peers-table peers-table-hosts' : 'peers-table mobile-details-peers-table');\n }\n function mobileDetailsFileTable(files){\n const rows = (files || []).map(file => {\n const priority = FILE_PRIORITY_LABELS[Number(file.priority || 0)] || file.priority || '-';\n const actions = `${renderFileInfoButton(file)}
`;\n return [\n `${esc(file.path || file.name || '-')}`,\n esc(file.size_h || '-'),\n progressBar(file.progress ?? 0, 'file-progress'),\n `${esc(priority)}`,\n renderFilePrioritySelect(file),\n actions,\n ];\n });\n // Note: Mobile files now reuse the same compact table pattern as peers, with per-file priority, state, info and download actions.\n if(!rows.length) return 'No files returned by rTorrent.
';\n return responsiveTable(['Path', 'Size', 'Done', 'Priority', 'Set priority', 'Actions'], rows, 'file-priority-table mobile-details-files-table');\n }\n function mobileDetailsTrackerRows(trackers){\n // Note: Mobile trackers mirror the desktop tracker table instead of using a separate list layout.\n return (trackers || []).slice(0, 12).map(t => [\n `#${esc(t.index)}`,\n trackerUrlCell(t),\n trackerEnabledCell(t.enabled),\n esc(trackerSeedsPeers(t)),\n esc(t.downloaded ?? '-'),\n fmtTs(t.last_announce),\n ]);\n }\n function mobileDetailsTrackerTable(trackers){\n const rows = mobileDetailsTrackerRows(trackers);\n if(!rows.length) return 'No trackers returned by rTorrent.
';\n return responsiveTable(['#', 'URL', 'On', 'Seeds / Peers', 'Done', 'Last announce'], rows, 'tracker-table mobile-details-trackers-table');\n }\n function mobileDetailsSection(title, icon, body, meta='', options={}){\n const collapsed = !!options.collapsed;\n const titleMarkup = ` ${esc(title)}${meta ? `${esc(meta)}` : ''}
`;\n if(collapsed){\n // Note: Heavy mobile sections start collapsed to keep torrent details quick to scan on phones.\n return `${titleMarkup}
${body} `;\n }\n return ``;\n }\n function ensureMobileDetailsModal(){\n let modal = $('mobileDetailsModal');\n if(modal) return modal;\n // Note: Mobile torrent details are lazy-created so the desktop details pane and existing tabs stay unchanged.\n modal = document.createElement('div');\n modal.id = 'mobileDetailsModal';\n modal.className = 'modal fade mobile-details-modal';\n modal.tabIndex = -1;\n modal.innerHTML = ` Loading torrent details...
`;\n document.body.appendChild(modal);\n return modal;\n }\n function renderMobileDetailsContent(t, payload){\n const peers = payload.peers?.status === 'fulfilled' ? (payload.peers.value.peers || []) : [];\n const files = payload.files?.status === 'fulfilled' ? (payload.files.value.files || []) : [];\n const trackers = payload.trackers?.status === 'fulfilled' ? (payload.trackers.value.trackers || []) : [];\n const failures = ['peers','files','trackers'].filter(key => payload[key]?.status === 'rejected').map(key => `${key}: ${payload[key].reason?.message || 'failed'}`);\n const fullPath = joinRemotePath(t.path, t.name);\n const peerTable = mobileDetailsPeerTable(peers);\n const fileTable = mobileDetailsFileTable(files);\n const trackerTable = mobileDetailsTrackerTable(trackers);\n const generalBody = `${esc(t.name || '-')}
Path${esc(fullPath)}
Hash${esc(t.hash || '-')}
${mobileDetailsStatCards(t)}
`;\n const messageBody = `${esc(t.message || 'No message.')}
`;\n const errorBox = failures.length ? `Partial details loaded${esc(failures.join(' | '))}
` : '';\n // Note: General and heavy lists start collapsed on mobile so the modal opens cleanly and the user expands only the section needed.\n return `${errorBox}${mobileDetailsSection('General', 'fa-circle-info', generalBody, '', {collapsed:true})}${mobileDetailsSection('Peers', 'fa-users', peerTable, peers.length > 40 ? `showing 40 of ${peers.length}` : `${peers.length} total`, {collapsed:true})}${mobileDetailsSection('Files', 'fa-folder-tree', fileTable, files.length ? `${files.length} total` : '', {collapsed:true})}${mobileDetailsSection('Trackers', 'fa-bullhorn', trackerTable, trackers.length > 12 ? `showing 12 of ${trackers.length}` : `${trackers.length} total`, {collapsed:true})}${mobileDetailsSection('Message', 'fa-message', messageBody, '', {collapsed:true})}`;\n }\n async function fetchMobileDetailsJson(hash, tab){\n const res = await fetch(`/api/torrents/${encodeURIComponent(hash)}/${tab}`, {headers:{'Accept':'application/json'}});\n const json = await res.json().catch(() => ({}));\n if(!res.ok || !json.ok) throw new Error(json.error || `HTTP ${res.status}`);\n return json;\n }\n async function openMobileDetails(hash){\n const t = torrents.get(hash);\n if(!t) return toast('Torrent is no longer available.','warning');\n selectedHash = hash;\n lastSelectedHash = hash;\n const modal = ensureMobileDetailsModal();\n const title = $('mobileDetailsTitle');\n const subtitle = $('mobileDetailsSubtitle');\n const body = $('mobileDetailsBody');\n if(title) title.innerHTML = ' Torrent details';\n if(subtitle) subtitle.textContent = t.name || hash;\n if(body) body.innerHTML = ' Loading peers, files and trackers...
';\n new bootstrap.Modal(modal).show();\n try{\n // Note: The mobile modal reads existing lightweight detail endpoints without changing the desktop details tabs.\n const [peers, files, trackers] = await Promise.allSettled([\n fetchMobileDetailsJson(hash, 'peers'),\n fetchMobileDetailsJson(hash, 'files'),\n fetchMobileDetailsJson(hash, 'trackers'),\n ]);\n if(body) body.innerHTML = renderMobileDetailsContent(t, {peers, files, trackers});\n mobileReverseDnsRefreshAttempts=0;\n scheduleMobileReverseDnsRefresh(hash, t, {peers, files, trackers});\n }catch(e){\n if(body) body.innerHTML = `Details failed${esc(e.message)}
`;\n }\n }\n\n";
+export const mobileTorrentDetailsSource = " function mobileDetailValue(value, fallback='-'){\n const text = value === null || value === undefined || value === '' ? fallback : String(value);\n return esc(text);\n }\n function mobileDetailsStatCards(t){\n const stats = [\n ['Status', t.status || '-'],\n ['Progress', `${Number(t.progress || 0)}%`],\n ['Size', t.size_h || '-'],\n ['Downloaded', t.down_total_h || '-'],\n ['Uploaded', t.up_total_h || '-'],\n ['DL / UL', `${t.down_rate_h || '-'} / ${t.up_rate_h || '-'}`],\n ['Seeds / Peers', `${t.seeds ?? 0} / ${t.peers ?? 0}`],\n ['Ratio', t.ratio ?? '-'],\n ['ETA', t.eta_h || '-'],\n ['Added', formatDateTime(t.created)],\n ];\n return stats.map(([label,value]) => `${esc(label)}${mobileDetailValue(value)}
`).join('');\n }\n function mobileDetailsPeerRows(peers){\n // Note: Mobile peers use the same responsive table wrapper as desktop details for consistent spacing and scrolling.\n return (peers || []).slice(0, 40).map(p => {\n const location = [p.country, p.city].filter(Boolean).join(', ') || '-';\n const ip = `${esc(p.ip || '-')}`;\n const row = [flag(p.country_iso), ip];\n if(reverseDnsEnabled) row.push(peerHostCell(p));\n row.push(esc(location), esc(p.client || '-'), progressBar(p.completed || 0, 'peer-progress peer-progress-wide'), esc(p.down_rate_h || '-'), esc(p.up_rate_h || '-'), esc(p.port || '-'), peerBadges(p));\n return row;\n });\n }\n function scheduleMobileReverseDnsRefresh(hash, torrent, payload){\n // Note: Mobile details refresh only the peers payload while preserving already-loaded files and trackers.\n clearTimeout(mobileReverseDnsRefreshTimer);\n if(!hasPendingReverseDns(payload.peers?.value?.peers || [])){ mobileReverseDnsRefreshAttempts=0; return; }\n if(!hash || mobileReverseDnsRefreshAttempts>=REVERSE_DNS_REFRESH_MAX_ATTEMPTS) return;\n mobileReverseDnsRefreshAttempts+=1;\n mobileReverseDnsRefreshTimer=setTimeout(async()=>{\n if(selectedHash!==hash || !$('mobileDetailsModal')?.classList.contains('show')) return;\n try{\n const peersJson=await fetchMobileDetailsJson(hash, 'peers');\n const nextPayload={...payload, peers:{status:'fulfilled', value:peersJson}};\n const body=$('mobileDetailsBody');\n if(body && selectedHash===hash){\n body.innerHTML=renderMobileDetailsContent(torrent, nextPayload);\n scheduleMobileReverseDnsRefresh(hash, torrent, nextPayload);\n }\n }catch(_){ }\n }, REVERSE_DNS_REFRESH_SECONDS*1000);\n }\n function mobileDetailsPeerTable(peers){\n const headers = ['Flag', 'IP'];\n if(reverseDnsEnabled) headers.push('Host');\n headers.push('Location', 'Client', '%', 'DL', 'UL', 'Port', 'Flags');\n const rows = mobileDetailsPeerRows(peers);\n if(!rows.length) return 'No peers returned by rTorrent.
';\n return responsiveTable(headers, rows, reverseDnsEnabled ? 'peers-table peers-table-hosts' : 'peers-table');\n }\n function mobileDetailsFileTable(files){\n const rows = (files || []).map(file => {\n const priority = FILE_PRIORITY_LABELS[Number(file.priority || 0)] || file.priority || '-';\n const actions = `${renderFileInfoButton(file)}
`;\n return [\n `${esc(file.path || file.name || '-')}`,\n esc(file.size_h || '-'),\n progressBar(file.progress ?? 0, 'file-progress'),\n `${esc(priority)}`,\n renderFilePrioritySelect(file),\n actions,\n ];\n });\n // Note: Mobile files now reuse the same compact table pattern as peers, with per-file priority, state, info and download actions.\n if(!rows.length) return 'No files returned by rTorrent.
';\n return responsiveTable(['Path', 'Size', 'Done', 'Priority', 'Set priority', 'Actions'], rows, 'file-priority-table');\n }\n function mobileDetailsTrackerRows(trackers){\n // Note: Mobile trackers mirror the desktop tracker table instead of using a separate list layout.\n return (trackers || []).slice(0, 12).map(t => [\n `#${esc(t.index)}`,\n trackerUrlCell(t),\n trackerEnabledCell(t.enabled),\n esc(trackerSeedsPeers(t)),\n esc(t.downloaded ?? '-'),\n fmtTs(t.last_announce),\n ]);\n }\n function mobileDetailsTrackerTable(trackers){\n const rows = mobileDetailsTrackerRows(trackers);\n if(!rows.length) return 'No trackers returned by rTorrent.
';\n return responsiveTable(['#', 'URL', 'On', 'Seeds / Peers', 'Done', 'Last announce'], rows, 'tracker-table');\n }\n function mobileDetailsSection(title, icon, body, meta='', options={}){\n const collapsed = !!options.collapsed;\n const titleMarkup = ` ${esc(title)}${meta ? `${esc(meta)}` : ''}
`;\n if(collapsed){\n // Note: Heavy mobile sections start collapsed to keep torrent details quick to scan on phones.\n return `${titleMarkup}
${body} `;\n }\n return ``;\n }\n function ensureMobileDetailsModal(){\n let modal = $('mobileDetailsModal');\n if(modal) return modal;\n // Note: Mobile torrent details are lazy-created so the desktop details pane and existing tabs stay unchanged.\n modal = document.createElement('div');\n modal.id = 'mobileDetailsModal';\n modal.className = 'modal fade mobile-details-modal';\n modal.tabIndex = -1;\n modal.innerHTML = ` Loading torrent details...
`;\n document.body.appendChild(modal);\n return modal;\n }\n function renderMobileDetailsContent(t, payload){\n const peers = payload.peers?.status === 'fulfilled' ? (payload.peers.value.peers || []) : [];\n const files = payload.files?.status === 'fulfilled' ? (payload.files.value.files || []) : [];\n const trackers = payload.trackers?.status === 'fulfilled' ? (payload.trackers.value.trackers || []) : [];\n const failures = ['peers','files','trackers'].filter(key => payload[key]?.status === 'rejected').map(key => `${key}: ${payload[key].reason?.message || 'failed'}`);\n const fullPath = joinRemotePath(t.path, t.name);\n const peerTable = mobileDetailsPeerTable(peers);\n const fileTable = mobileDetailsFileTable(files);\n const trackerTable = mobileDetailsTrackerTable(trackers);\n const generalBody = `${esc(t.name || '-')}
Path${esc(fullPath)}
Hash${esc(t.hash || '-')}
${mobileDetailsStatCards(t)}
`;\n const messageBody = `${esc(t.message || 'No message.')}
`;\n const errorBox = failures.length ? `Partial details loaded${esc(failures.join(' | '))}
` : '';\n // Note: General and heavy lists start collapsed on mobile so the modal opens cleanly and the user expands only the section needed.\n return `${errorBox}${mobileDetailsSection('General', 'fa-circle-info', generalBody, '', {collapsed:true})}${mobileDetailsSection('Peers', 'fa-users', peerTable, peers.length > 40 ? `showing 40 of ${peers.length}` : `${peers.length} total`, {collapsed:true})}${mobileDetailsSection('Files', 'fa-folder-tree', fileTable, files.length ? `${files.length} total` : '', {collapsed:true})}${mobileDetailsSection('Trackers', 'fa-bullhorn', trackerTable, trackers.length > 12 ? `showing 12 of ${trackers.length}` : `${trackers.length} total`, {collapsed:true})}${mobileDetailsSection('Message', 'fa-message', messageBody, '', {collapsed:true})}`;\n }\n async function fetchMobileDetailsJson(hash, tab){\n const res = await fetch(`/api/torrents/${encodeURIComponent(hash)}/${tab}`, {headers:{'Accept':'application/json'}});\n const json = await res.json().catch(() => ({}));\n if(!res.ok || !json.ok) throw new Error(json.error || `HTTP ${res.status}`);\n return json;\n }\n async function openMobileDetails(hash){\n const t = torrents.get(hash);\n if(!t) return toast('Torrent is no longer available.','warning');\n selectedHash = hash;\n lastSelectedHash = hash;\n const modal = ensureMobileDetailsModal();\n const title = $('mobileDetailsTitle');\n const subtitle = $('mobileDetailsSubtitle');\n const body = $('mobileDetailsBody');\n if(title) title.innerHTML = ' Torrent details';\n if(subtitle) subtitle.textContent = t.name || hash;\n if(body) body.innerHTML = ' Loading peers, files and trackers...
';\n new bootstrap.Modal(modal).show();\n try{\n // Note: The mobile modal reads existing lightweight detail endpoints without changing the desktop details tabs.\n const [peers, files, trackers] = await Promise.allSettled([\n fetchMobileDetailsJson(hash, 'peers'),\n fetchMobileDetailsJson(hash, 'files'),\n fetchMobileDetailsJson(hash, 'trackers'),\n ]);\n if(body) body.innerHTML = renderMobileDetailsContent(t, {peers, files, trackers});\n mobileReverseDnsRefreshAttempts=0;\n scheduleMobileReverseDnsRefresh(hash, t, {peers, files, trackers});\n }catch(e){\n if(body) body.innerHTML = `Details failed${esc(e.message)}
`;\n }\n }\n\n";
diff --git a/pytorrent/static/styles.css b/pytorrent/static/styles.css
index e81a616..a285e90 100644
--- a/pytorrent/static/styles.css
+++ b/pytorrent/static/styles.css
@@ -4991,72 +4991,72 @@ body,
width: 100%;
}
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(1),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(1),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(1),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(1) {
+.peers-table:not(.peers-table-hosts) th:nth-child(1),
+.peers-table:not(.peers-table-hosts) td:nth-child(1),
+.peers-table.peers-table-hosts th:nth-child(1),
+.peers-table.peers-table-hosts td:nth-child(1) {
width: 4%;
}
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(2),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(2),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(2),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(2) {
+.peers-table:not(.peers-table-hosts) th:nth-child(2),
+.peers-table:not(.peers-table-hosts) td:nth-child(2),
+.peers-table.peers-table-hosts th:nth-child(2),
+.peers-table.peers-table-hosts td:nth-child(2) {
width: 13%;
}
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(3),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(3) {
+.peers-table.peers-table-hosts th:nth-child(3),
+.peers-table.peers-table-hosts td:nth-child(3) {
width: 15%;
}
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(3),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(3),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(4),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(4),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(4),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(4),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(5),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(5) {
+.peers-table:not(.peers-table-hosts) th:nth-child(3),
+.peers-table:not(.peers-table-hosts) td:nth-child(3),
+.peers-table:not(.peers-table-hosts) th:nth-child(4),
+.peers-table:not(.peers-table-hosts) td:nth-child(4),
+.peers-table.peers-table-hosts th:nth-child(4),
+.peers-table.peers-table-hosts td:nth-child(4),
+.peers-table.peers-table-hosts th:nth-child(5),
+.peers-table.peers-table-hosts td:nth-child(5) {
width: 8%;
}
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(5),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(5),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(6),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(6) {
+.peers-table:not(.peers-table-hosts) th:nth-child(5),
+.peers-table:not(.peers-table-hosts) td:nth-child(5),
+.peers-table.peers-table-hosts th:nth-child(6),
+.peers-table.peers-table-hosts td:nth-child(6) {
width: 15%;
}
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(6),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(6),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(7),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(7) {
+.peers-table:not(.peers-table-hosts) th:nth-child(6),
+.peers-table:not(.peers-table-hosts) td:nth-child(6),
+.peers-table.peers-table-hosts th:nth-child(7),
+.peers-table.peers-table-hosts td:nth-child(7) {
width: 8rem;
}
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(7),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(7),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(8),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(8),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(8),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(8),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(9),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(9) {
+.peers-table:not(.peers-table-hosts) th:nth-child(7),
+.peers-table:not(.peers-table-hosts) td:nth-child(7),
+.peers-table:not(.peers-table-hosts) th:nth-child(8),
+.peers-table:not(.peers-table-hosts) td:nth-child(8),
+.peers-table.peers-table-hosts th:nth-child(8),
+.peers-table.peers-table-hosts td:nth-child(8),
+.peers-table.peers-table-hosts th:nth-child(9),
+.peers-table.peers-table-hosts td:nth-child(9) {
width: 6%;
}
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(9),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(9),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(10),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(10) {
+.peers-table:not(.peers-table-hosts) th:nth-child(9),
+.peers-table:not(.peers-table-hosts) td:nth-child(9),
+.peers-table.peers-table-hosts th:nth-child(10),
+.peers-table.peers-table-hosts td:nth-child(10) {
width: 5%;
}
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) th:nth-child(10),
-.peers-table:not(.mobile-details-peers-table):not(.peers-table-hosts) td:nth-child(10),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts th:nth-child(11),
-.peers-table:not(.mobile-details-peers-table).peers-table-hosts td:nth-child(11) {
+.peers-table:not(.peers-table-hosts) th:nth-child(10),
+.peers-table:not(.peers-table-hosts) td:nth-child(10),
+.peers-table.peers-table-hosts th:nth-child(11),
+.peers-table.peers-table-hosts td:nth-child(11) {
width: 10%;
}
@@ -5069,85 +5069,30 @@ body,
white-space: nowrap;
}
-/* Note: Mobile torrent details use a stable table so progress bars always render on a 0-100% track. */
+/* Note: Responsive table wrappers keep detail tables scrollable while preserving the rounded app table frame. */
+.responsive-table-wrap {
+ -webkit-overflow-scrolling: touch;
+ background: var(--bs-body-bg);
+ border: 1px solid var(--bs-border-color);
+ border-radius: 0.55rem;
+ max-width: 100%;
+ min-width: 0;
+ overflow-x: auto;
+ overflow-y: hidden;
+ overscroll-behavior-x: contain;
+ scrollbar-width: thin;
+ touch-action: pan-x pan-y;
+ width: 100%;
+}
+
+.responsive-table-wrap > .detail-table {
+ margin-bottom: 0;
+}
+
.mobile-details-modal .modal-body {
overflow-x: hidden;
}
-.mobile-details-modal .responsive-table-wrap {
- max-width: 100%;
-}
-
-.mobile-details-peers-table {
- margin-bottom: 0;
- min-width: 780px;
-}
-
-.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(1),
-.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(1),
-.mobile-details-peers-table.peers-table-hosts th:nth-child(1),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(1) {
- width: 5%;
-}
-
-.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(2),
-.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(2),
-.mobile-details-peers-table.peers-table-hosts th:nth-child(2),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(2) {
- width: 14%;
-}
-
-.mobile-details-peers-table.peers-table-hosts th:nth-child(3),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(3) {
- width: 16%;
-}
-
-.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(3),
-.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(3),
-.mobile-details-peers-table.peers-table-hosts th:nth-child(4),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(4) {
- width: 16%;
-}
-
-.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(4),
-.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(4),
-.mobile-details-peers-table.peers-table-hosts th:nth-child(5),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(5) {
- width: 15%;
-}
-
-.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(5),
-.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(5),
-.mobile-details-peers-table.peers-table-hosts th:nth-child(6),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(6) {
- width: 8rem;
-}
-
-.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(6),
-.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(6),
-.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(7),
-.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(7),
-.mobile-details-peers-table.peers-table-hosts th:nth-child(7),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(7),
-.mobile-details-peers-table.peers-table-hosts th:nth-child(8),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(8) {
- width: 7%;
-}
-
-.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(8),
-.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(8),
-.mobile-details-peers-table.peers-table-hosts th:nth-child(9),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(9) {
- width: 6%;
-}
-
-.mobile-details-peers-table:not(.peers-table-hosts) th:nth-child(9),
-.mobile-details-peers-table:not(.peers-table-hosts) td:nth-child(9),
-.mobile-details-peers-table.peers-table-hosts th:nth-child(10),
-.mobile-details-peers-table.peers-table-hosts td:nth-child(10) {
- width: 8%;
-}
-
/* App modal widths stay consistent while Bootstrap still handles full-screen mobile breakpoints. */
.app-modal-dialog,
.modal-dialog.modal-xl {
@@ -5612,23 +5557,6 @@ body,
overflow-wrap: anywhere;
}
-.mobile-details-files-table {
- margin-bottom: 0;
- min-width: 760px;
-}
-
-.mobile-details-trackers-table {
- margin-bottom: 0;
-}
-
-.mobile-details-files-table .file-progress {
- min-width: 7.5rem;
-}
-
-.mobile-details-files-table .file-priority {
- min-width: 6.5rem;
-}
-
.mobile-details-file-path {
display: inline-block;
max-width: 24rem;