@@ -119,6 +139,54 @@ const Contributors = () => {
+ {offlineNotice && (
+
{contributors.map((contributor) => (
diff --git a/src/styles/Contributors.css b/src/styles/Contributors.css
index 7b148b3..0a50b36 100644
--- a/src/styles/Contributors.css
+++ b/src/styles/Contributors.css
@@ -238,3 +238,52 @@
padding-top: 6rem;
}
}
+
+/* Inline notice for offline/cached status */
+.offline-notice {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ background: rgba(245, 158, 11, 0.08);
+ border: 1px solid rgba(245, 158, 11, 0.25);
+ border-radius: 12px;
+ padding: 1rem 1.5rem;
+ margin: 0 auto 3rem;
+ max-width: 800px;
+ width: 90%;
+ color: #f59e0b;
+ font-size: 0.95rem;
+ font-weight: 500;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
+ animation: fadeInUp 0.4s ease-out;
+}
+
+.offline-notice-content {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.offline-notice-icon {
+ flex-shrink: 0;
+ color: #fbbf24;
+}
+
+.offline-notice-close {
+ background: none;
+ border: none;
+ color: #a1a1aa;
+ cursor: pointer;
+ padding: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: all 0.2s;
+}
+
+.offline-notice-close:hover {
+ background: rgba(255, 255, 255, 0.08);
+ color: var(--text-primary);
+}
diff --git a/src/styles/NetworkStatusBanner.css b/src/styles/NetworkStatusBanner.css
new file mode 100644
index 0000000..c4de253
--- /dev/null
+++ b/src/styles/NetworkStatusBanner.css
@@ -0,0 +1,77 @@
+.network-status-banner {
+ position: fixed;
+ bottom: 24px;
+ left: 50%;
+ transform: translateX(-50%) translateY(100px);
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.75rem 1.5rem;
+ border-radius: 12px;
+ font-family: inherit;
+ font-size: 0.95rem;
+ font-weight: 500;
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s ease, border-color 0.3s ease, box-shadow 0.3s ease;
+ pointer-events: none;
+ opacity: 0;
+}
+
+.network-status-banner.visible {
+ transform: translateX(-50%) translateY(0);
+ opacity: 1;
+}
+
+.network-status-banner.hidden {
+ transform: translateX(-50%) translateY(100px);
+ opacity: 0;
+}
+
+/* Offline state (Amber/Red) */
+.network-status-banner.offline {
+ background: rgba(220, 38, 38, 0.1);
+ border: 1px solid rgba(220, 38, 38, 0.3);
+ color: #f87171;
+ box-shadow: 0 0 20px rgba(220, 38, 38, 0.15), 0 10px 25px -5px rgba(0, 0, 0, 0.5);
+}
+
+/* Online state (Emerald Green) */
+.network-status-banner.online {
+ background: rgba(16, 185, 129, 0.1);
+ border: 1px solid rgba(16, 185, 129, 0.3);
+ color: #34d399;
+ box-shadow: 0 0 20px rgba(16, 185, 129, 0.15), 0 10px 25px -5px rgba(0, 0, 0, 0.5);
+}
+
+.network-status-content {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.network-icon {
+ flex-shrink: 0;
+ animation: pulse-icon 2s infinite ease-in-out;
+}
+
+.network-status-banner.offline .network-icon {
+ color: #ef4444;
+}
+
+.network-status-banner.online .network-icon {
+ color: #10b981;
+}
+
+@keyframes pulse-icon {
+ 0%, 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.08);
+ opacity: 0.8;
+ }
+}