diff --git a/static/index.html b/static/index.html
index a08cf3c..50d6e84 100644
--- a/static/index.html
+++ b/static/index.html
@@ -194,20 +194,38 @@
.tab-panel.anim-exit {
display: block !important;
position: absolute; top: var(--header-height, 52px); left: 0; right: 0;
- animation: tabSlideOutLeft 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
+ animation: tabSlideOutLeft 0.22s cubic-bezier(0.4, 0, 0.2, 1) forwards;
+ z-index: 1; pointer-events: none;
+ }
+ .tab-panel.anim-exit-reverse {
+ display: block !important;
+ position: absolute; top: var(--header-height, 52px); left: 0; right: 0;
+ animation: tabSlideOutRight 0.22s cubic-bezier(0.4, 0, 0.2, 1) forwards;
z-index: 1; pointer-events: none;
}
.tab-panel.anim-enter {
display: block;
animation: tabSlideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
+ .tab-panel.anim-enter-reverse {
+ display: block;
+ animation: tabSlideInReverse 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+ }
@keyframes tabSlideIn {
- from { opacity: 0; transform: translateX(20px); }
+ from { opacity: 0; transform: translateX(24px); }
+ to { opacity: 1; transform: translateX(0); }
+ }
+ @keyframes tabSlideInReverse {
+ from { opacity: 0; transform: translateX(-24px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes tabSlideOutLeft {
from { opacity: 1; transform: translateX(0); }
- to { opacity: 0; transform: translateX(-20px); }
+ to { opacity: 0; transform: translateX(-24px); }
+ }
+ @keyframes tabSlideOutRight {
+ from { opacity: 1; transform: translateX(0); }
+ to { opacity: 0; transform: translateX(24px); }
}
/* ═══════════════════════════════════════════════════════════════════════
@@ -282,15 +300,31 @@
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
border: none; border-radius: var(--radius-sm); font-size: 14px; font-weight: 600;
- padding: 11px 18px; cursor: pointer; transition: opacity 0.15s, transform 0.1s;
+ padding: 11px 18px; cursor: pointer;
+ transition: opacity 0.18s, transform 0.18s, background 0.18s, box-shadow 0.18s;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
- .btn:active { transform: scale(0.97); }
+ .btn:hover { transform: scale(1.03); }
+ .btn:active { transform: scale(0.96); }
.btn-primary { background: var(--accent); color: #fff; width: 100%; }
- .btn-primary:hover { background: var(--accent2); }
+ .btn-primary:hover { background: var(--accent2); box-shadow: 0 2px 12px rgba(91,110,247,0.25); }
.btn-sm { padding: 8px 14px; font-size: 13px; width: auto; }
.btn-outline { background: transparent; border: 1.5px solid var(--border); color: var(--text); }
+ .btn-outline:hover { border-color: var(--text3); background: rgba(255,255,255,0.03); }
.btn-danger { background: transparent; border: 1.5px solid var(--red); color: var(--red); }
+ .btn-danger:hover { background: var(--red-bg); }
.btn-green { background: var(--green); color: #000; }
+ .btn-green:hover { box-shadow: 0 2px 12px rgba(74,222,128,0.25); }
+ .btn-icon {
+ width: 44px; height: 44px; padding: 0; border-radius: 50%;
+ background: var(--card2); border: 1px solid var(--border);
+ color: var(--text); font-size: 20px;
+ display: inline-flex; align-items: center; justify-content: center;
+ transition: transform 0.18s, background 0.18s, box-shadow 0.18s;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .btn-icon:hover { transform: scale(1.08); background: var(--card); }
+ .btn-icon:active { transform: scale(0.92); }
/* ═══════════════════════════════════════════════════════════════════════
INPUTS
@@ -333,13 +367,26 @@
═══════════════════════════════════════════════════════════════════════ */
.toast {
position: fixed; bottom: calc(var(--tab-height) + 12px); left: 50%;
- transform: translateX(-50%); background: var(--green); color: #000;
+ transform: translateX(-50%) translateY(20px);
+ background: var(--green); color: #000;
font-weight: 700; padding: 10px 22px; border-radius: 20px; font-size: 14px;
- z-index: 400; opacity: 0; transition: opacity 0.3s; pointer-events: none;
+ z-index: 400; opacity: 0; pointer-events: none;
white-space: nowrap; max-width: 90vw; overflow: hidden; text-overflow: ellipsis;
+ transition: opacity 0.25s, transform 0.25s;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .toast.show {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
}
- .toast.show { opacity: 1; }
.toast.error { background: var(--red); color: #fff; }
+ .toast-undo-btn {
+ background: rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.3);
+ color: #fff; font-weight: 700; font-size: 13px;
+ padding: 4px 12px; border-radius: 12px; cursor: pointer;
+ margin-left: 10px; white-space: nowrap;
+ }
+ .toast-undo-btn:active { background: rgba(0,0,0,0.4); }
/* ═══════════════════════════════════════════════════════════════════════
SPINNER
@@ -364,9 +411,132 @@
border-top-color: var(--accent); border-radius: 50%;
animation: spin 0.7s linear infinite;
}
+ /* =========================================================================
+ FAB - Floating Action Button (pulse animation)
+ ========================================================================= */
+ .fab {
+ position: fixed; bottom: calc(var(--tab-height) + 20px); right: 20px;
+ width: 56px; height: 56px; border-radius: 50%;
+ background: var(--accent); color: #fff;
+ border: none; font-size: 28px; font-weight: 300; cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ box-shadow: 0 4px 16px rgba(91,110,247,0.35);
+ z-index: 90;
+ transition: transform 0.18s, box-shadow 0.18s;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ animation: fabPulse 3s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+ }
+ .fab:hover {
+ transform: scale(1.08);
+ box-shadow: 0 6px 24px rgba(91,110,247,0.45);
+ }
+ .fab:active {
+ transform: scale(0.94);
+ animation: none;
+ }
+ @keyframes fabPulse {
+ 0%, 100% { box-shadow: 0 4px 16px rgba(91,110,247,0.35); }
+ 50% { box-shadow: 0 4px 28px rgba(91,110,247,0.55); }
+ }
+
/* ═══════════════════════════════════════════════════════════════════════
- PULL-TO-REFRESH (mobile touch)
+ SKELETON LOADING (shimmer)
+ ═══════════════════════════════════════════════════════════════════════ */
+ @keyframes shimmer {
+ 0% { background-position: -400px 0; }
+ 100% { background-position: 400px 0; }
+ }
+ .skel {
+ background: linear-gradient(90deg,
+ var(--card) 0%,
+ var(--card2) 40%,
+ var(--border) 50%,
+ var(--card2) 60%,
+ var(--card) 100%
+ );
+ background-size: 800px 100%;
+ animation: shimmer 1.6s ease-in-out infinite;
+ border-radius: var(--radius-xs);
+ }
+ .skel-circle {
+ border-radius: 50%;
+ background: linear-gradient(90deg,
+ var(--card) 0%,
+ var(--card2) 40%,
+ var(--border) 50%,
+ var(--card2) 60%,
+ var(--card) 100%
+ );
+ background-size: 800px 100%;
+ animation: shimmer 1.6s ease-in-out infinite;
+ }
+ .skel-text { height: 14px; margin-bottom: 8px; }
+ .skel-text:last-child { margin-bottom: 0; }
+ .skel-text.w80 { width: 80%; }
+ .skel-text.w60 { width: 60%; }
+ .skel-text.w40 { width: 40%; }
+ .skel-text.w30 { width: 30%; }
+
+ /* Asset list skeleton */
+ .skel-asset-item {
+ display: flex; align-items: center; gap: 12px; padding: 12px;
+ border-bottom: 1px solid rgba(255,255,255,0.04);
+ }
+ .skel-asset-item .skel-circle { width: 42px; height: 42px; flex-shrink: 0; }
+ .skel-asset-item .skel-lines { flex: 1; min-width: 0; }
+
+ /* Customer card skeleton — same layout as asset */
+ .skel-cust-card {
+ display: flex; align-items: center; gap: 12px; padding: 12px;
+ border-bottom: 1px solid rgba(255,255,255,0.04);
+ }
+ .skel-cust-card .skel-circle { width: 42px; height: 42px; flex-shrink: 0; }
+ .skel-cust-card .skel-lines { flex: 1; min-width: 0; }
+
+ /* Activity item skeleton */
+ .skel-act-item {
+ display: flex; align-items: flex-start; gap: 12px; padding: 12px 0;
+ border-bottom: 1px solid rgba(255,255,255,0.04);
+ }
+ .skel-act-item .skel-circle { width: 36px; height: 36px; flex-shrink: 0; }
+ .skel-act-item .skel-lines { flex: 1; min-width: 0; }
+
+ /* Stat card skeleton */
+ .skel-stat-card {
+ background: var(--card); border: 1px solid var(--border);
+ border-radius: var(--radius); padding: 16px; text-align: center;
+ }
+ .skel-stat-value { width: 60%; height: 28px; margin: 0 auto 8px auto; }
+ .skel-stat-label { width: 40%; height: 10px; margin: 0 auto; }
+
+ /* Dashboard stat bar skeleton */
+ .skel-stat-bar {
+ display: flex; align-items: center; gap: 10px; padding: 8px 0;
+ }
+ .skel-stat-bar .skel-bar-label { width: 80px; height: 13px; }
+ .skel-stat-bar .skel-bar-track { flex: 1; height: 8px; }
+
+ /* Report table skeleton */
+ .skel-rpt-row {
+ display: flex; gap: 8px; padding: 8px 0;
+ border-bottom: 1px solid rgba(255,255,255,0.03);
+ }
+ .skel-rpt-row .skel-cell { height: 13px; }
+ .skel-rpt-row .skel-cell.c1 { flex: 1.5; }
+ .skel-rpt-row .skel-cell.c2 { flex: 1; }
+ .skel-rpt-row .skel-cell.c3 { flex: 0.7; }
+
+ /* Skeleton fade-out when content replaces it */
+ .skel-wrapper {
+ transition: opacity 0.2s ease-out;
+ }
+ .skel-wrapper.skel-hide {
+ opacity: 0; pointer-events: none;
+ }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ EMPTY STATE
═══════════════════════════════════════════════════════════════════════ */
.ptr-container {
overflow-y: auto;
@@ -531,6 +701,31 @@
display: block; font-size: 12px; color: var(--text2); margin-bottom: 4px;
}
+ /* GPS warning banner in Navigate tab */
+ .gps-warning {
+ background: var(--amber-bg); color: var(--amber);
+ padding: 10px 14px; border-radius: var(--radius-sm);
+ font-size: 13px; margin-bottom: 12px;
+ display: flex; align-items: center; gap: 8px; line-height: 1.4;
+ }
+ .gps-enable-link {
+ text-decoration: underline; cursor: pointer; font-weight: 600;
+ white-space: nowrap;
+ }
+ .gps-enable-link:active { color: var(--accent2); }
+ .nav-route-info {
+ display: flex; gap: 12px; flex-wrap: wrap;
+ }
+ .nav-route-item {
+ flex: 1; min-width: 100px;
+ }
+ .nav-route-item .nri-label {
+ font-size: 11px; color: var(--text3); margin-bottom: 2px;
+ }
+ .nav-route-item .nri-value {
+ font-size: 20px; font-weight: 700;
+ }
+
/* Map legend */
.map-legend {
position: absolute; bottom: 10px; right: 10px; z-index: 1000;
@@ -1274,7 +1469,238 @@
width: 28px; height: 28px; border-radius: 4px; object-fit: cover;
border: 1px solid var(--border);
}
-
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ SKELETON LOADING & SHIMMER
+ ═══════════════════════════════════════════════════════════════════════ */
+ .skeleton {
+ background: linear-gradient(90deg, var(--card2) 25%, var(--border) 50%, var(--card2) 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite ease-in-out;
+ border-radius: var(--radius-xs);
+ }
+ @keyframes shimmer {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+ }
+ .skeleton-card {
+ background: var(--card); border: 1px solid var(--border);
+ border-radius: var(--radius); padding: 16px; margin-bottom: 10px;
+ }
+ .skeleton-line { height: 13px; margin-bottom: 10px; border-radius: 4px; }
+ .skeleton-line.short { width: 55%; }
+ .skeleton-line.medium { width: 75%; }
+ .skeleton-circle {
+ width: 42px; height: 42px; border-radius: 50%; flex-shrink: 0;
+ }
+ .skeleton-block { height: 70px; border-radius: var(--radius-sm); margin-bottom: 10px; }
+
+ /* Asset list skeleton rows */
+ .asset-skeleton {
+ display: flex; align-items: center; gap: 12px;
+ padding: 14px 12px; border-bottom: 1px solid rgba(255,255,255,0.04);
+ }
+ .asset-skeleton .skel-icon {
+ width: 42px; height: 42px; border-radius: var(--radius-sm);
+ }
+ .asset-skeleton .skel-lines { flex: 1; display: flex; flex-direction: column; gap: 7px; }
+ .asset-skeleton .skel-line { height: 12px; border-radius: 4px; }
+ .asset-skeleton .skel-line.s1 { width: 65%; }
+ .asset-skeleton .skel-line.s2 { width: 45%; }
+ .asset-skeleton .skel-line.s3 { width: 30%; }
+
+ /* Dashboard shimmer placeholders */
+ .shimmer-stat {
+ height: 40px; border-radius: var(--radius);
+ }
+ .shimmer-bar {
+ height: 13px; border-radius: 4px; margin-bottom: 8px;
+ }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ PAGE TRANSITION ENHANCEMENTS
+ ═══════════════════════════════════════════════════════════════════════ */
+ @keyframes fadeInUp {
+ from { opacity: 0; transform: translateY(12px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+ @keyframes fadeInScale {
+ from { opacity: 0; transform: scale(0.96); }
+ to { opacity: 1; transform: scale(1); }
+ }
+ .fade-in-up { animation: fadeInUp 0.35s ease-out; }
+ .fade-in-scale { animation: fadeInScale 0.3s ease-out; }
+ .page-enter {
+ animation: tabSlideIn 0.28s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ PULL-TO-REFRESH
+ ═══════════════════════════════════════════════════════════════════════ */
+ .ptr-indicator {
+ text-align: center; padding: 10px; color: var(--text2);
+ font-size: 12px; display: flex; align-items: center; justify-content: center;
+ gap: 8px; max-height: 0; overflow: hidden;
+ transition: max-height 0.25s ease; pointer-events: none;
+ }
+ .ptr-indicator.pulling { max-height: 44px; color: var(--accent2); }
+ .ptr-indicator.refreshing { max-height: 44px; }
+ .ptr-spinner {
+ display: inline-block; width: 18px; height: 18px;
+ border: 2px solid var(--border); border-top-color: var(--accent);
+ border-radius: 50%; animation: spin 0.7s linear infinite;
+ }
+ .ptr-arrow {
+ font-size: 16px; transition: transform 0.2s;
+ }
+ .ptr-indicator.pulling .ptr-arrow { transform: rotate(180deg); }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ SWIPE-TO-DELETE
+ ═══════════════════════════════════════════════════════════════════════ */
+ .swipe-item-wrap {
+ position: relative; overflow: hidden;
+ }
+ .swipe-bg {
+ position: absolute; right: 0; top: 0; bottom: 0;
+ background: var(--red); color: #fff;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 16px; font-weight: 600; padding: 0 20px;
+ transition: width 0.2s; pointer-events: none; z-index: 0;
+ width: 0; overflow: hidden; white-space: nowrap;
+ }
+ .swipe-item-wrap.swiped .swipe-bg { width: 80px; }
+ .asset-item.swipe-dragging {
+ transition: none !important;
+ }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ UNDO TOAST (for swipe-delete)
+ ═══════════════════════════════════════════════════════════════════════ */
+ .undo-toast {
+ position: fixed; bottom: calc(var(--tab-height) + 12px);
+ left: 50%; transform: translateX(-50%);
+ background: var(--card2); border: 1px solid var(--border);
+ color: var(--text); font-weight: 600;
+ padding: 10px 18px; border-radius: 20px; font-size: 13px;
+ z-index: 460; opacity: 0; transition: opacity 0.3s, transform 0.3s;
+ pointer-events: none; white-space: nowrap; display: flex;
+ align-items: center; gap: 10px; box-shadow: 0 4px 16px rgba(0,0,0,0.3);
+ }
+ .undo-toast.show { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
+ .undo-toast .undo-btn {
+ background: var(--accent); color: #fff; border: none;
+ padding: 5px 14px; border-radius: 12px; font-size: 12px;
+ font-weight: 600; cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ FLOATING ACTION BUTTON
+ ═══════════════════════════════════════════════════════════════════════ */
+ .fab {
+ position: fixed; bottom: calc(var(--tab-height) + 20px); right: 20px;
+ width: 56px; height: 56px; border-radius: 50%; background: var(--accent);
+ color: #fff; border: none; display: flex; align-items: center;
+ justify-content: center; font-size: 24px; cursor: pointer;
+ z-index: 150; box-shadow: 0 4px 18px rgba(91,110,247,0.45);
+ transition: transform 0.2s, box-shadow 0.2s, opacity 0.25s;
+ -webkit-tap-highlight-color: transparent;
+ }
+ .fab:active { transform: scale(0.88); box-shadow: 0 2px 8px rgba(91,110,247,0.3); }
+ @keyframes fabPulse {
+ 0%, 100% { box-shadow: 0 4px 18px rgba(91,110,247,0.45); }
+ 50% { box-shadow: 0 4px 28px rgba(91,110,247,0.65); }
+ }
+ .fab.pulse { animation: fabPulse 2.5s ease-in-out infinite; }
+ .fab.hidden-fab { opacity: 0; pointer-events: none; transform: scale(0.5); }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ HAPTIC BUTTON MICRO-INTERACTIONS
+ ═══════════════════════════════════════════════════════════════════════ */
+ .btn-haptic:active {
+ animation: hapticTap 0.15s ease-out;
+ }
+ @keyframes hapticTap {
+ 0% { transform: scale(0.96); }
+ 50% { transform: scale(0.94); }
+ 100% { transform: scale(0.98); }
+ }
+ .btn, .tab-btn, .qa-btn, .pill, .mode-toggle, .fab {
+ -webkit-tap-highlight-color: transparent;
+ user-select: none;
+ }
+ /* Ripple effect for buttons */
+ .btn-ripple-wrap {
+ position: relative; overflow: hidden;
+ }
+ @keyframes btnRipple {
+ to { transform: scale(4); opacity: 0; }
+ }
+ .ripple-effect {
+ position: absolute; border-radius: 50%;
+ background: rgba(255,255,255,0.25);
+ transform: scale(0); animation: btnRipple 0.5s linear;
+ pointer-events: none;
+ }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ SUCCESS / CONFIRMATION FEEDBACK
+ ═══════════════════════════════════════════════════════════════════════ */
+ .success-overlay {
+ position: fixed; inset: 0; background: rgba(74,222,128,0.18);
+ z-index: 470; display: flex; align-items: center; justify-content: center;
+ opacity: 0; pointer-events: none; transition: opacity 0.3s;
+ backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
+ }
+ .success-overlay.show { opacity: 1; }
+ .success-check {
+ width: 72px; height: 72px; border-radius: 50%;
+ background: var(--green); display: flex; align-items: center;
+ justify-content: center; font-size: 36px; color: #000;
+ animation: successPop 0.45s cubic-bezier(0.68, -0.55, 0.27, 1.55);
+ }
+ @keyframes successPop {
+ 0% { transform: scale(0); opacity: 0; }
+ 60% { transform: scale(1.15); }
+ 100% { transform: scale(1); opacity: 1; }
+ }
+ .success-overlay.warning .success-check {
+ background: var(--amber); color: #000;
+ }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ EMPTY STATE ANIMATION
+ ═══════════════════════════════════════════════════════════════════════ */
+ .empty-state-card { animation: fadeInUp 0.45s ease-out; }
+ .empty-state { animation: fadeInUp 0.35s ease-out; }
+ .guidance-card { animation: fadeInUp 0.4s ease-out; }
+
+ /* ═══════════════════════════════════════════════════════════════════════
+ ENHANCED TOAST ANIMATION
+ ═══════════════════════════════════════════════════════════════════════ */
+ .toast {
+ transform: translateX(-50%) translateY(20px);
+ transition: opacity 0.3s, transform 0.3s;
+ }
+ .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
+ .toast.success-toast { background: var(--green); color: #000; }
+ .toast.error { background: var(--red); color: #fff; }
+
+
+
+ /* =========================================================================
+ PREFERS REDUCED MOTION
+ ========================================================================= */
+ @media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+ }
+