diff --git a/src/database.py b/src/database.py
index 2d56712..6bd282b 100644
--- a/src/database.py
+++ b/src/database.py
@@ -2050,6 +2050,122 @@ class DatabaseManager:
finally:
self.close_session()
+ def search_attacks_and_ips(
+ self,
+ query: str,
+ page: int = 1,
+ page_size: int = 20,
+ ) -> Dict[str, Any]:
+ """
+ Search attacks and IPs matching a query string.
+
+ Searches across AttackDetection (attack_type, matched_pattern),
+ AccessLog (ip, path), and IpStats (ip, city, country, isp, asn_org).
+
+ Args:
+ query: Search term (partial match)
+ page: Page number (1-indexed)
+ page_size: Results per page
+
+ Returns:
+ Dictionary with matching attacks, ips, and pagination info
+ """
+ session = self.session
+ try:
+ offset = (page - 1) * page_size
+ like_q = f"%{query}%"
+
+ # --- Search attacks (AccessLog + AttackDetection) ---
+ attack_query = (
+ session.query(AccessLog)
+ .join(AttackDetection)
+ .filter(
+ or_(
+ AccessLog.ip.ilike(like_q),
+ AccessLog.path.ilike(like_q),
+ AttackDetection.attack_type.ilike(like_q),
+ AttackDetection.matched_pattern.ilike(like_q),
+ )
+ )
+ .distinct(AccessLog.id)
+ )
+
+ total_attacks = attack_query.count()
+ attack_logs = (
+ attack_query.order_by(AccessLog.timestamp.desc())
+ .offset(offset)
+ .limit(page_size)
+ .all()
+ )
+
+ attacks = [
+ {
+ "id": log.id,
+ "ip": log.ip,
+ "path": log.path,
+ "user_agent": log.user_agent,
+ "timestamp": log.timestamp.isoformat() if log.timestamp else None,
+ "attack_types": [d.attack_type for d in log.attack_detections],
+ "log_id": log.id,
+ }
+ for log in attack_logs
+ ]
+
+ # --- Search IPs (IpStats) ---
+ ip_query = session.query(IpStats).filter(
+ or_(
+ IpStats.ip.ilike(like_q),
+ IpStats.city.ilike(like_q),
+ IpStats.country.ilike(like_q),
+ IpStats.country_code.ilike(like_q),
+ IpStats.isp.ilike(like_q),
+ IpStats.asn_org.ilike(like_q),
+ IpStats.reverse.ilike(like_q),
+ )
+ )
+
+ total_ips = ip_query.count()
+ ips = (
+ ip_query.order_by(IpStats.total_requests.desc())
+ .offset(offset)
+ .limit(page_size)
+ .all()
+ )
+
+ ip_results = [
+ {
+ "ip": stat.ip,
+ "total_requests": stat.total_requests,
+ "first_seen": stat.first_seen.isoformat() if stat.first_seen else None,
+ "last_seen": stat.last_seen.isoformat() if stat.last_seen else None,
+ "country_code": stat.country_code,
+ "city": stat.city,
+ "category": stat.category,
+ "isp": stat.isp,
+ "asn_org": stat.asn_org,
+ }
+ for stat in ips
+ ]
+
+ total = total_attacks + total_ips
+ total_pages = max(1, (max(total_attacks, total_ips) + page_size - 1) // page_size)
+
+ return {
+ "attacks": attacks,
+ "ips": ip_results,
+ "query": query,
+ "pagination": {
+ "page": page,
+ "page_size": page_size,
+ "total_attacks": total_attacks,
+ "total_ips": total_ips,
+ "total": total,
+ "total_pages": total_pages,
+ },
+ }
+ finally:
+ self.close_session()
+
# Module-level singleton instance
_db_manager = DatabaseManager()
diff --git a/src/dependencies.py b/src/dependencies.py
index 774d9dd..a713738 100644
--- a/src/dependencies.py
+++ b/src/dependencies.py
@@ -30,7 +30,7 @@ def get_templates() -> Jinja2Templates:
return _templates
-def _format_ts(value, time_only=False):
+def _format_ts(value):
"""Custom Jinja2 filter for formatting ISO timestamps."""
if not value:
return "N/A"
@@ -39,7 +39,7 @@ def _format_ts(value, time_only=False):
value = datetime.fromisoformat(value)
except (ValueError, TypeError):
return value
- if time_only:
+ if value.date() == datetime.now().date():
return value.strftime("%H:%M:%S")
return value.strftime("%m/%d/%Y %H:%M:%S")
diff --git a/src/routes/htmx.py b/src/routes/htmx.py
index ef2d5c1..976fc35 100644
--- a/src/routes/htmx.py
+++ b/src/routes/htmx.py
@@ -2,7 +2,7 @@
"""
HTMX fragment endpoints.
-Server-rendered HTML partials for table pagination, sorting, and IP details.
+Server-rendered HTML partials for table pagination, sorting, IP details, and search.
"""
from fastapi import APIRouter, Request, Response, Query
@@ -369,3 +369,33 @@ async def htmx_ip_detail(ip_address: str, request: Request):
"stats": stats,
},
)
+
+
+# ── Search ───────────────────────────────────────────────────────────
+
+
+@router.get("/htmx/search")
+async def htmx_search(
+ request: Request,
+ q: str = Query(""),
+ page: int = Query(1),
+):
+ q = q.strip()
+ if not q:
+ return Response(content="", media_type="text/html")
+
+ db = get_db()
+ result = db.search_attacks_and_ips(query=q, page=max(1, page), page_size=20)
+
+ templates = get_templates()
+ return templates.TemplateResponse(
+ "dashboard/partials/search_results.html",
+ {
+ "request": request,
+ "dashboard_path": _dashboard_path(request),
+ "attacks": result["attacks"],
+ "ips": result["ips"],
+ "query": q,
+ "pagination": result["pagination"],
+ },
+ )
diff --git a/src/templates/jinja2/base.html b/src/templates/jinja2/base.html
index 1ba2af5..48097f0 100644
--- a/src/templates/jinja2/base.html
+++ b/src/templates/jinja2/base.html
@@ -8,6 +8,7 @@
+
diff --git a/src/templates/jinja2/dashboard/index.html b/src/templates/jinja2/dashboard/index.html
index 647a00c..97e7ce1 100644
--- a/src/templates/jinja2/dashboard/index.html
+++ b/src/templates/jinja2/dashboard/index.html
@@ -31,6 +31,27 @@
{# Stats cards - server-rendered #}
{% include "dashboard/partials/stats_cards.html" %}
+ {# Search bar #}
+
+
{# Tab navigation - Alpine.js #}
Overview
diff --git a/src/templates/jinja2/dashboard/partials/attack_types_table.html b/src/templates/jinja2/dashboard/partials/attack_types_table.html
index 9a2451a..e149bfc 100644
--- a/src/templates/jinja2/dashboard/partials/attack_types_table.html
+++ b/src/templates/jinja2/dashboard/partials/attack_types_table.html
@@ -77,7 +77,7 @@
{% else %}
-
| No attacks detected |
+
| No attacks detected |
{% endfor %}
diff --git a/src/templates/jinja2/dashboard/partials/attackers_table.html b/src/templates/jinja2/dashboard/partials/attackers_table.html
index 23c61e8..1bcbb40 100644
--- a/src/templates/jinja2/dashboard/partials/attackers_table.html
+++ b/src/templates/jinja2/dashboard/partials/attackers_table.html
@@ -25,8 +25,16 @@
hx-swap="innerHTML">
Total Requests
-
First Seen |
-
Last Seen |
+
+ First Seen |
+
+ Last Seen |
Location |
|
@@ -60,7 +68,7 @@
{% else %}
-
| No attackers found |
+
| No attackers found |
{% endfor %}
diff --git a/src/templates/jinja2/dashboard/partials/credentials_table.html b/src/templates/jinja2/dashboard/partials/credentials_table.html
index 491b5b2..c7ee193 100644
--- a/src/templates/jinja2/dashboard/partials/credentials_table.html
+++ b/src/templates/jinja2/dashboard/partials/credentials_table.html
@@ -60,7 +60,7 @@
{% else %}
-
| No credentials captured |
+
| No credentials captured |
{% endfor %}
diff --git a/src/templates/jinja2/dashboard/partials/honeypot_table.html b/src/templates/jinja2/dashboard/partials/honeypot_table.html
index 3215bc7..302df69 100644
--- a/src/templates/jinja2/dashboard/partials/honeypot_table.html
+++ b/src/templates/jinja2/dashboard/partials/honeypot_table.html
@@ -54,7 +54,7 @@
{% else %}
-
| No data |
+
| No data |
{% endfor %}
diff --git a/src/templates/jinja2/dashboard/partials/patterns_table.html b/src/templates/jinja2/dashboard/partials/patterns_table.html
index 260f31d..003f7e3 100644
--- a/src/templates/jinja2/dashboard/partials/patterns_table.html
+++ b/src/templates/jinja2/dashboard/partials/patterns_table.html
@@ -37,7 +37,7 @@
{{ pattern.count }} |
{% else %}
-
| No patterns found |
+
| No patterns found |
{% endfor %}
diff --git a/src/templates/jinja2/dashboard/partials/search_results.html b/src/templates/jinja2/dashboard/partials/search_results.html
new file mode 100644
index 0000000..a1e4046
--- /dev/null
+++ b/src/templates/jinja2/dashboard/partials/search_results.html
@@ -0,0 +1,158 @@
+{# HTMX fragment: Search results for attacks and IPs #}
+
+
+
+
+ {# ── Matching IPs ─────────────────────────────────── #}
+ {% if ips %}
+
+
Matching IPs
+
+
+
+ | # |
+ IP Address |
+ Requests |
+ Category |
+ Location |
+ ISP / ASN |
+ Last Seen |
+
+
+
+ {% for ip in ips %}
+
+ | {{ loop.index + (pagination.page - 1) * pagination.page_size }} |
+
+ {{ ip.ip | e }}
+ |
+ {{ ip.total_requests }} |
+
+ {% if ip.category %}
+
+ {{ ip.category | e }}
+
+ {% else %}
+ unknown
+ {% endif %}
+ |
+ {{ ip.city | default('') | e }}{% if ip.city and ip.country_code %}, {% endif %}{{ ip.country_code | default('N/A') | e }} |
+ {{ ip.isp | default(ip.asn_org | default('N/A')) | e }} |
+ {{ ip.last_seen | format_ts }} |
+
+
+ |
+
+ |
+
+ {% endfor %}
+
+
+
+ {% endif %}
+
+ {# ── Matching Attacks ─────────────────────────────── #}
+ {% if attacks %}
+
+
Matching Attacks
+
+
+
+ | # |
+ IP Address |
+ Path |
+ Attack Types |
+ User-Agent |
+ Time |
+ Actions |
+
+
+
+ {% for attack in attacks %}
+
+ | {{ loop.index + (pagination.page - 1) * pagination.page_size }} |
+
+ {{ attack.ip | e }}
+ |
+
+
+ {{ attack.path | e }}
+ {% if attack.path | length > 30 %}
+ {{ attack.path | e }}
+ {% endif %}
+
+ |
+
+
+ {% set types_str = attack.attack_types | join(', ') %}
+ {{ types_str | e }}
+ {% if types_str | length > 30 %}
+ {{ types_str | e }}
+ {% endif %}
+
+ |
+ {{ (attack.user_agent | default(''))[:50] | e }} |
+ {{ attack.timestamp | format_ts }} |
+
+ {% if attack.log_id %}
+
+ {% endif %}
+ |
+
+
+ |
+
+ |
+
+ {% endfor %}
+
+
+
+ {% endif %}
+
+ {# ── Pagination ───────────────────────────────────── #}
+ {% if pagination.total_pages > 1 %}
+
+ {% endif %}
+
+ {# ── No results ───────────────────────────────────── #}
+ {% if not attacks and not ips %}
+
+ No results found for “{{ query | e }}”
+
+ {% endif %}
+
+
diff --git a/src/templates/jinja2/dashboard/partials/suspicious_table.html b/src/templates/jinja2/dashboard/partials/suspicious_table.html
index c10502c..333e8df 100644
--- a/src/templates/jinja2/dashboard/partials/suspicious_table.html
+++ b/src/templates/jinja2/dashboard/partials/suspicious_table.html
@@ -38,7 +38,7 @@
{% else %}
-
| No suspicious activity detected |
+
| No suspicious activity detected |
{% endfor %}
diff --git a/src/templates/jinja2/dashboard/partials/top_ips_table.html b/src/templates/jinja2/dashboard/partials/top_ips_table.html
index afcf26b..d4614c2 100644
--- a/src/templates/jinja2/dashboard/partials/top_ips_table.html
+++ b/src/templates/jinja2/dashboard/partials/top_ips_table.html
@@ -60,7 +60,7 @@
{% else %}
-
| No data |
+
| No data |
{% endfor %}
diff --git a/src/templates/jinja2/dashboard/partials/top_paths_table.html b/src/templates/jinja2/dashboard/partials/top_paths_table.html
index d1ec6d1..c102410 100644
--- a/src/templates/jinja2/dashboard/partials/top_paths_table.html
+++ b/src/templates/jinja2/dashboard/partials/top_paths_table.html
@@ -35,7 +35,7 @@
{{ item.count }} |
{% else %}
-
| No data |
+
| No data |
{% endfor %}
diff --git a/src/templates/jinja2/dashboard/partials/top_ua_table.html b/src/templates/jinja2/dashboard/partials/top_ua_table.html
index faf487e..dc38e90 100644
--- a/src/templates/jinja2/dashboard/partials/top_ua_table.html
+++ b/src/templates/jinja2/dashboard/partials/top_ua_table.html
@@ -31,11 +31,11 @@
{% for item in items %}
| {{ loop.index + (pagination.page - 1) * pagination.page_size }} |
- {{ item.user_agent | e }} |
+ {{ item.user_agent | e }} |
{{ item.count }} |
{% else %}
-
| No data |
+
| No data |
{% endfor %}
diff --git a/src/templates/static/css/dashboard.css b/src/templates/static/css/dashboard.css
index 9ed0186..a4dae5f 100644
--- a/src/templates/static/css/dashboard.css
+++ b/src/templates/static/css/dashboard.css
@@ -41,6 +41,8 @@ h1 {
color: #58a6ff;
text-align: center;
margin-bottom: 40px;
+ font-weight: 900;
+ font-family: 'Google Sans Flex', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.download-section {
position: absolute;
@@ -74,20 +76,21 @@ h1 {
display: block;
width: 100%;
padding: 8px 14px;
- background: #238636;
- color: #ffffff;
+ background: rgba(35, 134, 54, 0.4);
+ color: rgba(255, 255, 255, 0.7);
text-decoration: none;
border-radius: 6px;
font-weight: 500;
font-size: 13px;
- transition: background 0.2s;
- border: 1px solid #2ea043;
+ transition: background 0.2s, color 0.2s;
+ border: 1px solid rgba(46, 160, 67, 0.4);
cursor: pointer;
text-align: left;
box-sizing: border-box;
}
.banlist-dropdown-btn:hover {
- background: #2ea043;
+ background: rgba(46, 160, 67, 0.6);
+ color: #ffffff;
}
.banlist-dropdown-menu {
display: none;
@@ -189,8 +192,8 @@ tr:hover {
font-weight: bold;
}
.alert-section {
- background: #1c1917;
- border-left: 4px solid #f85149;
+ background: #161b22;
+ border-left: 6px solid rgba(248, 81, 73, 0.4);
}
th.sortable {
cursor: pointer;
@@ -1263,339 +1266,133 @@ tbody {
display: none !important;
}
-/* ========================================
- Single IP Page Styles
- ======================================== */
-
-.ip-page-header {
- text-align: center;
- margin-bottom: 30px;
- padding-top: 20px;
+/* ── Search Bar ────────────────────────────────────── */
+.search-bar-container {
+ max-width: 100%;
+ margin: 0 0 20px 0;
}
-
-.ip-page-header h1 {
+.search-bar {
+ position: relative;
display: flex;
align-items: center;
- justify-content: center;
- gap: 15px;
- flex-wrap: wrap;
- margin-bottom: 8px;
}
-
-.ip-address-title {
- font-family: 'SF Mono', Monaco, Consolas, monospace;
- font-size: 32px;
- color: #58a6ff;
-}
-
-.ip-location-subtitle {
+.search-icon {
+ position: absolute;
+ left: 14px;
+ width: 18px;
+ height: 18px;
color: #8b949e;
- font-size: 16px;
- margin: 0;
+ pointer-events: none;
}
-
-.ip-page-grid {
- display: grid;
- grid-template-columns: 2fr 1fr;
- gap: 20px;
- margin-bottom: 20px;
-}
-
-.ip-page-left,
-.ip-page-right {
- display: flex;
- flex-direction: column;
- gap: 20px;
-}
-
-.ip-info-card h2 {
- margin-bottom: 20px;
-}
-
-.ip-info-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 20px;
-}
-
-.ip-info-section {
- background: #0d1117;
- border: 1px solid #21262d;
- border-radius: 6px;
- padding: 15px;
-}
-
-.ip-info-section h3 {
- color: #58a6ff;
- font-size: 13px;
- font-weight: 600;
- text-transform: uppercase;
- margin: 0 0 12px 0;
- padding-bottom: 8px;
- border-bottom: 1px solid #21262d;
-}
-
-.ip-flag {
- display: inline-block;
- padding: 2px 8px;
- background: #f0883e1a;
- color: #f0883e;
- border: 1px solid #f0883e4d;
- border-radius: 4px;
- font-size: 11px;
- font-weight: 600;
- margin-right: 4px;
-}
-
-.reputation-score {
- font-weight: 700;
- padding: 2px 8px;
- border-radius: 4px;
-}
-
-.reputation-score.bad {
- background: #f851491a;
- color: #f85149;
-}
-
-.reputation-score.medium {
- background: #f0883e1a;
- color: #f0883e;
-}
-
-.reputation-score.good {
- background: #3fb9501a;
- color: #3fb950;
-}
-
-.radar-chart-container {
- display: flex;
- justify-content: center;
- padding: 20px 0;
-}
-
-/* Single IP page: radar chart with legend on the right */
-.ip-page-right .radar-chart-container {
- padding: 10px 0;
- justify-content: flex-start;
-}
-
-/* Target the wrapper div injected by generateRadarChart inside radar chart containers */
-.ip-page-right #ip-radar-chart > div,
-.ip-page-right #insight-radar-chart > div {
- display: flex !important;
- flex-direction: row !important;
- align-items: center !important;
- gap: 15px;
-}
-
-.ip-page-right #ip-radar-chart > div > svg,
-.ip-page-right #insight-radar-chart > div > svg {
- flex-shrink: 0;
-}
-
-.ip-page-right #ip-radar-chart .radar-legend,
-.ip-page-right #insight-radar-chart .radar-legend {
- margin-top: 0;
- text-align: left;
-}
-
-/* Single IP page: limit timeline height to match map */
-.ip-page-right .timeline {
- max-height: 250px;
- overflow-y: auto;
- padding-right: 10px;
-}
-
-/* Dark theme scrollbar for timeline */
-.ip-page-right .timeline::-webkit-scrollbar {
- width: 6px;
-}
-
-.ip-page-right .timeline::-webkit-scrollbar-track {
- background: #21262d;
- border-radius: 3px;
-}
-
-.ip-page-right .timeline::-webkit-scrollbar-thumb {
- background: #484f58;
- border-radius: 3px;
-}
-
-.ip-page-right .timeline::-webkit-scrollbar-thumb:hover {
- background: #6e7681;
-}
-
-.single-ip-marker {
- background: none !important;
- border: none !important;
-}
-
-/* Mobile responsiveness for IP page */
-@media (max-width: 1024px) {
- .ip-page-grid {
- grid-template-columns: 1fr;
- }
-
- .ip-page-right {
- order: -1;
- }
-
- .ip-info-grid {
- grid-template-columns: 1fr;
- }
-
- /* On mobile, stack legend below chart again */
- .ip-page-right #ip-radar-chart > div,
- .ip-page-right #insight-radar-chart > div {
- flex-direction: column !important;
- }
-
- .ip-page-right #ip-radar-chart .radar-legend,
- .ip-page-right #insight-radar-chart .radar-legend {
- margin-top: 10px;
- text-align: center;
- }
-
- /* On mobile, remove timeline height limit */
- .ip-page-right .timeline {
- max-height: none;
- }
-}
-
-@media (max-width: 768px) {
- .ip-address-title {
- font-size: 24px;
- }
-
- .ip-page-header h1 {
- flex-direction: column;
- gap: 10px;
- }
-
- .ip-info-section {
- padding: 12px;
- }
-
- .ip-info-section h3 {
- font-size: 12px;
- }
-}
-
-@media (max-width: 480px) {
- .ip-address-title {
- font-size: 18px;
- }
-
- .ip-location-subtitle {
- font-size: 14px;
- }
-}
-
-/* ========================================
- IP Lookup Panel
- ======================================== */
-
-.ip-lookup-panel {
- background: #161b22;
- border: 1px solid #30363d;
- border-radius: 8px;
- padding: 20px;
- margin-bottom: 30px;
- display: flex;
- justify-content: center;
-}
-
-.ip-lookup-form {
- display: flex;
- gap: 12px;
+.search-bar input[type="search"] {
width: 100%;
- max-width: 500px;
-}
-
-.ip-lookup-input {
- flex: 1;
- padding: 12px 16px;
+ padding: 12px 40px 12px 42px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #c9d1d9;
font-size: 14px;
- font-family: 'SF Mono', Monaco, Consolas, monospace;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
-
-.ip-lookup-input:focus {
+.search-bar input[type="search"]::placeholder {
+ color: #6e7681;
+}
+.search-bar input[type="search"]:focus {
border-color: #58a6ff;
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15);
}
-
-.ip-lookup-input::placeholder {
- color: #6e7681;
-}
-
-.ip-lookup-btn {
- padding: 12px 24px;
- background: #238636;
- color: #ffffff;
- border: 1px solid #2ea043;
- border-radius: 6px;
- font-weight: 600;
- font-size: 14px;
+.search-bar input[type="search"]::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%238b949e'%3E%3Cpath d='M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z'/%3E%3C/svg%3E") center/contain no-repeat;
cursor: pointer;
- transition: background 0.2s;
- white-space: nowrap;
}
-
-.ip-lookup-btn:hover {
- background: #2ea043;
-}
-
-.ip-lookup-btn:active {
- background: #1f7a2f;
-}
-
-@media (max-width: 480px) {
- .ip-lookup-form {
- flex-direction: column;
- }
-
- .ip-lookup-btn {
- width: 100%;
- }
-}
-
-/* ========================================
- Inspect Button
- ======================================== */
-
-.inspect-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- padding: 4px 10px;
- background: #21262d;
+.search-spinner {
+ position: absolute;
+ right: 14px;
+ font-size: 18px;
color: #58a6ff;
+ animation: spin 0.8s linear infinite;
+}
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* ── Search Results ───────────────────────────────── */
+.search-results {
+ margin-top: 12px;
+ background: #161b22;
border: 1px solid #30363d;
- border-radius: 4px;
- font-size: 11px;
- font-weight: 500;
+ border-radius: 6px;
+ padding: 16px;
+ animation: fadeIn 0.3s ease-in;
+}
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(-4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+.search-results-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 14px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #30363d;
+}
+.search-results-summary {
+ color: #8b949e;
+ font-size: 13px;
+}
+.search-results-summary strong {
+ color: #58a6ff;
+}
+.search-close-btn {
+ background: none;
+ border: none;
+ color: #8b949e;
+ font-size: 22px;
cursor: pointer;
- transition: all 0.2s;
- text-decoration: none;
- white-space: nowrap;
+ padding: 0 4px;
+ line-height: 1;
+ transition: color 0.2s;
+}
+.search-close-btn:hover {
+ color: #f85149;
+}
+.search-section {
+ margin-bottom: 16px;
+}
+.search-section:last-of-type {
+ margin-bottom: 0;
+}
+.search-section-title {
+ color: #58a6ff;
+ font-size: 14px;
+ font-weight: 600;
+ margin: 0 0 8px 0;
+}
+.search-pagination {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 12px;
+ padding-top: 10px;
+ border-top: 1px solid #30363d;
+}
+.search-no-results {
+ text-align: center;
+ color: #4a515a;
+ padding: 24px 0;
+ font-size: 14px;
}
-.inspect-btn:hover {
- background: #30363d;
- border-color: #58a6ff;
- color: #79c0ff;
-}
-
-.inspect-btn svg {
- width: 12px;
- height: 12px;
- fill: currentColor;
+/* ── Empty State (no data rows) ───────────────────── */
+.empty-state {
+ text-align: center;
+ color: #4a515a;
+ padding: 20px 12px;
}