diff --git a/src/pages/nearle/dispatch/CompareDataPanel.js b/src/pages/nearle/dispatch/CompareDataPanel.js
new file mode 100644
index 0000000..8b0f019
--- /dev/null
+++ b/src/pages/nearle/dispatch/CompareDataPanel.js
@@ -0,0 +1,956 @@
+import React, { useMemo } from 'react';
+import {
+ MdPublic,
+ MdSwapHoriz,
+ MdExpandMore,
+ MdCheckCircle,
+ MdAccessTime,
+ MdAccountBalanceWallet,
+ MdStraighten,
+ MdTrendingUp,
+ MdTrendingDown,
+ MdErrorOutline,
+ MdFormatListBulleted,
+ MdTimer,
+ MdWarning,
+ MdClose
+} from 'react-icons/md';
+import {
+ stepColor,
+ getStatusStyle,
+ FINAL_STATUSES,
+ SKIPPED_STATUSES,
+ ordinal
+} from './dispatchShared';
+
+// Right-side data panel rendered in Compare mode. Pure presentation +
+// memoized derivations: feed it the comparison state from Dispatch and
+// the panel handles its own layout (compliance score, day overview, route
+// sequence with cascade grouping, KPIs, highlights, trips, focused-step
+// details, deviations, full step list).
+//
+// Props:
+// focusedRider — the rider whose day is being compared
+// compareDeltas — per-step planned/actual deltas (see useMemo
+// in Dispatch.js)
+// compareSummary — day rollup: plannedKm/actualKm/onTime/etc
+// actualOrdered — compareDeltas sorted by sequenceStep (visit
+// order). Used by the route-sequence section.
+// focusedCompareStep — currently focused step (1..N) or null
+// setFocusedCompareStep — setter; pass a function-updater for toggle
+// sequenceOpen — whether the "Route sequence" section is open
+// setSequenceOpen — setter for sequenceOpen
+// expandedSeqGroups — Set of expanded sequence-diff group indices
+// setExpandedSeqGroups — setter (Set state)
+// onClose — called when the user clicks the × header btn
+function CompareDataPanel({
+ focusedRider,
+ compareDeltas,
+ compareSummary,
+ actualOrdered,
+ focusedCompareStep,
+ setFocusedCompareStep,
+ sequenceOpen,
+ setSequenceOpen,
+ expandedSeqGroups,
+ setExpandedSeqGroups,
+ onClose
+}) {
+ // All derivations live in a single useMemo so the cost of re-running
+ // them is paid only when an upstream input actually changes — not on
+ // every parent render (e.g. cursor moving over the map, sync toggle
+ // toggling, etc.). Keeping them grouped also makes the data contract
+ // visible at a glance.
+ const view = useMemo(() => {
+ const sum = compareSummary;
+ const totalSteps = sum.onTime + sum.late;
+ const totalProfit = focusedRider.orders.reduce(
+ (s, o) => s + parseFloat(o.profit || 0),
+ 0
+ );
+ const isLoss = totalProfit < 0;
+ const deviations = compareDeltas.filter((d) => d.anomaly);
+ const delivered = compareDeltas.filter((d) =>
+ FINAL_STATUSES.has(String(d.orderstatus || '').toLowerCase())
+ ).length;
+ const skipped = compareDeltas.filter((d) =>
+ SKIPPED_STATUSES.has(String(d.orderstatus || '').toLowerCase())
+ ).length;
+ const stepDeltaPct =
+ sum.kmDeltaPct == null
+ ? ''
+ : sum.kmDeltaPct > 25
+ ? 'is-over'
+ : sum.kmDeltaPct < -5
+ ? 'is-under'
+ : '';
+
+ // Compliance score (0-100): 60% delivered + 25% on-time + 15% no-deviation.
+ const totalForScore = compareDeltas.length || 1;
+ const onTimeForScore = sum.onTime + sum.late || 1;
+ const score = Math.round(
+ (delivered / totalForScore) * 60 +
+ (sum.onTime / onTimeForScore) * 25 +
+ ((totalForScore - sum.anomalies) / totalForScore) * 15
+ );
+ const scoreColor = score >= 85 ? '#16a34a' : score >= 65 ? '#f59e0b' : '#dc2626';
+ const scoreLabel = score >= 85 ? 'Excellent' : score >= 65 ? 'Acceptable' : 'Needs review';
+
+ // KPIs derived from delivery timestamps.
+ const withActual = compareDeltas.filter((d) => d.actualTs);
+ const firstDelivery = withActual.reduce(
+ (acc, d) => (!acc || d.actualTs.isBefore(acc) ? d.actualTs : acc),
+ null
+ );
+ const lastDelivery = withActual.reduce(
+ (acc, d) => (!acc || d.actualTs.isAfter(acc) ? d.actualTs : acc),
+ null
+ );
+ const activeMin =
+ firstDelivery && lastDelivery
+ ? Math.max(0, lastDelivery.diff(firstDelivery, 'minute'))
+ : 0;
+ const avgPerStop =
+ compareDeltas.length > 1
+ ? Math.round(activeMin / (compareDeltas.length - 1))
+ : 0;
+ const avgSpeed =
+ activeMin > 0 ? (sum.actualKm / (activeMin / 60)).toFixed(1) : null;
+
+ // Best / worst step.
+ const readyDeltas = compareDeltas.filter(
+ (d) => !d.isLoading && d.coordsCount > 0
+ );
+ const bestStep =
+ readyDeltas
+ .filter((d) => d.timeDeltaMin != null && !d.anomaly)
+ .sort((a, b) => a.timeDeltaMin - b.timeDeltaMin)[0] || null;
+ const worstStep =
+ readyDeltas
+ .filter((d) => d.anomaly)
+ .sort((a, b) => {
+ const sa =
+ Math.abs(a.kmDeltaPct || 0) + (a.timeDeltaMin > 0 ? a.timeDeltaMin : 0);
+ const sb =
+ Math.abs(b.kmDeltaPct || 0) + (b.timeDeltaMin > 0 ? b.timeDeltaMin : 0);
+ return sb - sa;
+ })[0] || null;
+
+ // Route sequence — which actual-visit positions don't match the
+ // dispatch-planned step.
+ const outOfOrderSteps = actualOrdered.filter((d, i) => {
+ const planned = d.order?.step;
+ return planned != null && planned !== i + 1;
+ });
+
+ // Cascade-aware grouping of out-of-order steps: consecutive entries
+ // with the same `delta` collapse into one "N consecutive shifted +K"
+ // card so a single bad first stop doesn't paint 12 noisy rows.
+ const seqRuns = [];
+ outOfOrderSteps.forEach((d) => {
+ const planned = d.order?.step;
+ const actualPos =
+ actualOrdered.findIndex((x) => x.sequenceStep === d.sequenceStep) + 1;
+ const delta = actualPos - planned;
+ const last = seqRuns[seqRuns.length - 1];
+ if (last && last.delta === delta && last.lastActualPos + 1 === actualPos) {
+ last.items.push({ d, planned, actualPos, delta });
+ last.lastActualPos = actualPos;
+ } else {
+ seqRuns.push({
+ delta,
+ items: [{ d, planned, actualPos, delta }],
+ lastActualPos: actualPos
+ });
+ }
+ });
+
+ // Trip-by-trip rollup.
+ const tripBuckets = {};
+ focusedRider.orders.forEach((o) => {
+ const t = o.trip_number || 1;
+ if (!tripBuckets[t]) tripBuckets[t] = [];
+ tripBuckets[t].push(o);
+ });
+ const tripList = Object.entries(tripBuckets)
+ .sort(([a], [b]) => Number(a) - Number(b))
+ .map(([tNum, tOrders]) => ({
+ tNum,
+ count: tOrders.length,
+ plannedKm: tOrders.reduce((s, o) => s + parseFloat(o.kms || 0), 0),
+ actualKm: tOrders.reduce(
+ (s, o) => s + parseFloat(o.actualkms || o.kms || 0),
+ 0
+ ),
+ profit: tOrders.reduce((s, o) => s + parseFloat(o.profit || 0), 0),
+ delivered: tOrders.filter((o) =>
+ FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase())
+ ).length
+ }));
+
+ return {
+ sum,
+ totalSteps,
+ totalProfit,
+ isLoss,
+ deviations,
+ delivered,
+ skipped,
+ stepDeltaPct,
+ score,
+ scoreColor,
+ scoreLabel,
+ firstDelivery,
+ lastDelivery,
+ activeMin,
+ avgPerStop,
+ avgSpeed,
+ bestStep,
+ worstStep,
+ outOfOrderSteps,
+ seqRuns,
+ tripList
+ };
+ }, [focusedRider, compareDeltas, compareSummary, actualOrdered]);
+
+ const focused =
+ focusedCompareStep != null
+ ? compareDeltas.find((d) => d.sequenceStep === focusedCompareStep)
+ : null;
+
+ const toggleSeqGroup = (idx) => {
+ setExpandedSeqGroups((prev) => {
+ const next = new Set(prev);
+ if (next.has(idx)) next.delete(idx);
+ else next.add(idx);
+ return next;
+ });
+ };
+
+ const focusStep = (sequenceStep) => {
+ setFocusedCompareStep((prev) => (prev === sequenceStep ? null : sequenceStep));
+ };
+
+ // Renders a single shifted-step diff card (used both stand-alone and
+ // nested under an expanded group).
+ const renderDiffRow = (item, focusable = true) => {
+ const { d, planned, actualPos, delta } = item;
+ return (
+
focusStep(d.sequenceStep)}
+ >
+
+ {planned || d.sequenceStep}
+
+
+
+ {d.deliverycustomer || `Step ${planned || d.sequenceStep}`}
+
+
+ Visited {ordinal(actualPos)} {' '}
+ · planned {ordinal(planned)}
+
+
+
+ {delta > 0 ? `+${delta}` : `${delta}`}
+
+
+ );
+ };
+
+ const {
+ sum,
+ totalSteps,
+ totalProfit,
+ isLoss,
+ deviations,
+ delivered,
+ skipped,
+ stepDeltaPct,
+ score,
+ scoreColor,
+ scoreLabel,
+ firstDelivery,
+ lastDelivery,
+ activeMin,
+ avgPerStop,
+ avgSpeed,
+ bestStep,
+ worstStep,
+ outOfOrderSteps,
+ seqRuns,
+ tripList
+ } = view;
+
+ return (
+
+
+
+
+
+
{focusedRider.riderName}
+
PLANNED vs ACTUAL
+
+
+
+
+
+
+
+
+ {/* Compliance score — headline gauge blending delivery, on-time,
+ and route-fidelity into one number. */}
+
+
+
+
+
+ {scoreLabel}
+
+
Compliance score
+
+ {delivered}/{compareDeltas.length} delivered
+ {sum.anomalies > 0
+ ? ` · ${sum.anomalies} deviation${sum.anomalies > 1 ? 's' : ''}`
+ : ''}
+ {sum.late > 0 ? ` · ${sum.late} late` : ''}
+ {skipped > 0 ? ` · ${skipped} skipped` : ''}
+
+
+
+
+
+
+
+
+ Day overview
+
+
+
+
+ Distance
+
+
+ {sum.actualKm.toFixed(1)}
+ km
+
+
+ planned {sum.plannedKm.toFixed(1)} km
+
+
+
0 ? ' is-warn' : ''}`}>
+
+ Deviation
+
+
+ {sum.kmDeltaPct != null
+ ? `${sum.kmDeltaPct > 0 ? '+' : ''}${sum.kmDeltaPct.toFixed(0)}%`
+ : '—'}
+
+
+ {sum.anomalies > 0 ? `${sum.anomalies} flagged` : 'within plan'}
+
+
+
0 ? ' is-warn' : ''}`}>
+
+ On-time
+
+
+ {sum.onTime}
+ {totalSteps > 0 && (
+ /{totalSteps}
+ )}
+
+
+ {sum.late > 0 ? `${sum.late} late` : 'all on schedule'}
+
+
+
+
+ {isLoss ? : }{' '}
+ {isLoss ? 'Loss' : 'Profit'}
+
+
+ {isLoss ? '-' : ''}₹{Math.abs(totalProfit).toFixed(0)}
+
+
+ {focusedRider.orders.length}{' '}
+ {focusedRider.orders.length === 1 ? 'order' : 'orders'}
+ {' · '}
+ {delivered} delivered
+
+
+
+
+
+ {/* Route sequence — collapsible, default open. Shows planned vs
+ actual visit order with cascade-aware diff grouping. */}
+ {compareDeltas.length > 0 && (
+
+ setSequenceOpen((v) => !v)}
+ role="button"
+ aria-expanded={sequenceOpen}
+ title={sequenceOpen ? 'Collapse route sequence' : 'Expand route sequence'}
+ >
+
+
+
+ Route sequence
+ 0 ? ' is-warn' : ' is-good'}`}
+ >
+ {outOfOrderSteps.length > 0
+ ? `${outOfOrderSteps.length} out of order`
+ : 'In order'}
+
+
+
+
+
+ {sequenceOpen && (
+
+ {outOfOrderSteps.length > 0 ? (
+
+ {seqRuns.map((run, runIdx) => {
+ if (run.items.length === 1) {
+ return renderDiffRow(run.items[0]);
+ }
+ const first = run.items[0];
+ const last = run.items[run.items.length - 1];
+ const isOpen = expandedSeqGroups.has(runIdx);
+ const deltaStr =
+ run.delta > 0 ? `+${run.delta}` : `${run.delta}`;
+ const groupFocused = run.items.some(
+ (it) => it.d.sequenceStep === focusedCompareStep
+ );
+ return (
+
+ toggleSeqGroup(runIdx)}
+ aria-expanded={isOpen}
+ >
+
+
+
+ {run.items.length}×
+
+
+
+
+ {run.items.length} consecutive steps shifted{' '}
+ {deltaStr}
+
+
+ Planned {ordinal(first.planned)}–{ordinal(last.planned)}{' '}
+ visited{' '}
+
+ {ordinal(first.actualPos)}–{ordinal(last.actualPos)}
+
+
+
+ {deltaStr}
+
+
+
+
+ {isOpen && (
+
+
+ {run.items.map((it) => renderDiffRow(it, false))}
+
+
+ )}
+
+ );
+ })}
+
+ ) : (
+
+ Rider followed the planned route in order.
+
+ )}
+
+ )}
+
+ )}
+
+ {/* Timing KPIs */}
+ {(firstDelivery || lastDelivery) && (
+
+
+
+ Timing KPIs
+
+
+
+
First delivery
+
+ {firstDelivery ? firstDelivery.format('hh:mm A') : '—'}
+
+
+
+
Last delivery
+
+ {lastDelivery ? lastDelivery.format('hh:mm A') : '—'}
+
+
+
+
Active time
+
+ {activeMin > 0
+ ? activeMin >= 60
+ ? `${Math.floor(activeMin / 60)}h ${activeMin % 60}m`
+ : `${activeMin}m`
+ : '—'}
+
+
+
+
Avg / stop
+
+ {avgPerStop > 0 ? `${avgPerStop}m` : '—'}
+
+
+ {avgSpeed != null && (
+
+
Avg speed
+
+ {avgSpeed}
+ km/h
+
+
+ )}
+
+
+ )}
+
+ {/* Highlights — best/worst step quick-pick. */}
+ {(bestStep || worstStep) && (
+
+
+
+ Highlights
+
+
+ {bestStep && (
+
focusStep(bestStep.sequenceStep)}
+ role="button"
+ title="Focus this step"
+ >
+
+ {bestStep.sequenceStep}
+
+
+
+ Fastest stop
+
+
+ {bestStep.deliverycustomer || `Step ${bestStep.sequenceStep}`}
+
+
+ {bestStep.timeDeltaMin != null
+ ? `${bestStep.timeDeltaMin > 0 ? '+' : ''}${bestStep.timeDeltaMin} min vs plan`
+ : 'on schedule'}
+
+
+
+ )}
+ {worstStep && (
+
focusStep(worstStep.sequenceStep)}
+ role="button"
+ title="Focus this step"
+ >
+
+ {worstStep.sequenceStep}
+
+
+
+ Biggest deviation
+
+
+ {worstStep.deliverycustomer || `Step ${worstStep.sequenceStep}`}
+
+
+ {worstStep.kmDeltaPct != null
+ ? `${worstStep.kmDeltaPct > 0 ? '+' : ''}${worstStep.kmDeltaPct.toFixed(0)}% route`
+ : ''}
+ {worstStep.timeDeltaMin != null && worstStep.timeDeltaMin > 0
+ ? ` · +${worstStep.timeDeltaMin}m late`
+ : ''}
+
+
+
+ )}
+
+
+ )}
+
+ {/* Trip breakdown — only when rider ran >1 trip. */}
+ {tripList.length > 1 && (
+
+
+
+ Trips ({tripList.length})
+
+
+ {tripList.map((t) => {
+ const tripLoss = t.profit < 0;
+ return (
+
+
+ Trip {t.tNum}
+
+ {t.delivered}/{t.count} delivered
+
+
+
+
+
+ {t.actualKm.toFixed(1)}km
+ / {t.plannedKm.toFixed(1)}
+
+
+
+ {tripLoss ? '-' : ''}₹{Math.abs(t.profit).toFixed(0)}
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Focused-step deep-dive — appears only when a step is selected. */}
+ {focused && (() => {
+ const color = stepColor(focused.sequenceStep - 1);
+ const kmDeltaSign = focused.kmDelta >= 0 ? '+' : '';
+ const kmDeltaCls = focused.anomaly
+ ? 'is-over'
+ : focused.kmDelta < -0.1
+ ? 'is-under'
+ : '';
+ const timeDeltaCls =
+ focused.timeDeltaMin != null
+ ? focused.timeDeltaMin > 10
+ ? 'is-over'
+ : focused.timeDeltaMin < -2
+ ? 'is-under'
+ : ''
+ : '';
+ const statusStyle = getStatusStyle(focused.orderstatus);
+ const profit = parseFloat(focused.order?.profit || 0);
+ return (
+
+
+
+
+ Step {focused.sequenceStep} details
+
+ setFocusedCompareStep(null)}
+ title="Clear step focus"
+ >
+ Show all
+
+
+
+
+
+ {focused.sequenceStep}
+
+
+
+ {focused.deliverycustomer || `Step ${focused.sequenceStep}`}
+
+
+ {focused.pickupcustomer ? `from ${focused.pickupcustomer} · ` : ''}
+ Order #{focused.orderid}
+
+
+ {focused.orderstatus && (
+
+ {statusStyle.label}
+
+ )}
+
+
+
+ Distance
+
+ {focused.actualKm.toFixed(2)}{' '}
+ km
+
+
+ planned {focused.plannedKm.toFixed(2)} km
+
+
+
+ Δ Route
+
+ {kmDeltaSign}
+ {focused.kmDelta.toFixed(2)} km
+
+
+ {focused.kmDeltaPct != null
+ ? `${kmDeltaSign}${focused.kmDeltaPct.toFixed(0)}% vs plan`
+ : 'no planned km'}
+
+
+
+ Time
+
+ {focused.timeDeltaMin != null
+ ? `${focused.timeDeltaMin > 0 ? '+' : ''}${focused.timeDeltaMin} min`
+ : '—'}
+
+
+ {focused.actualTs && focused.expectedTs
+ ? `${focused.actualTs.format('HH:mm')} vs ${focused.expectedTs.format('HH:mm')}`
+ : focused.actualTs
+ ? `delivered ${focused.actualTs.format('HH:mm')}`
+ : 'in flight'}
+
+
+ {!Number.isNaN(profit) && profit !== 0 && (
+
+
+ {profit < 0 ? 'Loss' : 'Profit'}
+
+
+ {profit < 0 ? '-' : ''}₹{Math.abs(profit).toFixed(0)}
+
+ order revenue
+
+ )}
+
+
+
+ );
+ })()}
+
+ {/* Deviations list — anomaly-only steps. */}
+ {deviations.length > 0 && (
+
+
+
+
+
+
+ Deviations ({deviations.length})
+
+
+
+ {deviations.map((d) => {
+ const color = stepColor(d.sequenceStep - 1);
+ const kmSign = d.kmDelta >= 0 ? '+' : '';
+ return (
+ focusStep(d.sequenceStep)}
+ >
+
+ {d.sequenceStep}
+
+
+
+ {d.deliverycustomer || `Step ${d.sequenceStep}`}
+
+
+ {d.kmDeltaPct != null && (
+
+ {kmSign}{d.kmDeltaPct.toFixed(0)}% route
+
+ )}
+ {d.timeDeltaMin != null && d.timeDeltaMin > 10 && (
+
+ +{d.timeDeltaMin}m late
+
+ )}
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {/* Full step list. */}
+
+
+
+
+
+
+ Steps ({compareDeltas.length})
+
+
+ {delivered}/{compareDeltas.length} delivered
+
+
+
+ {compareDeltas.map((d) => {
+ const color = stepColor(d.sequenceStep - 1);
+ const statusLow = String(d.orderstatus || '').toLowerCase();
+ const isDelivered = FINAL_STATUSES.has(statusLow);
+ const isSkipped = SKIPPED_STATUSES.has(statusLow);
+ const isCorrect = isDelivered && !d.anomaly;
+ const isFocused = focusedCompareStep === d.sequenceStep;
+ const statusStyle = getStatusStyle(d.orderstatus);
+ const profit = parseFloat(d.order?.profit || 0);
+ const kmSign = d.kmDelta >= 0 ? '+' : '';
+ const timeCls =
+ d.timeDeltaMin != null
+ ? d.timeDeltaMin > 10
+ ? 'is-over'
+ : d.timeDeltaMin < -2
+ ? 'is-under'
+ : ''
+ : '';
+ const stepCls = [
+ 'cdp-step',
+ isFocused ? 'is-focused' : '',
+ d.anomaly ? 'is-anomaly' : '',
+ isCorrect ? 'is-correct' : '',
+ isSkipped ? 'is-skipped' : '',
+ d.isLoading ? 'is-loading' : ''
+ ].filter(Boolean).join(' ');
+ return (
+ focusStep(d.sequenceStep)}
+ >
+
+ {d.sequenceStep}
+ {isCorrect && (
+
+
+
+ )}
+ {d.anomaly && (
+
+
+
+ )}
+
+
+
+
+ {d.deliverycustomer || `Step ${d.sequenceStep}`}
+
+ {d.orderstatus && (
+
+ {statusStyle.label}
+
+ )}
+
+
+ {d.pickupcustomer ? `from ${d.pickupcustomer} · ` : ''}
+ Order #{d.orderid}
+
+
+
+
+ {d.actualKm.toFixed(1)}km
+ {d.plannedKm > 0 && (
+
+ {' '}{kmSign}{d.kmDelta.toFixed(1)}
+
+ )}
+
+
+
+ {d.actualTs ? d.actualTs.format('HH:mm') : '—'}
+ {d.timeDeltaMin != null && (
+
+ {' '}{d.timeDeltaMin > 0 ? '+' : ''}{d.timeDeltaMin}m
+
+ )}
+
+ {!Number.isNaN(profit) && profit !== 0 && (
+
+
+ {profit < 0 ? '-' : ''}₹{Math.abs(profit).toFixed(0)}
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
+
+export default CompareDataPanel;
diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css
index 773de64..f813660 100644
--- a/src/pages/nearle/dispatch/Dispatch.css
+++ b/src/pages/nearle/dispatch/Dispatch.css
@@ -15,7 +15,7 @@
--shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.08);
}
-.testing-container {
+.dispatch-container {
width: calc(100% + 48px);
height: calc(100vh - 88px);
margin: -24px;
@@ -31,7 +31,7 @@
/* Embedded mode: rendered inside a parent container (e.g. a Dialog),
so drop the negative margin and viewport-based sizing that assumes
the standalone /dispatch page is wrapped in MainCard's 24px padding. */
-.testing-container.embedded {
+.dispatch-container.embedded {
width: 100%;
height: 100%;
margin: 0;
@@ -39,14 +39,14 @@
min-height: 0;
}
-.testing-container * {
+.dispatch-container * {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Header */
-.testing-container #hdr {
+.dispatch-container #hdr {
height: 56px;
flex-shrink: 0;
display: flex;
@@ -58,13 +58,13 @@
z-index: 10;
}
-.testing-container .logo {
+.dispatch-container .logo {
display: flex;
align-items: center;
gap: 12px;
}
-.testing-container .logo-badge {
+.dispatch-container .logo-badge {
width: 32px;
height: 32px;
border-radius: 8px;
@@ -77,14 +77,14 @@
color: #fff;
}
-.testing-container .logo-name {
+.dispatch-container .logo-name {
font-size: 18px;
font-weight: 800;
color: var(--text);
letter-spacing: -0.02em;
}
-.testing-container .logo-name em {
+.dispatch-container .logo-name em {
color: var(--accent);
font-style: normal;
opacity: 0.8;
@@ -93,12 +93,12 @@
/* Operating-city pill — sits to the RIGHT of the "Dispatch" heading inline. */
/* The location pill is now an interactive dropdown trigger. Wrapped in
.logo-city-wrap so the absolute-positioned menu below anchors to it. */
-.testing-container .logo-city-wrap {
+.dispatch-container .logo-city-wrap {
position: relative;
display: inline-block;
}
-.testing-container .logo-city {
+.dispatch-container .logo-city {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -116,32 +116,32 @@
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
-.testing-container .logo-city:hover {
+.dispatch-container .logo-city:hover {
background: rgba(123, 31, 162, 0.14);
border-color: rgba(123, 31, 162, 0.45);
}
-.testing-container .logo-city.open {
+.dispatch-container .logo-city.open {
background: rgba(123, 31, 162, 0.18);
border-color: rgba(123, 31, 162, 0.55);
box-shadow: 0 4px 12px rgba(123, 31, 162, 0.18);
}
-.testing-container .logo-city svg {
+.dispatch-container .logo-city svg {
font-size: 13px;
flex-shrink: 0;
}
-.testing-container .logo-city-caret {
+.dispatch-container .logo-city-caret {
font-size: 15px;
transition: transform 0.2s ease;
}
-.testing-container .logo-city.open .logo-city-caret {
+.dispatch-container .logo-city.open .logo-city-caret {
transform: rotate(180deg);
}
-.testing-container .logo-city-text {
+.dispatch-container .logo-city-text {
max-width: 180px;
white-space: nowrap;
overflow: hidden;
@@ -149,7 +149,7 @@
}
/* Dropdown menu — anchored under the trigger, scrolls if there are many hubs. */
-.testing-container .logo-city-menu {
+.dispatch-container .logo-city-menu {
position: absolute;
top: calc(100% + 6px);
left: 0;
@@ -170,16 +170,16 @@
to { opacity: 1; transform: translateY(0); }
}
-.testing-container .logo-city-menu::-webkit-scrollbar {
+.dispatch-container .logo-city-menu::-webkit-scrollbar {
width: 6px;
}
-.testing-container .logo-city-menu::-webkit-scrollbar-thumb {
+.dispatch-container .logo-city-menu::-webkit-scrollbar-thumb {
background: rgba(123, 31, 162, 0.3);
border-radius: 999px;
}
-.testing-container .logo-city-option {
+.dispatch-container .logo-city-option {
display: flex;
align-items: center;
gap: 8px;
@@ -197,48 +197,48 @@
transition: background 0.12s ease;
}
-.testing-container .logo-city-option:hover {
+.dispatch-container .logo-city-option:hover {
background: rgba(123, 31, 162, 0.06);
}
-.testing-container .logo-city-option.active {
+.dispatch-container .logo-city-option.active {
background: rgba(123, 31, 162, 0.1);
color: #7b1fa2;
}
-.testing-container .logo-city-option-icon {
+.dispatch-container .logo-city-option-icon {
font-size: 14px;
color: #7b1fa2;
flex-shrink: 0;
}
-.testing-container .logo-city-option span:not(.logo-city-option-check) {
+.dispatch-container .logo-city-option span:not(.logo-city-option-check) {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
-.testing-container .logo-city-option-check {
+.dispatch-container .logo-city-option-check {
color: #7b1fa2;
font-weight: 800;
flex-shrink: 0;
}
-.testing-container .hdr-sep {
+.dispatch-container .hdr-sep {
width: 1px;
height: 20px;
background: var(--border);
margin: 0 4px;
}
-.testing-container .hdr-meta {
+.dispatch-container .hdr-meta {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
-.testing-container #clock {
+.dispatch-container #clock {
font-size: 13px;
color: var(--text);
font-weight: 600;
@@ -252,7 +252,7 @@
/* Header right-cluster — profit/loss + orders pill + date picker, sits to the
LEFT of the running clock. Pushed against the clock with margin-left:auto so
the .logo on the left stays anchored and the cluster floats right. */
-.testing-container .hdr-stats {
+.dispatch-container .hdr-stats {
display: flex;
align-items: center;
gap: 10px;
@@ -263,7 +263,7 @@
}
/* Tabs */
-.testing-container #strat-row {
+.dispatch-container #strat-row {
height: 48px;
flex-shrink: 0;
display: flex;
@@ -274,7 +274,7 @@
border-bottom: 1px solid var(--border);
}
-.testing-container .sbt {
+.dispatch-container .sbt {
padding: 8px 14px;
border-radius: 10px;
border: 1px solid var(--border);
@@ -291,13 +291,13 @@
font-family: inherit;
}
-.testing-container .sbt:hover {
+.dispatch-container .sbt:hover {
background: var(--bg-sub);
color: var(--text);
border-color: var(--text-muted);
}
-.testing-container .sbt.active {
+.dispatch-container .sbt.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
@@ -306,7 +306,7 @@
/* SVG icon slot inside each tab button — fixed square, color inherits from button
so active-state white propagates without per-tab overrides. */
-.testing-container .sbt .sbt-icon {
+.dispatch-container .sbt .sbt-icon {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -318,7 +318,7 @@
color: inherit;
}
-.testing-container .sbt .sbt-icon svg {
+.dispatch-container .sbt .sbt-icon svg {
width: 1em;
height: 1em;
display: block;
@@ -328,7 +328,7 @@
}
/* Strat-row quick stats — total orders + profit/loss chips next to the view-mode buttons */
-.testing-container .strat-stats {
+.dispatch-container .strat-stats {
display: inline-flex;
align-items: center;
gap: 8px;
@@ -340,13 +340,13 @@
/* Right-floating variant — used for the profit/loss chip when there's no
live-controls block to nest inside. */
-.testing-container .strat-stats.strat-stats-right {
+.dispatch-container .strat-stats.strat-stats-right {
margin-left: auto;
padding-left: 0;
border-left: none;
}
-.testing-container .strat-stat {
+.dispatch-container .strat-stat {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -362,12 +362,12 @@
white-space: nowrap;
}
-.testing-container .strat-stat-icon {
+.dispatch-container .strat-stat-icon {
font-size: 13px;
line-height: 1;
}
-.testing-container .strat-stat-label {
+.dispatch-container .strat-stat-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
@@ -375,49 +375,49 @@
color: var(--text-muted);
}
-.testing-container .strat-stat-value {
+.dispatch-container .strat-stat-value {
font-size: 13px;
font-weight: 800;
}
-.testing-container .strat-stat-orders {
+.dispatch-container .strat-stat-orders {
background: var(--accent-soft);
border-color: rgba(59, 130, 246, 0.25);
}
-.testing-container .strat-stat-orders .strat-stat-value {
+.dispatch-container .strat-stat-orders .strat-stat-value {
color: var(--accent);
}
-.testing-container .strat-stat-profit {
+.dispatch-container .strat-stat-profit {
background: rgba(34, 197, 94, 0.1);
border-color: rgba(34, 197, 94, 0.3);
}
-.testing-container .strat-stat-profit .strat-stat-value,
-.testing-container .strat-stat-profit .strat-stat-label {
+.dispatch-container .strat-stat-profit .strat-stat-value,
+.dispatch-container .strat-stat-profit .strat-stat-label {
color: var(--success);
}
-.testing-container .strat-stat-loss {
+.dispatch-container .strat-stat-loss {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.35);
}
-.testing-container .strat-stat-loss .strat-stat-value,
-.testing-container .strat-stat-loss .strat-stat-label {
+.dispatch-container .strat-stat-loss .strat-stat-value,
+.dispatch-container .strat-stat-loss .strat-stat-label {
color: #dc2626;
}
/* Live data controls (date picker + load status) */
-.testing-container .live-controls {
+.dispatch-container .live-controls {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
}
-.testing-container .live-status {
+.dispatch-container .live-status {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -430,17 +430,17 @@
border: 1px solid var(--border);
}
-.testing-container .live-status-ready { color: var(--success); }
-.testing-container .live-status-error { color: #ef4444; }
+.dispatch-container .live-status-ready { color: var(--success); }
+.dispatch-container .live-status-error { color: #ef4444; }
-.testing-container .live-status-sub {
+.dispatch-container .live-status-sub {
color: var(--text-muted);
font-weight: 500;
font-size: 11px;
opacity: 0.85;
}
-.testing-container .live-dot {
+.dispatch-container .live-dot {
width: 8px;
height: 8px;
border-radius: 50%;
@@ -448,15 +448,15 @@
animation: live-pulse 1.2s ease-in-out infinite;
}
-.testing-container .live-dot.ready { background: var(--success); animation: none; }
-.testing-container .live-dot.error { background: #ef4444; animation: none; }
+.dispatch-container .live-dot.ready { background: var(--success); animation: none; }
+.dispatch-container .live-dot.error { background: #ef4444; animation: none; }
@keyframes live-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.85); }
}
-.testing-container .live-date-label {
+.dispatch-container .live-date-label {
display: inline-flex;
align-items: center;
gap: 8px;
@@ -467,7 +467,7 @@
letter-spacing: 0.04em;
}
-.testing-container .live-date-label input[type="date"] {
+.dispatch-container .live-date-label input[type="date"] {
font-family: inherit;
font-size: 13px;
font-weight: 600;
@@ -481,14 +481,14 @@
transition: border-color 0.15s;
}
-.testing-container .live-date-label input[type="date"]:hover,
-.testing-container .live-date-label input[type="date"]:focus {
+.dispatch-container .live-date-label input[type="date"]:hover,
+.dispatch-container .live-date-label input[type="date"]:focus {
border-color: var(--accent);
}
/* ── Batch selector (live /dispatch only) ─────────────────────── */
-.testing-container #batch-row {
+.dispatch-container #batch-row {
display: flex;
align-items: center;
gap: 8px;
@@ -502,7 +502,7 @@
/* Horizontal scroller for the slot chips. Keeps the "Slot" label fixed on the
left and lets the chip list scroll when it overflows the viewport. */
-.testing-container .batch-scroll {
+.dispatch-container .batch-scroll {
display: flex;
align-items: center;
gap: 8px;
@@ -517,24 +517,24 @@
padding-bottom: 2px;
}
-.testing-container .batch-scroll::-webkit-scrollbar {
+.dispatch-container .batch-scroll::-webkit-scrollbar {
height: 6px;
}
-.testing-container .batch-scroll::-webkit-scrollbar-track {
+.dispatch-container .batch-scroll::-webkit-scrollbar-track {
background: transparent;
}
-.testing-container .batch-scroll::-webkit-scrollbar-thumb {
+.dispatch-container .batch-scroll::-webkit-scrollbar-thumb {
background: rgba(100, 116, 139, 0.3);
border-radius: 999px;
}
-.testing-container .batch-scroll::-webkit-scrollbar-thumb:hover {
+.dispatch-container .batch-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.55);
}
-.testing-container .batch-label {
+.dispatch-container .batch-label {
font-size: 10px;
font-weight: 800;
color: var(--text-muted);
@@ -547,14 +547,14 @@
/* Slot-time-field dropdown — picks which timestamp column drives slot
bucketing. Styled to match the location-pill dropdown in the header so
both feel like the same kind of filter control. */
-.testing-container .time-field-wrap {
+.dispatch-container .time-field-wrap {
position: relative;
display: inline-block;
flex-shrink: 0;
margin-right: 4px;
}
-.testing-container .time-field-btn {
+.dispatch-container .time-field-btn {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -572,36 +572,36 @@
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
-.testing-container .time-field-btn:hover {
+.dispatch-container .time-field-btn:hover {
background: rgba(123, 31, 162, 0.14);
border-color: rgba(123, 31, 162, 0.45);
}
-.testing-container .time-field-btn.open {
+.dispatch-container .time-field-btn.open {
background: rgba(123, 31, 162, 0.18);
border-color: rgba(123, 31, 162, 0.55);
box-shadow: 0 4px 12px rgba(123, 31, 162, 0.18);
}
-.testing-container .time-field-btn svg {
+.dispatch-container .time-field-btn svg {
font-size: 13px;
flex-shrink: 0;
}
-.testing-container .time-field-caret {
+.dispatch-container .time-field-caret {
font-size: 15px;
transition: transform 0.2s ease;
}
-.testing-container .time-field-btn.open .time-field-caret {
+.dispatch-container .time-field-btn.open .time-field-caret {
transform: rotate(180deg);
}
-.testing-container .time-field-text {
+.dispatch-container .time-field-text {
white-space: nowrap;
}
-.testing-container .time-field-menu {
+.dispatch-container .time-field-menu {
position: absolute;
top: calc(100% + 6px);
left: 0;
@@ -615,7 +615,7 @@
animation: logo-city-menu-in 0.14s ease-out;
}
-.testing-container .time-field-option {
+.dispatch-container .time-field-option {
display: flex;
align-items: center;
gap: 8px;
@@ -633,22 +633,22 @@
transition: background 0.12s ease;
}
-.testing-container .time-field-option:hover {
+.dispatch-container .time-field-option:hover {
background: rgba(123, 31, 162, 0.06);
}
-.testing-container .time-field-option.active {
+.dispatch-container .time-field-option.active {
background: rgba(123, 31, 162, 0.1);
color: #7b1fa2;
}
-.testing-container .time-field-option-icon {
+.dispatch-container .time-field-option-icon {
font-size: 14px;
color: #7b1fa2;
flex-shrink: 0;
}
-.testing-container .time-field-option-check {
+.dispatch-container .time-field-option-check {
margin-left: auto;
color: #7b1fa2;
font-weight: 800;
@@ -657,14 +657,14 @@
/* Slot timings editor — popover anchored to a small "Edit slots" button in
the batch row. Lets the operator tweak start/end hours, add new slots,
delete existing ones, or reset to the default 5-slot layout. */
-.testing-container .slot-edit-wrap {
+.dispatch-container .slot-edit-wrap {
position: relative;
display: inline-block;
flex-shrink: 0;
margin-right: 4px;
}
-.testing-container .slot-edit-btn {
+.dispatch-container .slot-edit-btn {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -681,25 +681,25 @@
font-family: inherit;
}
-.testing-container .slot-edit-btn:hover {
+.dispatch-container .slot-edit-btn:hover {
background: rgba(15, 23, 42, 0.08);
border-color: rgba(15, 23, 42, 0.32);
color: #0f172a;
}
-.testing-container .slot-edit-btn.open {
+.dispatch-container .slot-edit-btn.open {
background: rgba(123, 31, 162, 0.1);
border-color: rgba(123, 31, 162, 0.5);
border-style: solid;
color: #7b1fa2;
}
-.testing-container .slot-edit-btn svg {
+.dispatch-container .slot-edit-btn svg {
font-size: 13px;
flex-shrink: 0;
}
-.testing-container .slot-edit-panel {
+.dispatch-container .slot-edit-panel {
position: absolute;
top: calc(100% + 6px);
left: 0;
@@ -713,23 +713,23 @@
animation: logo-city-menu-in 0.14s ease-out;
}
-.testing-container .slot-edit-head {
+.dispatch-container .slot-edit-head {
margin-bottom: 10px;
}
-.testing-container .slot-edit-title {
+.dispatch-container .slot-edit-title {
font-size: 13px;
font-weight: 800;
color: #0f172a;
}
-.testing-container .slot-edit-sub {
+.dispatch-container .slot-edit-sub {
font-size: 11px;
color: #64748b;
margin-top: 2px;
}
-.testing-container .slot-edit-list {
+.dispatch-container .slot-edit-list {
display: flex;
flex-direction: column;
gap: 8px;
@@ -738,14 +738,14 @@
padding-right: 2px;
}
-.testing-container .slot-edit-row {
+.dispatch-container .slot-edit-row {
display: grid;
grid-template-columns: 22px 70px 70px 1fr 28px;
align-items: center;
gap: 8px;
}
-.testing-container .slot-edit-idx {
+.dispatch-container .slot-edit-idx {
width: 22px;
height: 22px;
border-radius: 6px;
@@ -758,13 +758,13 @@
justify-content: center;
}
-.testing-container .slot-edit-field {
+.dispatch-container .slot-edit-field {
display: flex;
flex-direction: column;
gap: 2px;
}
-.testing-container .slot-edit-field-label {
+.dispatch-container .slot-edit-field-label {
font-size: 9px;
font-weight: 700;
color: #94a3b8;
@@ -772,7 +772,7 @@
letter-spacing: 0.06em;
}
-.testing-container .slot-edit-field input {
+.dispatch-container .slot-edit-field input {
width: 100%;
border: 1px solid rgba(15, 23, 42, 0.16);
border-radius: 8px;
@@ -784,13 +784,13 @@
background: #fff;
}
-.testing-container .slot-edit-field input:focus {
+.dispatch-container .slot-edit-field input:focus {
outline: none;
border-color: #7b1fa2;
box-shadow: 0 0 0 3px rgba(123, 31, 162, 0.18);
}
-.testing-container .slot-edit-preview {
+.dispatch-container .slot-edit-preview {
font-size: 11px;
color: #475569;
font-weight: 600;
@@ -799,7 +799,7 @@
text-overflow: ellipsis;
}
-.testing-container .slot-edit-remove {
+.dispatch-container .slot-edit-remove {
width: 26px;
height: 26px;
border-radius: 50%;
@@ -816,17 +816,17 @@
padding: 0;
}
-.testing-container .slot-edit-remove:hover:not(:disabled) {
+.dispatch-container .slot-edit-remove:hover:not(:disabled) {
background: rgba(220, 38, 38, 0.14);
border-color: rgba(220, 38, 38, 0.55);
}
-.testing-container .slot-edit-remove:disabled {
+.dispatch-container .slot-edit-remove:disabled {
opacity: 0.4;
cursor: not-allowed;
}
-.testing-container .slot-edit-actions {
+.dispatch-container .slot-edit-actions {
display: flex;
gap: 8px;
margin-top: 12px;
@@ -834,8 +834,8 @@
border-top: 1px dashed rgba(15, 23, 42, 0.1);
}
-.testing-container .slot-edit-add,
-.testing-container .slot-edit-reset {
+.dispatch-container .slot-edit-add,
+.dispatch-container .slot-edit-reset {
flex: 1;
border-radius: 8px;
padding: 7px 10px;
@@ -847,28 +847,28 @@
border: 1px solid transparent;
}
-.testing-container .slot-edit-add {
+.dispatch-container .slot-edit-add {
background: #7b1fa2;
color: #fff;
border-color: #7b1fa2;
}
-.testing-container .slot-edit-add:hover {
+.dispatch-container .slot-edit-add:hover {
background: #6a1591;
}
-.testing-container .slot-edit-reset {
+.dispatch-container .slot-edit-reset {
background: #fff;
color: #475569;
border-color: rgba(15, 23, 42, 0.16);
}
-.testing-container .slot-edit-reset:hover {
+.dispatch-container .slot-edit-reset:hover {
background: rgba(15, 23, 42, 0.04);
color: #0f172a;
}
-.testing-container .batch-btn {
+.dispatch-container .batch-btn {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -887,12 +887,12 @@
white-space: nowrap;
}
-.testing-container .batch-btn:hover {
+.dispatch-container .batch-btn:hover {
border-color: var(--text-muted);
color: var(--text);
}
-.testing-container .batch-btn.active {
+.dispatch-container .batch-btn.active {
color: #fff;
border-color: transparent;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
@@ -900,20 +900,20 @@
/* Unified active style for hourly slot chips — single accent gradient instead of
per-wave colors since 12 different colors would be visually noisy. */
-.testing-container .batch-btn.batch-slot.active {
+.dispatch-container .batch-btn.batch-slot.active {
background: linear-gradient(135deg, #3b82f6, #6366f1);
}
-.testing-container .batch-btn-icon {
+.dispatch-container .batch-btn-icon {
font-size: 14px;
line-height: 1;
}
-.testing-container .batch-btn-label {
+.dispatch-container .batch-btn-label {
letter-spacing: 0.01em;
}
-.testing-container .batch-btn-count {
+.dispatch-container .batch-btn-count {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -928,13 +928,13 @@
font-variant-numeric: tabular-nums;
}
-.testing-container .batch-btn.active .batch-btn-count {
+.dispatch-container .batch-btn.active .batch-btn-count {
background: rgba(255, 255, 255, 0.28);
color: #fff;
}
/* Status chips on step rows (kept for marker popup which still uses pill style) */
-.testing-container .status-chip {
+.dispatch-container .status-chip {
display: inline-flex;
align-items: center;
font-size: 10px;
@@ -948,7 +948,7 @@
}
/* Flag indicator inside step rows — matches the map marker flag visually */
-.testing-container .step-flag {
+.dispatch-container .step-flag {
display: inline-flex;
align-items: center;
gap: 5px;
@@ -956,14 +956,14 @@
flex-shrink: 0;
}
-.testing-container .step-flag-svg {
+.dispatch-container .step-flag-svg {
width: 14px;
height: 18px;
flex-shrink: 0;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.15));
}
-.testing-container .step-flag-label {
+.dispatch-container .step-flag-label {
font-size: 10px;
font-weight: 800;
letter-spacing: 0.04em;
@@ -972,12 +972,12 @@
}
/* Marker status flag (pole + banner above the numbered marker) */
-.testing-container .cmark {
+.dispatch-container .cmark {
position: relative;
}
/* Pulse: a marker glows when its row is hovered in the assignment table */
-.testing-container .cmark.pulse {
+.dispatch-container .cmark.pulse {
z-index: 1500 !important;
animation: cmark-pulse 0.8s ease-out infinite;
}
@@ -990,7 +990,7 @@
/* Leaflet sets overflow:hidden on its panes; the flag pokes up past the marker bounds,
so we let the divIcon container overflow visibly. */
-.testing-container .cmark .cmark-flag {
+.dispatch-container .cmark .cmark-flag {
position: absolute;
top: -20px;
left: 50%;
@@ -1002,7 +1002,7 @@
}
/* Live rider position bike */
-.testing-container .rider-bike {
+.dispatch-container .rider-bike {
--rider-color: #475569;
position: relative;
width: 44px;
@@ -1012,7 +1012,7 @@
justify-content: center;
}
-.testing-container .rider-bike-ring {
+.dispatch-container .rider-bike-ring {
position: absolute;
inset: 0;
border-radius: 50%;
@@ -1022,7 +1022,7 @@
animation: rider-pulse 1.8s ease-in-out infinite;
}
-.testing-container .rider-bike-svg {
+.dispatch-container .rider-bike-svg {
position: relative;
width: 26px;
height: 26px;
@@ -1033,13 +1033,13 @@
/* Bike stays upright — direction is conveyed by the route line. */
}
-.testing-container .rider-bike-svg svg {
+.dispatch-container .rider-bike-svg svg {
width: 100%;
height: 100%;
display: block;
}
-.testing-container .rider-bike-progress {
+.dispatch-container .rider-bike-progress {
position: absolute;
bottom: -16px;
left: 50%;
@@ -1067,14 +1067,14 @@
the color: green for active, red otherwise. Lives next to the synthetic
bike markers but uses a distinct visual so the operator can tell that this
one is real-GPS, not route-progress estimate. */
-.testing-container .live-rider-pin {
+.dispatch-container .live-rider-pin {
--pin-color: #16a34a;
position: relative;
width: 24px;
height: 41px;
}
-.testing-container .live-rider-pin-marker {
+.dispatch-container .live-rider-pin-marker {
position: absolute;
left: 0;
top: 0;
@@ -1087,7 +1087,7 @@
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
-.testing-container .live-rider-pin-marker::after {
+.dispatch-container .live-rider-pin-marker::after {
content: '';
position: absolute;
inset: 4px;
@@ -1095,7 +1095,7 @@
border-radius: 50%;
}
-.testing-container .live-rider-pin-label {
+.dispatch-container .live-rider-pin-label {
position: absolute;
left: 30px;
top: 2px;
@@ -1110,14 +1110,14 @@
line-height: 1.2;
}
-.testing-container .live-rider-pin-label span {
+.dispatch-container .live-rider-pin-label span {
font-weight: 500;
opacity: 0.85;
margin-left: 4px;
}
/* Body layout */
-.testing-container #body {
+.dispatch-container #body {
flex: 1;
display: flex;
min-height: 0;
@@ -1126,7 +1126,7 @@
}
/* Sidebar */
-.testing-container #sidebar {
+.dispatch-container #sidebar {
width: 400px;
flex: 0 0 400px;
background: var(--bg-sub);
@@ -1143,7 +1143,7 @@
/* Collapsed state — slide the sidebar out so the maps can use the full width.
Children stay rendered (their state is preserved) but are masked by
overflow:hidden. The peek tab below stays visible to re-open. */
-.testing-container #body.sidebar-collapsed #sidebar {
+.dispatch-container #body.sidebar-collapsed #sidebar {
width: 0;
flex: 0 0 0;
border-right-color: transparent;
@@ -1152,7 +1152,7 @@
/* Peek tab — vertical pill that hugs the sidebar's right edge. Tracks the
sidebar width so it sits flush against whichever side is currently visible:
at left:400px when expanded, at left:0 when collapsed. */
-.testing-container .sidebar-toggle-tab {
+.dispatch-container .sidebar-toggle-tab {
position: absolute;
top: 50%;
left: 400px;
@@ -1180,36 +1180,36 @@
box-shadow 0.18s ease;
}
-.testing-container .sidebar-toggle-tab:hover {
+.dispatch-container .sidebar-toggle-tab:hover {
background: linear-gradient(135deg, #6366f1, #3b82f6);
color: #fff;
transform: translate(-50%, -50%) scale(1.06);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.35);
}
-.testing-container .sidebar-toggle-tab:focus-visible {
+.dispatch-container .sidebar-toggle-tab:focus-visible {
outline: 2px solid var(--accent, #3b82f6);
outline-offset: 2px;
}
-.testing-container .sidebar-toggle-tab.is-collapsed {
+.dispatch-container .sidebar-toggle-tab.is-collapsed {
left: 0;
transform: translate(0, -50%);
border-radius: 0 10px 10px 0;
border-left: none;
}
-.testing-container .sidebar-toggle-tab.is-collapsed:hover {
+.dispatch-container .sidebar-toggle-tab.is-collapsed:hover {
transform: translate(0, -50%) scale(1.06);
}
-.testing-container .sidebar-toggle-tab svg {
+.dispatch-container .sidebar-toggle-tab svg {
display: block;
}
/* Sidebar header — moved here from the top bar. Layered card: title row with
a scope badge, an area chip, then two stat tiles for orders + riders. */
-.testing-container .sb-header {
+.dispatch-container .sb-header {
position: relative;
padding: 18px 18px 16px;
background: linear-gradient(180deg, #ffffff 0%, var(--bg-sub) 100%);
@@ -1217,7 +1217,7 @@
overflow: hidden;
}
-.testing-container .sb-header::before {
+.dispatch-container .sb-header::before {
content: '';
position: absolute;
inset: -40px -40px auto auto;
@@ -1228,12 +1228,12 @@
pointer-events: none;
}
-.testing-container .sb-header > * {
+.dispatch-container .sb-header > * {
position: relative;
}
/* Top row — title on the left, active-scope badge on the right */
-.testing-container .sb-header-top {
+.dispatch-container .sb-header-top {
display: flex;
align-items: center;
justify-content: space-between;
@@ -1241,13 +1241,13 @@
margin-bottom: 12px;
}
-.testing-container .sb-header-title {
+.dispatch-container .sb-header-title {
display: inline-flex;
align-items: center;
gap: 8px;
}
-.testing-container .sb-title-bar {
+.dispatch-container .sb-title-bar {
display: inline-block;
width: 3px;
height: 14px;
@@ -1255,14 +1255,14 @@
background: linear-gradient(180deg, var(--accent), #6366f1);
}
-.testing-container .sb-title-text {
+.dispatch-container .sb-title-text {
font-size: 12px;
font-weight: 800;
letter-spacing: 0.12em;
color: var(--text);
}
-.testing-container .sb-header-scope {
+.dispatch-container .sb-header-scope {
display: inline-flex;
align-items: center;
gap: 5px;
@@ -1281,7 +1281,7 @@
white-space: nowrap;
}
-.testing-container .sb-scope-dot {
+.dispatch-container .sb-scope-dot {
width: 6px;
height: 6px;
border-radius: 50%;
@@ -1291,7 +1291,7 @@
}
/* Area chip — small location pill */
-.testing-container .sb-header-area {
+.dispatch-container .sb-header-area {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -1305,7 +1305,7 @@
font-weight: 700;
}
-.testing-container .sb-area-icon {
+.dispatch-container .sb-area-icon {
display: inline-flex;
align-items: center;
font-size: 14px;
@@ -1313,13 +1313,13 @@
}
/* Stat tiles — two side-by-side cards, large numerals */
-.testing-container .sb-header-tiles {
+.dispatch-container .sb-header-tiles {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
-.testing-container .sb-tile {
+.dispatch-container .sb-tile {
display: flex;
align-items: center;
gap: 10px;
@@ -1331,12 +1331,12 @@
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
-.testing-container .sb-tile:hover {
+.dispatch-container .sb-tile:hover {
transform: translateY(-1px);
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.06);
}
-.testing-container .sb-tile-icon {
+.dispatch-container .sb-tile-icon {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -1347,22 +1347,22 @@
flex-shrink: 0;
}
-.testing-container .sb-tile-orders .sb-tile-icon {
+.dispatch-container .sb-tile-orders .sb-tile-icon {
background: var(--accent-soft);
color: var(--accent);
}
-.testing-container .sb-tile-riders .sb-tile-icon {
+.dispatch-container .sb-tile-riders .sb-tile-icon {
background: rgba(245, 158, 11, 0.12);
color: var(--kitchen);
}
-.testing-container .sb-tile-body {
+.dispatch-container .sb-tile-body {
min-width: 0;
line-height: 1.1;
}
-.testing-container .sb-tile-value {
+.dispatch-container .sb-tile-value {
font-size: 22px;
font-weight: 800;
letter-spacing: -0.01em;
@@ -1370,7 +1370,7 @@
font-variant-numeric: tabular-nums;
}
-.testing-container .sb-tile-label {
+.dispatch-container .sb-tile-label {
margin-top: 2px;
font-size: 10px;
font-weight: 700;
@@ -1379,7 +1379,7 @@
color: var(--text-muted);
}
-.testing-container #stats-strip {
+.dispatch-container #stats-strip {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
@@ -1388,14 +1388,14 @@
border-bottom: 1px solid var(--border);
}
-.testing-container .sc {
+.dispatch-container .sc {
background: var(--bg-sub);
padding: 12px;
border-radius: 12px;
border: 1px solid var(--border);
}
-.testing-container .sc-lbl {
+.dispatch-container .sc-lbl {
font-size: 10px;
font-weight: 700;
color: var(--text-muted);
@@ -1404,30 +1404,30 @@
margin-bottom: 4px;
}
-.testing-container .sc-val {
+.dispatch-container .sc-val {
font-size: 22px;
font-weight: 800;
color: var(--text);
line-height: 1;
}
-.testing-container .sc-val.g {
+.dispatch-container .sc-val.g {
color: var(--success);
}
-.testing-container .sc-sub {
+.dispatch-container .sc-sub {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
}
-.testing-container #riders-panel {
+.dispatch-container #riders-panel {
flex: 1;
overflow-y: auto;
padding: 16px;
}
-.testing-container .ph {
+.dispatch-container .ph {
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
@@ -1439,7 +1439,7 @@
gap: 12px;
}
-.testing-container .ph::after {
+.dispatch-container .ph::after {
content: '';
flex: 1;
height: 1px;
@@ -1447,7 +1447,7 @@
}
/* Cards */
-.testing-container .rcard {
+.dispatch-container .rcard {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
@@ -1458,20 +1458,20 @@
box-shadow: var(--shadow);
}
-.testing-container .rcard:hover {
+.dispatch-container .rcard:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
border-color: var(--accent);
}
-.testing-container .rcard-top {
+.dispatch-container .rcard-top {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
-.testing-container .kitchen-mark {
+.dispatch-container .kitchen-mark {
background: #f59e0b;
color: #fff;
width: 34px;
@@ -1486,11 +1486,11 @@
box-shadow: 0 0 20px rgba(245, 158, 11, 0.6), 0 0 40px rgba(245, 158, 11, 0.3);
}
-.testing-container .rcard-info {
+.dispatch-container .rcard-info {
flex: 1;
}
-.testing-container .rcard-emo {
+.dispatch-container .rcard-emo {
width: 40px;
height: 40px;
border-radius: 10px;
@@ -1502,19 +1502,19 @@
}
-.testing-container .rcard-name {
+.dispatch-container .rcard-name {
font-size: 15px;
font-weight: 700;
color: var(--text);
}
-.testing-container .rcard-zone {
+.dispatch-container .rcard-zone {
font-size: 12px;
color: var(--text-muted);
margin-top: 2px;
}
-.testing-container .rcard-badge {
+.dispatch-container .rcard-badge {
font-size: 12px;
font-weight: 700;
padding: 4px 10px;
@@ -1522,7 +1522,7 @@
background: var(--bg-sub);
}
-.testing-container .bar-bg {
+.dispatch-container .bar-bg {
background: var(--bg-sub);
border-radius: 4px;
height: 5px;
@@ -1530,12 +1530,12 @@
margin-bottom: 12px;
}
-.testing-container .bar-fg {
+.dispatch-container .bar-fg {
height: 100%;
border-radius: 4px;
}
-.testing-container .rcard-meta {
+.dispatch-container .rcard-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
@@ -1543,14 +1543,14 @@
font-weight: 500;
}
-.testing-container .step-ids {
+.dispatch-container .step-ids {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
-.testing-container .step-id {
+.dispatch-container .step-id {
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
@@ -1561,14 +1561,14 @@
}
/* Detail View */
-.testing-container #route-detail {
+.dispatch-container #route-detail {
flex: 1;
overflow-y: auto;
padding: 20px;
background: var(--bg);
}
-.testing-container .rd-back {
+.dispatch-container .rd-back {
background: var(--bg-sub);
border: 1px solid var(--border);
color: var(--text);
@@ -1581,11 +1581,11 @@
transition: all 0.2s;
}
-.testing-container .rd-back:hover {
+.dispatch-container .rd-back:hover {
background: var(--border);
}
-.testing-container .rd-rider-name {
+.dispatch-container .rd-rider-name {
font-size: 28px;
font-weight: 800;
letter-spacing: -0.02em;
@@ -1593,7 +1593,7 @@
line-height: 1.1;
}
-.testing-container .rd-rider-sub {
+.dispatch-container .rd-rider-sub {
font-size: 13px;
color: var(--text-muted);
margin-bottom: 24px;
@@ -1602,14 +1602,14 @@
}
/* Focused-rider stat grid — three tiles: orders / distance / profit */
-.testing-container .rd-stats-grid {
+.dispatch-container .rd-stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin: 14px 0 22px;
}
-.testing-container .rd-stat {
+.dispatch-container .rd-stat {
padding: 14px 10px 12px;
border-radius: 12px;
text-align: center;
@@ -1618,19 +1618,19 @@
transition: transform 0.15s, box-shadow 0.15s;
}
-.testing-container .rd-stat:hover {
+.dispatch-container .rd-stat:hover {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
-.testing-container .rd-stat-icon {
+.dispatch-container .rd-stat-icon {
font-size: 18px;
line-height: 1;
margin-bottom: 6px;
opacity: 0.9;
}
-.testing-container .rd-stat-value {
+.dispatch-container .rd-stat-value {
font-size: 22px;
font-weight: 800;
line-height: 1;
@@ -1639,14 +1639,14 @@
font-variant-numeric: tabular-nums;
}
-.testing-container .rd-stat-unit {
+.dispatch-container .rd-stat-unit {
font-size: 12px;
font-weight: 700;
margin-left: 3px;
opacity: 0.7;
}
-.testing-container .rd-stat-label {
+.dispatch-container .rd-stat-label {
font-size: 10px;
font-weight: 800;
color: var(--text-muted);
@@ -1656,33 +1656,33 @@
}
/* Per-stat color theming */
-.testing-container .rd-stat-orders {
+.dispatch-container .rd-stat-orders {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.09), rgba(99, 102, 241, 0.04));
border-color: rgba(59, 130, 246, 0.22);
}
-.testing-container .rd-stat-orders .rd-stat-value { color: #2563eb; }
+.dispatch-container .rd-stat-orders .rd-stat-value { color: #2563eb; }
-.testing-container .rd-stat-distance {
+.dispatch-container .rd-stat-distance {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.10), rgba(249, 115, 22, 0.04));
border-color: rgba(245, 158, 11, 0.25);
}
-.testing-container .rd-stat-distance .rd-stat-value { color: #d97706; }
+.dispatch-container .rd-stat-distance .rd-stat-value { color: #d97706; }
-.testing-container .rd-stat-profit.is-gain {
+.dispatch-container .rd-stat-profit.is-gain {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.12), rgba(20, 184, 166, 0.04));
border-color: rgba(34, 197, 94, 0.35);
}
-.testing-container .rd-stat-profit.is-gain .rd-stat-value { color: #16a34a; }
-.testing-container .rd-stat-profit.is-gain .rd-stat-label { color: #16a34a; opacity: 0.75; }
+.dispatch-container .rd-stat-profit.is-gain .rd-stat-value { color: #16a34a; }
+.dispatch-container .rd-stat-profit.is-gain .rd-stat-label { color: #16a34a; opacity: 0.75; }
-.testing-container .rd-stat-profit.is-loss {
+.dispatch-container .rd-stat-profit.is-loss {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.12), rgba(244, 63, 94, 0.04));
border-color: rgba(239, 68, 68, 0.35);
}
-.testing-container .rd-stat-profit.is-loss .rd-stat-value { color: #dc2626; }
-.testing-container .rd-stat-profit.is-loss .rd-stat-label { color: #dc2626; opacity: 0.75; }
+.dispatch-container .rd-stat-profit.is-loss .rd-stat-value { color: #dc2626; }
+.dispatch-container .rd-stat-profit.is-loss .rd-stat-label { color: #dc2626; opacity: 0.75; }
-.testing-container .trip-block {
+.dispatch-container .trip-block {
margin-bottom: 24px;
border: 1px solid var(--border);
border-radius: 16px;
@@ -1690,7 +1690,7 @@
overflow: hidden;
}
-.testing-container .trip-header {
+.dispatch-container .trip-header {
padding: 12px 16px;
display: flex;
justify-content: space-between;
@@ -1699,7 +1699,7 @@
background: #fff;
}
-.testing-container .th-badge {
+.dispatch-container .th-badge {
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
@@ -1707,7 +1707,7 @@
color: #fff;
}
-.testing-container .trip-stats {
+.dispatch-container .trip-stats {
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
@@ -1715,11 +1715,11 @@
gap: 12px;
}
-.testing-container .step-wrap {
+.dispatch-container .step-wrap {
padding: 16px;
}
-.testing-container .step-row {
+.dispatch-container .step-row {
display: flex;
gap: 16px;
padding-bottom: 20px;
@@ -1728,20 +1728,20 @@
transition: background 0.15s ease, box-shadow 0.15s ease;
}
-.testing-container .step-row.clickable {
+.dispatch-container .step-row.clickable {
cursor: pointer;
}
-.testing-container .step-row.clickable:hover {
+.dispatch-container .step-row.clickable:hover {
background: rgba(99, 102, 241, 0.06);
}
-.testing-container .step-row.clickable:focus-visible {
+.dispatch-container .step-row.clickable:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
-.testing-container .step-row.active {
+.dispatch-container .step-row.active {
background: rgba(99, 102, 241, 0.1);
box-shadow: inset 3px 0 0 #6366f1;
padding-left: 8px;
@@ -1749,7 +1749,7 @@
}
-.testing-container .step-row:not(:last-child)::before {
+.dispatch-container .step-row:not(:last-child)::before {
content: '';
position: absolute;
left: 15px;
@@ -1759,7 +1759,7 @@
background: var(--border);
}
-.testing-container .step-dot {
+.dispatch-container .step-dot {
width: 32px;
height: 32px;
border-radius: 50%;
@@ -1772,28 +1772,28 @@
flex-shrink: 0;
}
-.testing-container .step-dot.kitchen {
+.dispatch-container .step-dot.kitchen {
background: var(--kitchen);
color: #fff;
}
-.testing-container .step-dot.delivery {
+.dispatch-container .step-dot.delivery {
background: #fff;
border: 2px solid var(--border);
color: var(--text-muted);
}
-.testing-container .step-label {
+.dispatch-container .step-label {
font-size: 15px;
font-weight: 700;
}
-.testing-container .step-label-row {
+.dispatch-container .step-label-row {
display: flex;
align-items: flex-start;
}
-.testing-container .step-customer {
+.dispatch-container .step-customer {
flex: 1;
min-width: 0;
word-break: break-word;
@@ -1801,17 +1801,17 @@
line-height: 1.35;
}
-.testing-container .kitchen-tag {
+.dispatch-container .kitchen-tag {
color: var(--kitchen);
}
-.testing-container .step-dest {
+.dispatch-container .step-dest {
font-size: 13px;
color: var(--text-muted);
margin-top: 3px;
}
-.testing-container .step-detail {
+.dispatch-container .step-detail {
display: flex;
flex-wrap: wrap;
gap: 10px;
@@ -1820,23 +1820,23 @@
font-weight: 600;
}
-.testing-container .step-profit {
+.dispatch-container .step-profit {
color: var(--success);
}
-.testing-container .step-profit.is-loss {
+.dispatch-container .step-profit.is-loss {
color: #dc2626;
}
/* Enriched step row metadata */
-.testing-container .step-location {
+.dispatch-container .step-location {
font-size: 11px;
color: var(--text-muted);
margin-top: 3px;
font-weight: 500;
}
-.testing-container .step-notes {
+.dispatch-container .step-notes {
font-size: 11px;
color: var(--text-muted);
margin-top: 3px;
@@ -1851,12 +1851,12 @@
max-width: 100%;
}
-.testing-container .step-charges {
+.dispatch-container .step-charges {
color: #0074e7;
font-weight: 600;
}
-.testing-container .step-type {
+.dispatch-container .step-type {
font-size: 10px;
font-weight: 800;
padding: 2px 7px;
@@ -1865,24 +1865,24 @@
letter-spacing: 0.04em;
}
-.testing-container .step-type.type-economy { background: rgba(34, 197, 94, 0.15); color: #16a34a; }
-.testing-container .step-type.type-risky { background: rgba(239, 68, 68, 0.15); color: #dc2626; }
-.testing-container .step-type.type-express { background: rgba(59, 130, 246, 0.15); color: #2563eb; }
-.testing-container .step-type:not(.type-economy):not(.type-risky):not(.type-express) {
+.dispatch-container .step-type.type-economy { background: rgba(34, 197, 94, 0.15); color: #16a34a; }
+.dispatch-container .step-type.type-risky { background: rgba(239, 68, 68, 0.15); color: #dc2626; }
+.dispatch-container .step-type.type-express { background: rgba(59, 130, 246, 0.15); color: #2563eb; }
+.dispatch-container .step-type:not(.type-economy):not(.type-risky):not(.type-express) {
background: rgba(100, 116, 139, 0.15);
color: #475569;
}
/* ── Zone card (in panel list) ──────────────────────────────── */
-.testing-container .rcard.zone-card {
+.dispatch-container .rcard.zone-card {
padding: 14px 14px 12px;
background: linear-gradient(180deg, #ffffff 0%, #fafbff 100%);
position: relative;
overflow: hidden;
}
-.testing-container .rcard.zone-card::before {
+.dispatch-container .rcard.zone-card::before {
content: '';
position: absolute;
top: 0;
@@ -1894,25 +1894,25 @@
transition: opacity 0.2s, width 0.2s;
}
-.testing-container .rcard.zone-card:hover {
+.dispatch-container .rcard.zone-card:hover {
border-color: rgba(59, 130, 246, 0.5);
background: linear-gradient(180deg, #ffffff 0%, #f0f7ff 100%);
box-shadow: 0 8px 22px rgba(59, 130, 246, 0.12);
}
-.testing-container .rcard.zone-card:hover::before {
+.dispatch-container .rcard.zone-card:hover::before {
opacity: 1;
width: 4px;
}
-.testing-container .zone-card-header {
+.dispatch-container .zone-card-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
-.testing-container .zone-card-emoji {
+.dispatch-container .zone-card-emoji {
width: 36px;
height: 36px;
border-radius: 10px;
@@ -1925,12 +1925,12 @@
flex-shrink: 0;
}
-.testing-container .zone-card-titles {
+.dispatch-container .zone-card-titles {
flex: 1;
min-width: 0;
}
-.testing-container .zone-card-name {
+.dispatch-container .zone-card-name {
font-size: 15px;
font-weight: 800;
color: var(--text);
@@ -1940,14 +1940,14 @@
text-overflow: ellipsis;
}
-.testing-container .zone-card-sub {
+.dispatch-container .zone-card-sub {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
font-weight: 500;
}
-.testing-container .zone-card-arrow {
+.dispatch-container .zone-card-arrow {
font-size: 18px;
font-weight: 800;
color: var(--accent);
@@ -1956,26 +1956,26 @@
flex-shrink: 0;
}
-.testing-container .rcard.zone-card:hover .zone-card-arrow {
+.dispatch-container .rcard.zone-card:hover .zone-card-arrow {
opacity: 1;
transform: translateX(4px);
}
/* Progress row: status bar + delivered/total counter */
-.testing-container .zone-progress-row {
+.dispatch-container .zone-progress-row {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0 4px;
}
-.testing-container .zone-progress-row .zone-status-bar {
+.dispatch-container .zone-progress-row .zone-status-bar {
flex: 1;
margin: 0;
height: 6px;
}
-.testing-container .zone-progress-label {
+.dispatch-container .zone-progress-label {
font-size: 10px;
font-weight: 800;
color: var(--text-muted);
@@ -1985,14 +1985,14 @@
}
/* Stat pills row */
-.testing-container .zone-stat-pills {
+.dispatch-container .zone-stat-pills {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 10px;
}
-.testing-container .zone-stat-pill {
+.dispatch-container .zone-stat-pill {
display: inline-flex;
align-items: center;
gap: 4px;
@@ -2005,41 +2005,41 @@
transition: all 0.15s;
}
-.testing-container .zone-stat-icon {
+.dispatch-container .zone-stat-icon {
font-size: 12px;
opacity: 0.85;
}
-.testing-container .zone-stat-value {
+.dispatch-container .zone-stat-value {
font-weight: 800;
color: var(--text);
font-variant-numeric: tabular-nums;
}
-.testing-container .zone-stat-label {
+.dispatch-container .zone-stat-label {
font-size: 10px;
color: var(--text-muted);
font-weight: 600;
}
-.testing-container .zone-stat-pill.profit-positive {
+.dispatch-container .zone-stat-pill.profit-positive {
background: rgba(34, 197, 94, 0.08);
border-color: rgba(34, 197, 94, 0.25);
}
-.testing-container .zone-stat-pill.profit-positive .zone-stat-value {
+.dispatch-container .zone-stat-pill.profit-positive .zone-stat-value {
color: var(--success);
}
-.testing-container .zone-stat-pill.profit-negative {
+.dispatch-container .zone-stat-pill.profit-negative {
background: rgba(239, 68, 68, 0.08);
border-color: rgba(239, 68, 68, 0.25);
}
-.testing-container .zone-stat-pill.profit-negative .zone-stat-value {
+.dispatch-container .zone-stat-pill.profit-negative .zone-stat-value {
color: #ef4444;
}
/* Suburb preview line at the bottom of the card */
-.testing-container .zone-card-suburbs {
+.dispatch-container .zone-card-suburbs {
display: flex;
align-items: center;
gap: 6px;
@@ -2051,7 +2051,7 @@
line-height: 1.4;
}
-.testing-container .zone-card-suburbs-text {
+.dispatch-container .zone-card-suburbs-text {
flex: 1;
min-width: 0;
overflow: hidden;
@@ -2060,7 +2060,7 @@
font-weight: 600;
}
-.testing-container .zone-card-suburbs-more {
+.dispatch-container .zone-card-suburbs-more {
flex-shrink: 0;
font-size: 10px;
font-weight: 800;
@@ -2072,7 +2072,7 @@
/* ── Status bar (proportional segments) ─────────────────────── */
-.testing-container .zone-status-bar {
+.dispatch-container .zone-status-bar {
display: flex;
height: 5px;
border-radius: 3px;
@@ -2082,12 +2082,12 @@
border: 1px solid var(--border);
}
-.testing-container .zone-status-bar.tall {
+.dispatch-container .zone-status-bar.tall {
height: 16px;
border-radius: 4px;
}
-.testing-container .zone-status-seg {
+.dispatch-container .zone-status-seg {
height: 100%;
display: flex;
align-items: center;
@@ -2096,22 +2096,22 @@
min-width: 1px;
}
-.testing-container .zone-status-bar.tall .zone-status-seg {
+.dispatch-container .zone-status-bar.tall .zone-status-seg {
font-size: 10px;
font-weight: 800;
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
-.testing-container .zone-status-seg:hover {
+.dispatch-container .zone-status-seg:hover {
filter: brightness(1.1);
}
-.testing-container .zone-status-seg-label {
+.dispatch-container .zone-status-seg-label {
padding: 0 4px;
}
-.testing-container .zone-status-legend {
+.dispatch-container .zone-status-legend {
display: flex;
flex-wrap: wrap;
gap: 10px;
@@ -2120,19 +2120,19 @@
color: var(--text-muted);
}
-.testing-container .legend-item {
+.dispatch-container .legend-item {
display: inline-flex;
align-items: center;
gap: 5px;
}
-.testing-container .legend-item strong {
+.dispatch-container .legend-item strong {
color: var(--text);
font-weight: 700;
margin-left: 2px;
}
-.testing-container .legend-dot {
+.dispatch-container .legend-dot {
width: 9px;
height: 9px;
border-radius: 50%;
@@ -2141,11 +2141,11 @@
/* ── Zone detail sections ───────────────────────────────────── */
-.testing-container .zone-detail-section {
+.dispatch-container .zone-detail-section {
margin: 18px 0;
}
-.testing-container .zone-section-label {
+.dispatch-container .zone-section-label {
font-size: 10px;
font-weight: 800;
color: var(--text-muted);
@@ -2157,27 +2157,27 @@
gap: 10px;
}
-.testing-container .zone-section-label::after {
+.dispatch-container .zone-section-label::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
-.testing-container .section-count {
+.dispatch-container .section-count {
color: var(--accent);
font-weight: 700;
}
/* ── Chips (suburbs + kitchens) ─────────────────────────────── */
-.testing-container .zone-chips {
+.dispatch-container .zone-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
-.testing-container .zone-chip {
+.dispatch-container .zone-chip {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -2191,26 +2191,26 @@
transition: all 0.15s;
}
-.testing-container .zone-chip:hover {
+.dispatch-container .zone-chip:hover {
border-color: var(--accent);
background: var(--accent-soft);
}
-.testing-container .zone-chip.kitchen {
+.dispatch-container .zone-chip.kitchen {
background: rgba(245, 158, 11, 0.06);
border-color: rgba(245, 158, 11, 0.25);
}
-.testing-container .zone-chip.kitchen:hover {
+.dispatch-container .zone-chip.kitchen:hover {
background: rgba(245, 158, 11, 0.12);
border-color: rgba(245, 158, 11, 0.5);
}
-.testing-container .zone-chip-name {
+.dispatch-container .zone-chip-name {
white-space: nowrap;
}
-.testing-container .zone-chip-count {
+.dispatch-container .zone-chip-count {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -2224,31 +2224,31 @@
font-weight: 800;
}
-.testing-container .zone-chip-count.kitchen {
+.dispatch-container .zone-chip-count.kitchen {
background: var(--kitchen);
}
/* Clickable area chip (button variant) — same look as the chip but with cursor +
stronger active state for the currently-expanded suburb. */
-.testing-container .zone-chip.zone-chip-clickable {
+.dispatch-container .zone-chip.zone-chip-clickable {
cursor: pointer;
font-family: inherit;
}
-.testing-container .zone-chip.zone-chip-clickable.active {
+.dispatch-container .zone-chip.zone-chip-clickable.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.25);
}
-.testing-container .zone-chip.zone-chip-clickable.active .zone-chip-count {
+.dispatch-container .zone-chip.zone-chip-clickable.active .zone-chip-count {
background: rgba(255, 255, 255, 0.25);
color: #fff;
}
/* Inline drill-down panel for a selected suburb */
-.testing-container .zone-suburb-panel {
+.dispatch-container .zone-suburb-panel {
margin-top: 12px;
border: 1px solid var(--border);
border-radius: 12px;
@@ -2262,7 +2262,7 @@
to { opacity: 1; transform: translateY(0); }
}
-.testing-container .zone-suburb-panel-head {
+.dispatch-container .zone-suburb-panel-head {
display: flex;
align-items: center;
justify-content: space-between;
@@ -2272,7 +2272,7 @@
border-bottom: 1px solid var(--border);
}
-.testing-container .zone-suburb-panel-title {
+.dispatch-container .zone-suburb-panel-title {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -2282,7 +2282,7 @@
letter-spacing: -0.01em;
}
-.testing-container .zone-suburb-panel-count {
+.dispatch-container .zone-suburb-panel-count {
margin-left: 6px;
padding: 2px 8px;
border-radius: 999px;
@@ -2293,7 +2293,7 @@
letter-spacing: 0.04em;
}
-.testing-container .zone-suburb-panel-close {
+.dispatch-container .zone-suburb-panel-close {
width: 24px;
height: 24px;
border-radius: 50%;
@@ -2307,12 +2307,12 @@
transition: all 0.15s;
}
-.testing-container .zone-suburb-panel-close:hover {
+.dispatch-container .zone-suburb-panel-close:hover {
background: rgba(239, 68, 68, 0.15);
color: #dc2626;
}
-.testing-container .zone-suburb-panel-empty {
+.dispatch-container .zone-suburb-panel-empty {
padding: 16px;
font-size: 12px;
color: var(--text-muted);
@@ -2324,14 +2324,14 @@
tickets rather than a wall of text. All accents use the console's base
purple (#7b1fa2) so the page sits inside the existing design language. */
-.testing-container .zone-order-grid {
+.dispatch-container .zone-order-grid {
display: flex;
flex-direction: column;
gap: 10px;
padding: 4px 2px 12px;
}
-.testing-container .zone-order-card {
+.dispatch-container .zone-order-card {
position: relative;
background: #ffffff;
border: 1px solid rgba(123, 31, 162, 0.14);
@@ -2342,7 +2342,7 @@
overflow: hidden;
}
-.testing-container .zone-order-card::before {
+.dispatch-container .zone-order-card::before {
content: '';
position: absolute;
top: 0;
@@ -2354,28 +2354,28 @@
transition: width 0.18s ease, opacity 0.18s ease;
}
-.testing-container .zone-order-card.clickable {
+.dispatch-container .zone-order-card.clickable {
cursor: pointer;
}
-.testing-container .zone-order-card.clickable:hover {
+.dispatch-container .zone-order-card.clickable:hover {
transform: translateY(-1px);
box-shadow: 0 8px 20px rgba(123, 31, 162, 0.16);
border-color: rgba(123, 31, 162, 0.45);
}
-.testing-container .zone-order-card.clickable:hover::before {
+.dispatch-container .zone-order-card.clickable:hover::before {
width: 5px;
opacity: 1;
}
-.testing-container .zone-order-card.active {
+.dispatch-container .zone-order-card.active {
border-color: #7b1fa2;
box-shadow: 0 10px 24px rgba(123, 31, 162, 0.22);
background: linear-gradient(180deg, #ffffff 0%, #f7eaff 100%);
}
-.testing-container .zone-order-card.active::before {
+.dispatch-container .zone-order-card.active::before {
width: 6px;
opacity: 1;
}
@@ -2383,33 +2383,33 @@
/* `going-on` = this is the rider's currently-active stop. Overrides the
purple accent with a green rail + soft green wash so the operator can
pick the in-progress card out of the trip at a glance. */
-.testing-container .zone-order-card.going-on {
+.dispatch-container .zone-order-card.going-on {
background: linear-gradient(180deg, #ffffff 0%, #ecfdf5 100%);
border-color: rgba(34, 197, 94, 0.4);
box-shadow: 0 6px 18px rgba(34, 197, 94, 0.15);
}
-.testing-container .zone-order-card.going-on::before {
+.dispatch-container .zone-order-card.going-on::before {
background: #16a34a;
opacity: 1;
}
/* When the in-progress card is the focused stop, keep the green priority
signal but tint a hair stronger so the click state is still felt. */
-.testing-container .zone-order-card.going-on.active {
+.dispatch-container .zone-order-card.going-on.active {
background: linear-gradient(180deg, #ffffff 0%, #d1fae5 100%);
border-color: #16a34a;
}
/* Header: number badge + order id/rider + status pill */
-.testing-container .zone-order-card-head {
+.dispatch-container .zone-order-card-head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
-.testing-container .zone-order-num {
+.dispatch-container .zone-order-num {
width: 28px;
height: 28px;
border-radius: 8px;
@@ -2425,12 +2425,12 @@
box-shadow: 0 2px 6px rgba(123, 31, 162, 0.22);
}
-.testing-container .zone-order-id-block {
+.dispatch-container .zone-order-id-block {
flex: 1;
min-width: 0;
}
-.testing-container .zone-order-id {
+.dispatch-container .zone-order-id {
font-size: 12px;
font-weight: 700;
color: var(--text);
@@ -2442,7 +2442,7 @@
/* Icon flows inline with the rider name — keeps them visually glued
together instead of split across a flex gap. */
-.testing-container .zone-order-rider {
+.dispatch-container .zone-order-rider {
font-size: 11px;
font-weight: 600;
color: #7b1fa2;
@@ -2452,7 +2452,7 @@
text-overflow: ellipsis;
}
-.testing-container .zone-order-status {
+.dispatch-container .zone-order-status {
font-size: 10px;
font-weight: 700;
padding: 3px 8px;
@@ -2464,7 +2464,7 @@
/* Right-side header cluster: status pill on top, delivery time below it
so the operator reads the outcome and the wall-clock together. */
-.testing-container .zone-order-status-stack {
+.dispatch-container .zone-order-status-stack {
display: flex;
flex-direction: column;
align-items: flex-end;
@@ -2472,7 +2472,7 @@
flex-shrink: 0;
}
-.testing-container .zone-order-time {
+.dispatch-container .zone-order-time {
display: inline-flex;
align-items: center;
gap: 4px;
@@ -2484,7 +2484,7 @@
white-space: nowrap;
}
-.testing-container .zone-order-time svg {
+.dispatch-container .zone-order-time svg {
font-size: 14px;
color: #7b1fa2;
flex-shrink: 0;
@@ -2492,18 +2492,18 @@
/* Expected (not yet delivered) — muted color + dashed icon tint so the
operator can tell at a glance which orders are still in flight. */
-.testing-container .zone-order-time.is-expected {
+.dispatch-container .zone-order-time.is-expected {
color: #64748b;
font-weight: 600;
}
-.testing-container .zone-order-time.is-expected svg {
+.dispatch-container .zone-order-time.is-expected svg {
color: #94a3b8;
}
/* Customer line — visually the most important text in the card. Inline
icon flows with text so there's no awkward gap on short names. */
-.testing-container .zone-order-customer {
+.dispatch-container .zone-order-customer {
font-size: 14px;
font-weight: 700;
color: var(--text);
@@ -2517,7 +2517,7 @@
/* Generic icon + text row for kitchen / address / notes. Uses -webkit-box
so the icon stays inline at the very start of the text and any wrap goes
under it (not back to the left edge), keeping the visual block tight. */
-.testing-container .zone-order-line {
+.dispatch-container .zone-order-line {
font-size: 12px;
color: var(--text-muted);
line-height: 1.4;
@@ -2530,7 +2530,7 @@
text-overflow: ellipsis;
}
-.testing-container .zone-order-notes {
+.dispatch-container .zone-order-notes {
font-style: italic;
color: #475569;
/* Notes can be longer; let them breathe over 2 lines and override the
@@ -2543,7 +2543,7 @@
}
/* Footer stat chips */
-.testing-container .zone-order-stats {
+.dispatch-container .zone-order-stats {
display: flex;
flex-wrap: wrap;
gap: 6px;
@@ -2552,7 +2552,7 @@
border-top: 1px dashed rgba(123, 31, 162, 0.14);
}
-.testing-container .zone-order-chip {
+.dispatch-container .zone-order-chip {
display: inline-flex;
align-items: center;
gap: 4px;
@@ -2565,30 +2565,30 @@
color: var(--text);
}
-.testing-container .zone-order-chip.is-profit {
+.dispatch-container .zone-order-chip.is-profit {
background: rgba(34, 197, 94, 0.1);
border-color: rgba(34, 197, 94, 0.25);
color: #16a34a;
}
-.testing-container .zone-order-chip.is-loss {
+.dispatch-container .zone-order-chip.is-loss {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.25);
color: #dc2626;
}
-.testing-container .zone-order-chip.zone-order-trip {
+.dispatch-container .zone-order-chip.zone-order-trip {
background: rgba(123, 31, 162, 0.1);
border-color: rgba(123, 31, 162, 0.25);
color: #7b1fa2;
}
-.testing-container .zone-order-type {
+.dispatch-container .zone-order-type {
text-transform: uppercase;
letter-spacing: 0.05em;
}
-.testing-container .kitchen-transition {
+.dispatch-container .kitchen-transition {
padding: 12px;
background: var(--kitchen-soft);
border: 1px dashed var(--kitchen);
@@ -2600,13 +2600,13 @@
/* When the kitchen switch marker sits between two zone-order-cards, drop the
step-row indent and let it span the full card width. */
-.testing-container .zone-order-grid .kitchen-transition {
+.dispatch-container .zone-order-grid .kitchen-transition {
margin: 2px 0;
padding: 8px 12px;
}
/* Map */
-.testing-container #map-wrap {
+.dispatch-container #map-wrap {
flex: 1;
position: relative;
transition: flex 0.32s cubic-bezier(0.4, 0, 0.2, 1);
@@ -2616,7 +2616,7 @@
actual-tracks pane (#compare-map-wrap) can sit beside it. A real gutter
between the two maps (margin-right) replaces the old hairline border so
the two halves read as clearly separate panels. */
-.testing-container #map-wrap.compare-split {
+.dispatch-container #map-wrap.compare-split {
flex: 1 1 calc(50% - 8px);
min-width: 0;
margin-right: 16px;
@@ -2627,7 +2627,7 @@
}
/* "Planned Route" badge floating on the left map when compare is open */
-.testing-container .compare-planned-label {
+.dispatch-container .compare-planned-label {
position: absolute;
top: 12px;
left: 12px;
@@ -2650,7 +2650,7 @@
animation: compare-label-in 0.22s ease-out;
}
-.testing-container .compare-planned-dot {
+.dispatch-container .compare-planned-dot {
width: 7px;
height: 7px;
border-radius: 50%;
@@ -2658,13 +2658,54 @@
flex-shrink: 0;
}
+/* "Actual GPS" badge — mirror of the planned label, lives on the actual
+ map (top half in compare mode). Same shape and animation, but tinted
+ cyan/sky to read as "live data" rather than "plan". */
+.dispatch-container .compare-actual-label {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ z-index: 1000;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 12px 5px 9px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(8px);
+ border: 1px solid rgba(14, 165, 233, 0.35);
+ box-shadow: 0 4px 14px rgba(15, 23, 42, 0.1);
+ font-size: 10px;
+ font-weight: 800;
+ color: #0369a1;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ pointer-events: none;
+ animation: compare-label-in 0.22s ease-out;
+}
+
+.dispatch-container .compare-actual-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #0ea5e9, #0369a1);
+ flex-shrink: 0;
+ box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.15);
+ animation: compare-actual-pulse 1.6s ease-in-out infinite;
+}
+
+@keyframes compare-actual-pulse {
+ 0%, 100% { box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.15); }
+ 50% { box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.05); }
+}
+
@keyframes compare-label-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Right half of Compare mode */
-.testing-container #compare-map-wrap {
+.dispatch-container #compare-map-wrap {
flex: 1 1 calc(50% - 8px);
min-width: 0;
position: relative;
@@ -2683,7 +2724,7 @@
}
-.testing-container #compare-map-wrap .leaflet-container {
+.dispatch-container #compare-map-wrap .leaflet-container {
flex: 1;
min-height: 0;
background: #f0f4f8 !important;
@@ -2693,7 +2734,7 @@
bottom-right Animate Routes overlay (mirrors #ov-br on the main map). The
delta panel sits outside this wrapper so the overlay can't accidentally
land on top of the stats cards. */
-.testing-container .compare-map-area {
+.dispatch-container .compare-map-area {
flex: 1;
min-height: 0;
position: relative;
@@ -2701,7 +2742,7 @@
flex-direction: column;
}
-.testing-container .compare-ov-br {
+.dispatch-container .compare-ov-br {
position: absolute;
right: 12px;
bottom: 12px;
@@ -2711,14 +2752,14 @@
}
/* Compare header */
-.testing-container .compare-header {
+.dispatch-container .compare-header {
padding: 10px 14px 8px;
border-bottom: 1px solid var(--border, rgba(15, 23, 42, 0.08));
background: linear-gradient(180deg, #fff 0%, #f8fafc 100%);
flex-shrink: 0;
}
-.testing-container .compare-header-top {
+.dispatch-container .compare-header-top {
display: flex;
align-items: center;
justify-content: space-between;
@@ -2726,7 +2767,7 @@
margin-bottom: 7px;
}
-.testing-container .compare-title {
+.dispatch-container .compare-title {
display: inline-flex;
align-items: center;
gap: 7px;
@@ -2737,7 +2778,7 @@
min-width: 0;
}
-.testing-container .compare-title-dot {
+.dispatch-container .compare-title-dot {
width: 12px;
height: 12px;
border-radius: 50%;
@@ -2745,13 +2786,13 @@
flex-shrink: 0;
}
-.testing-container .compare-title-name {
+.dispatch-container .compare-title-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
-.testing-container .compare-title-badge {
+.dispatch-container .compare-title-badge {
display: inline-flex;
align-items: center;
padding: 3px 9px;
@@ -2767,13 +2808,13 @@
}
/* Track-load progress bar */
-.testing-container .compare-progress {
+.dispatch-container .compare-progress {
display: flex;
align-items: center;
gap: 8px;
}
-.testing-container .compare-progress-bar-wrap {
+.dispatch-container .compare-progress-bar-wrap {
flex: 1;
height: 3px;
background: rgba(15, 23, 42, 0.07);
@@ -2781,18 +2822,18 @@
overflow: hidden;
}
-.testing-container .compare-progress-bar-fill {
+.dispatch-container .compare-progress-bar-fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, #0ea5e9, #6366f1);
transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
-.testing-container .compare-progress-bar-fill.is-done {
+.dispatch-container .compare-progress-bar-fill.is-done {
background: linear-gradient(90deg, #22c55e, #16a34a);
}
-.testing-container .compare-progress-text {
+.dispatch-container .compare-progress-text {
font-size: 12px;
font-weight: 700;
color: #94a3b8;
@@ -2802,7 +2843,7 @@
}
/* Per-delivery track legend — sits between map and stats card */
-.testing-container .compare-track-legend {
+.dispatch-container .compare-track-legend {
flex-shrink: 0;
max-height: 150px;
overflow-y: auto;
@@ -2811,16 +2852,16 @@
scrollbar-color: rgba(100, 116, 139, 0.25) transparent;
}
-.testing-container .compare-track-legend::-webkit-scrollbar {
+.dispatch-container .compare-track-legend::-webkit-scrollbar {
width: 4px;
}
-.testing-container .compare-track-legend::-webkit-scrollbar-thumb {
+.dispatch-container .compare-track-legend::-webkit-scrollbar-thumb {
background: rgba(100, 116, 139, 0.25);
border-radius: 999px;
}
-.testing-container .compare-track-item {
+.dispatch-container .compare-track-item {
display: flex;
align-items: center;
gap: 9px;
@@ -2829,15 +2870,15 @@
transition: background 0.12s;
}
-.testing-container .compare-track-item:last-child {
+.dispatch-container .compare-track-item:last-child {
border-bottom: 0;
}
-.testing-container .compare-track-item:hover {
+.dispatch-container .compare-track-item:hover {
background: rgba(15, 23, 42, 0.025);
}
-.testing-container .compare-track-num {
+.dispatch-container .compare-track-num {
width: 20px;
height: 20px;
border-radius: 50%;
@@ -2851,12 +2892,12 @@
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
-.testing-container .compare-track-info {
+.dispatch-container .compare-track-info {
flex: 1;
min-width: 0;
}
-.testing-container .compare-track-customer {
+.dispatch-container .compare-track-customer {
font-size: 11px;
font-weight: 700;
color: #1e293b;
@@ -2865,14 +2906,14 @@
text-overflow: ellipsis;
}
-.testing-container .compare-track-meta {
+.dispatch-container .compare-track-meta {
font-size: 10px;
color: #94a3b8;
font-weight: 600;
margin-top: 1px;
}
-.testing-container .compare-track-right {
+.dispatch-container .compare-track-right {
display: flex;
flex-direction: column;
align-items: flex-end;
@@ -2880,7 +2921,7 @@
flex-shrink: 0;
}
-.testing-container .compare-track-status {
+.dispatch-container .compare-track-status {
display: inline-flex;
align-items: center;
padding: 1px 6px;
@@ -2891,13 +2932,13 @@
text-transform: uppercase;
}
-.testing-container .compare-track-kms {
+.dispatch-container .compare-track-kms {
font-size: 10px;
font-weight: 700;
color: #64748b;
}
-.testing-container .compare-track-no-data {
+.dispatch-container .compare-track-no-data {
display: inline-flex;
align-items: center;
gap: 4px;
@@ -2906,7 +2947,7 @@
color: #cbd5e1;
}
-.testing-container .compare-track-spinner {
+.dispatch-container .compare-track-spinner {
width: 9px;
height: 9px;
border-radius: 50%;
@@ -2921,28 +2962,28 @@
}
/* Summary stats card at the bottom of the Compare pane */
-.testing-container .compare-overall-card {
+.dispatch-container .compare-overall-card {
background: linear-gradient(180deg, #fff 0%, #f8fafc 100%);
border-top: 1px solid var(--border, rgba(15, 23, 42, 0.07));
padding: 10px 14px;
flex-shrink: 0;
}
-.testing-container .compare-overall-head {
+.dispatch-container .compare-overall-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 9px;
}
-.testing-container .compare-overall-dot {
+.dispatch-container .compare-overall-dot {
width: 11px;
height: 11px;
border-radius: 50%;
flex-shrink: 0;
}
-.testing-container .compare-overall-name {
+.dispatch-container .compare-overall-name {
font-size: 12px;
font-weight: 800;
color: #0f172a;
@@ -2953,7 +2994,7 @@
text-overflow: ellipsis;
}
-.testing-container .compare-overall-rate {
+.dispatch-container .compare-overall-rate {
display: inline-flex;
align-items: center;
padding: 2px 8px;
@@ -2968,25 +3009,25 @@
flex-shrink: 0;
}
-.testing-container .compare-overall-rate.is-partial {
+.dispatch-container .compare-overall-rate.is-partial {
background: rgba(245, 158, 11, 0.1);
border-color: rgba(245, 158, 11, 0.25);
color: #b45309;
}
-.testing-container .compare-overall-rate.is-zero {
+.dispatch-container .compare-overall-rate.is-zero {
background: rgba(100, 116, 139, 0.08);
border-color: rgba(100, 116, 139, 0.2);
color: #64748b;
}
-.testing-container .compare-overall-stats {
+.dispatch-container .compare-overall-stats {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 6px;
}
-.testing-container .compare-overall-stat {
+.dispatch-container .compare-overall-stat {
background: rgba(15, 23, 42, 0.03);
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 10px;
@@ -2995,12 +3036,12 @@
transition: background 0.15s, transform 0.15s;
}
-.testing-container .compare-overall-stat:hover {
+.dispatch-container .compare-overall-stat:hover {
background: rgba(15, 23, 42, 0.055);
transform: translateY(-1px);
}
-.testing-container .compare-overall-stat-icon {
+.dispatch-container .compare-overall-stat-icon {
font-size: 13px;
line-height: 1;
margin-bottom: 3px;
@@ -3010,21 +3051,21 @@
justify-content: center;
}
-.testing-container .compare-overall-stat-value {
+.dispatch-container .compare-overall-stat-value {
font-size: 14px;
font-weight: 800;
color: #0f172a;
line-height: 1.1;
}
-.testing-container .compare-overall-stat-unit {
+.dispatch-container .compare-overall-stat-unit {
font-size: 9px;
font-weight: 700;
color: #94a3b8;
margin-left: 2px;
}
-.testing-container .compare-overall-stat-label {
+.dispatch-container .compare-overall-stat-label {
font-size: 9px;
font-weight: 700;
color: #94a3b8;
@@ -3033,21 +3074,21 @@
margin-top: 2px;
}
-.testing-container .compare-overall-stat.is-profit {
+.dispatch-container .compare-overall-stat.is-profit {
background: rgba(34, 197, 94, 0.06);
border-color: rgba(34, 197, 94, 0.16);
}
-.testing-container .compare-overall-stat.is-profit .compare-overall-stat-value { color: #16a34a; }
-.testing-container .compare-overall-stat.is-profit .compare-overall-stat-icon { color: #22c55e; }
+.dispatch-container .compare-overall-stat.is-profit .compare-overall-stat-value { color: #16a34a; }
+.dispatch-container .compare-overall-stat.is-profit .compare-overall-stat-icon { color: #22c55e; }
-.testing-container .compare-overall-stat.is-loss {
+.dispatch-container .compare-overall-stat.is-loss {
background: rgba(239, 68, 68, 0.06);
border-color: rgba(239, 68, 68, 0.16);
}
-.testing-container .compare-overall-stat.is-loss .compare-overall-stat-value { color: #dc2626; }
-.testing-container .compare-overall-stat.is-loss .compare-overall-stat-icon { color: #ef4444; }
+.dispatch-container .compare-overall-stat.is-loss .compare-overall-stat-value { color: #dc2626; }
+.dispatch-container .compare-overall-stat.is-loss .compare-overall-stat-icon { color: #ef4444; }
/* ── Compare UI v2 ──────────────────────────────────────────────
Redesigned compare pane: rich header (title + sync toggle + step
@@ -3055,7 +3096,7 @@
planned-vs-actual per focused step or rolled up across the day.
─────────────────────────────────────────────────────────────────── */
-.testing-container .compare-header-v2 {
+.dispatch-container .compare-header-v2 {
padding: 12px 14px 10px;
border-bottom: 1px solid var(--border, rgba(15, 23, 42, 0.08));
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
@@ -3065,14 +3106,14 @@
gap: 10px;
}
-.testing-container .compare-header-row {
+.dispatch-container .compare-header-row {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
-.testing-container .compare-header-row .compare-title {
+.dispatch-container .compare-header-row .compare-title {
flex: 1;
display: inline-flex;
align-items: center;
@@ -3083,14 +3124,14 @@
min-width: 0;
}
-.testing-container .compare-header-tools {
+.dispatch-container .compare-header-tools {
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
-.testing-container .compare-overall-btn {
+.dispatch-container .compare-overall-btn {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -3107,7 +3148,7 @@
transition: all 0.18s ease;
}
-.testing-container .compare-overall-btn:hover {
+.dispatch-container .compare-overall-btn:hover {
background: linear-gradient(135deg, #6366f1, #3b82f6);
border-color: #6366f1;
color: #fff;
@@ -3115,11 +3156,11 @@
transform: translateY(-1px);
}
-.testing-container .compare-overall-btn svg {
+.dispatch-container .compare-overall-btn svg {
font-size: 15px;
}
-.testing-container .compare-sync-toggle {
+.dispatch-container .compare-sync-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -3136,59 +3177,145 @@
transition: all 0.18s ease;
}
-.testing-container .compare-sync-toggle:hover {
+.dispatch-container .compare-sync-toggle:hover {
border-color: rgba(99, 102, 241, 0.4);
color: #4338ca;
}
-.testing-container .compare-sync-toggle.is-on {
+.dispatch-container .compare-sync-toggle.is-on {
background: linear-gradient(135deg, #22c55e, #16a34a);
border-color: #16a34a;
color: #fff;
box-shadow: 0 4px 10px rgba(34, 197, 94, 0.22);
}
-.testing-container .compare-sync-toggle svg {
+.dispatch-container .compare-sync-toggle svg {
font-size: 15px;
}
+/* Chevron toggle that collapses/expands the planned/actual timeline below
+ the compare header. Sits to the right of the Sync button. Defaults to
+ "open" (chevron points down); rotates 180° when collapsed. */
+.dispatch-container .compare-timeline-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ border-radius: 8px;
+ border: 1px solid var(--border, rgba(15, 23, 42, 0.12));
+ background: #fff;
+ color: #64748b;
+ cursor: pointer;
+ transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
+}
+.dispatch-container .compare-timeline-toggle:hover {
+ border-color: rgba(99, 102, 241, 0.4);
+ color: #4338ca;
+ background: rgba(99, 102, 241, 0.06);
+}
+.dispatch-container .compare-timeline-toggle svg {
+ font-size: 18px;
+ transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+}
+.dispatch-container .compare-timeline-toggle.is-open svg {
+ transform: rotate(180deg);
+ color: #4338ca;
+}
+
/* Step timeline — a horizontally scrollable row of step dots. Each step
is a button so it's keyboard-focusable + screen-reader friendly. */
-.testing-container .compare-timeline-wrap {
+.dispatch-container .compare-timeline-wrap {
display: flex;
flex-direction: column;
gap: 6px;
}
-.testing-container .compare-timeline {
+.dispatch-container .compare-timeline-container {
display: flex;
- align-items: center;
- gap: 0;
- padding: 2px 2px 4px;
- overflow-x: auto;
- scrollbar-width: thin;
- scrollbar-color: rgba(100, 116, 139, 0.25) transparent;
+ align-items: stretch;
+ background: rgba(15, 23, 42, 0.02);
+ border: 1px solid rgba(15, 23, 42, 0.06);
+ border-radius: 12px;
+ padding: 10px 14px;
+ gap: 16px;
+ box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.02);
}
-.testing-container .compare-timeline::-webkit-scrollbar { height: 4px; }
-.testing-container .compare-timeline::-webkit-scrollbar-thumb {
+.dispatch-container .compare-timeline-labels {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding-right: 14px;
+ border-right: 1.5px dashed rgba(15, 23, 42, 0.08);
+ flex-shrink: 0;
+ gap: 16px;
+}
+
+.dispatch-container .compare-timeline-label {
+ font-size: 11px;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: #64748b;
+ display: flex;
+ align-items: center;
+ height: 32px; /* aligns with circle height */
+}
+
+.dispatch-container .compare-timeline-scrollable {
+ flex: 1;
+ overflow-x: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ scrollbar-width: thin;
+ scrollbar-color: rgba(100, 116, 139, 0.2) transparent;
+ padding-bottom: 2px;
+}
+
+.dispatch-container .compare-timeline-scrollable::-webkit-scrollbar {
+ height: 4px;
+}
+
+.dispatch-container .compare-timeline-scrollable::-webkit-scrollbar-thumb {
background: rgba(100, 116, 139, 0.25);
border-radius: 999px;
}
-.testing-container .compare-step-spacer {
+.dispatch-container .compare-timeline-track {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ position: relative;
+}
+
+/* Planned track overrides to align vertically centered since there are no ticks */
+.dispatch-container .compare-timeline-track.is-planned .compare-step {
+ gap: 0;
+ padding: 0;
+ height: 32px;
+}
+
+.dispatch-container .compare-timeline-track.is-planned .compare-step-spacer {
+ margin-bottom: 0;
+ align-self: center;
+}
+
+/* Actual track overrides for the spacer alignment */
+.dispatch-container .compare-timeline-track.is-actual .compare-step-spacer {
+ margin-bottom: 22px; /* Centers spacer dynamically relative to the 32px circle */
+}
+
+.dispatch-container .compare-step-spacer {
width: 16px;
height: 2px;
background: linear-gradient(90deg, rgba(148, 163, 184, 0), rgba(148, 163, 184, 0.55) 30%, rgba(148, 163, 184, 0.55) 70%, rgba(148, 163, 184, 0));
flex-shrink: 0;
- /* Pushes the spacer line up so it visually centers on the circle row
- (the step is a column with circle on top + tick below; align-items:
- center on the parent would otherwise center the dash between them).
- Tuned for circle 32px + gap 11 + tick ~13. */
- margin-bottom: 24px;
}
-.testing-container .compare-step {
+.dispatch-container .compare-step {
display: inline-flex;
flex-direction: column;
align-items: center;
@@ -3205,11 +3332,11 @@
transition: transform 0.18s ease;
}
-.testing-container .compare-step:hover {
+.dispatch-container .compare-step:hover {
transform: translateY(-1px);
}
-.testing-container .compare-step-circle {
+.dispatch-container .compare-step-circle {
width: 32px;
height: 32px;
border-radius: 50%;
@@ -3224,7 +3351,7 @@
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
-.testing-container .compare-step:hover .compare-step-circle {
+.dispatch-container .compare-step:hover .compare-step-circle {
transform: scale(1.08);
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.6);
}
@@ -3233,7 +3360,7 @@
own color so it visually matches the polyline / drop pin on the map. The
glow is intentionally contained (low blur + low spread) so the tick label
below stays readable; a wider halo would bleed through the time text. */
-.testing-container .compare-step.is-focused .compare-step-circle {
+.dispatch-container .compare-step.is-focused .compare-step-circle {
transform: scale(1.18);
box-shadow:
0 4px 10px rgba(15, 23, 42, 0.22),
@@ -3242,30 +3369,30 @@
}
/* Step number stays crisp on the focused circle */
-.testing-container .compare-step.is-focused .compare-step-tick {
+.dispatch-container .compare-step.is-focused .compare-step-tick {
color: var(--step-color, #4338ca);
font-weight: 800;
}
-.testing-container .compare-step.is-pending .compare-step-circle {
+.dispatch-container .compare-step.is-pending .compare-step-circle {
background: #fff;
border: 2px solid var(--step-color, #cbd5e1);
color: var(--step-color, #94a3b8);
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
}
-.testing-container .compare-step.is-skipped .compare-step-circle {
+.dispatch-container .compare-step.is-skipped .compare-step-circle {
opacity: 0.42;
background: #cbd5e1;
}
-.testing-container .compare-step.is-loading .compare-step-circle {
+.dispatch-container .compare-step.is-loading .compare-step-circle {
background: #fff;
border: 2px solid rgba(99, 102, 241, 0.45);
color: transparent;
}
-.testing-container .compare-step.is-no-data .compare-step-circle {
+.dispatch-container .compare-step.is-no-data .compare-step-circle {
background: repeating-linear-gradient(
45deg,
#e2e8f0 0 4px,
@@ -3274,7 +3401,7 @@
color: #94a3b8;
}
-.testing-container .compare-step-spin {
+.dispatch-container .compare-step-spin {
width: 14px;
height: 14px;
border-radius: 50%;
@@ -3287,7 +3414,7 @@
to { transform: rotate(360deg); }
}
-.testing-container .compare-step-tick {
+.dispatch-container .compare-step-tick {
font-size: 11px;
font-weight: 700;
color: #64748b;
@@ -3298,7 +3425,7 @@
/* (focused-tick styling consolidated above with the step color) */
-.testing-container .compare-step-flag {
+.dispatch-container .compare-step-flag {
position: absolute;
top: -2px;
right: -2px;
@@ -3312,14 +3439,14 @@
/* Progress strip — sits under the timeline, reuses the existing progress
bar children styles. Same role as the old compare-progress block. */
-.testing-container .compare-progress-strip {
+.dispatch-container .compare-progress-strip {
display: flex;
align-items: center;
gap: 8px;
}
/* Legend strip — horizontal row of swatches identifying line styles. */
-.testing-container .compare-legend {
+.dispatch-container .compare-legend {
display: flex;
align-items: center;
gap: 14px;
@@ -3328,7 +3455,7 @@
border-top: 1px dashed rgba(15, 23, 42, 0.06);
}
-.testing-container .compare-legend-item {
+.dispatch-container .compare-legend-item {
display: inline-flex;
align-items: center;
gap: 7px;
@@ -3338,14 +3465,14 @@
letter-spacing: 0.02em;
}
-.testing-container .compare-legend-swatch {
+.dispatch-container .compare-legend-swatch {
width: 22px;
height: 4px;
border-radius: 2px;
flex-shrink: 0;
}
-.testing-container .compare-legend-swatch.is-planned {
+.dispatch-container .compare-legend-swatch.is-planned {
background: repeating-linear-gradient(
90deg,
#6366f1 0 5px,
@@ -3354,7 +3481,7 @@
height: 3px;
}
-.testing-container .compare-legend-swatch.is-actual {
+.dispatch-container .compare-legend-swatch.is-actual {
background: linear-gradient(90deg, currentColor, currentColor);
height: 4px;
}
@@ -3362,7 +3489,7 @@
/* Solid step-color swatch used by both "Planned (left)" and "Actual GPS
(right)" entries — they now share the same per-step palette so the same
color appears on both maps for the same delivery. */
-.testing-container .compare-legend-swatch.is-step-color {
+.dispatch-container .compare-legend-swatch.is-step-color {
height: 4px;
border-radius: 2px;
}
@@ -3370,7 +3497,7 @@
/* Small status note in the legend strip — replaces the now-removed
"Transit (no GPS)" item with a one-liner telling the operator how the
actual-GPS polyline gets built (Kalman smooth + OSRM road-snap). */
-.testing-container .compare-legend-note {
+.dispatch-container .compare-legend-note {
margin-left: auto;
font-size: 11px;
font-weight: 700;
@@ -3382,7 +3509,7 @@
text-overflow: ellipsis;
}
-.testing-container .compare-legend-swatch.is-transit {
+.dispatch-container .compare-legend-swatch.is-transit {
background: repeating-linear-gradient(
90deg,
#94a3b8 0 3px,
@@ -3393,7 +3520,7 @@
/* Delta panel — sits below the actual-GPS map. Per-step view when a step
is focused, day-summary view otherwise. */
-.testing-container .compare-delta {
+.dispatch-container .compare-delta {
padding: 12px 14px 14px;
border-top: 1px solid var(--border, rgba(15, 23, 42, 0.07));
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
@@ -3404,7 +3531,7 @@
animation: compare-delta-in 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
-.testing-container .compare-delta.is-anomaly {
+.dispatch-container .compare-delta.is-anomaly {
background: linear-gradient(180deg, #fff 0%, #fef2f2 100%);
border-top-color: rgba(220, 38, 38, 0.25);
}
@@ -3414,14 +3541,14 @@
to { opacity: 1; transform: translateY(0); }
}
-.testing-container .compare-delta-title {
+.dispatch-container .compare-delta-title {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
-.testing-container .compare-delta-step-badge {
+.dispatch-container .compare-delta-step-badge {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -3436,16 +3563,16 @@
box-shadow: 0 3px 10px rgba(15, 23, 42, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.5);
}
-.testing-container .compare-delta-step-badge svg {
+.dispatch-container .compare-delta-step-badge svg {
font-size: 17px;
}
-.testing-container .compare-delta-title-text {
+.dispatch-container .compare-delta-title-text {
flex: 1;
min-width: 0;
}
-.testing-container .compare-delta-title-main {
+.dispatch-container .compare-delta-title-main {
font-size: 16px;
font-weight: 800;
color: #0f172a;
@@ -3455,7 +3582,7 @@
line-height: 1.25;
}
-.testing-container .compare-delta-title-sub {
+.dispatch-container .compare-delta-title-sub {
font-size: 13px;
font-weight: 600;
color: #94a3b8;
@@ -3465,7 +3592,7 @@
text-overflow: ellipsis;
}
-.testing-container .compare-delta-status {
+.dispatch-container .compare-delta-status {
display: inline-flex;
align-items: center;
padding: 4px 11px;
@@ -3477,13 +3604,13 @@
flex-shrink: 0;
}
-.testing-container .compare-delta-grid {
+.dispatch-container .compare-delta-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
-.testing-container .compare-delta-cell {
+.dispatch-container .compare-delta-cell {
display: flex;
flex-direction: column;
gap: 4px;
@@ -3494,18 +3621,18 @@
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
-.testing-container .compare-delta-cell:hover {
+.dispatch-container .compare-delta-cell:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06);
}
-.testing-container .compare-delta-cell.is-anomaly {
+.dispatch-container .compare-delta-cell.is-anomaly {
border-color: rgba(220, 38, 38, 0.42);
background: linear-gradient(180deg, #fff, #fef2f2);
box-shadow: 0 0 0 1px rgba(220, 38, 38, 0.18) inset;
}
-.testing-container .compare-delta-cell-label {
+.dispatch-container .compare-delta-cell-label {
font-size: 11px;
font-weight: 800;
color: #94a3b8;
@@ -3513,7 +3640,7 @@
text-transform: uppercase;
}
-.testing-container .compare-delta-cell-val {
+.dispatch-container .compare-delta-cell-val {
font-size: 22px;
font-weight: 800;
color: #0f172a;
@@ -3521,17 +3648,17 @@
line-height: 1.15;
}
-.testing-container .compare-delta-cell-val.is-over { color: #dc2626; }
-.testing-container .compare-delta-cell-val.is-under { color: #16a34a; }
+.dispatch-container .compare-delta-cell-val.is-over { color: #dc2626; }
+.dispatch-container .compare-delta-cell-val.is-under { color: #16a34a; }
-.testing-container .compare-delta-cell-unit {
+.dispatch-container .compare-delta-cell-unit {
font-size: 13px;
font-weight: 700;
color: #94a3b8;
margin-left: 2px;
}
-.testing-container .compare-delta-cell-sub {
+.dispatch-container .compare-delta-cell-sub {
font-size: 12px;
font-weight: 600;
color: #64748b;
@@ -3544,7 +3671,7 @@
delivery's last GPS ping (the drop). Carries the step number and, when
delivered, a green check overlay. The .is-focused variant pulses + scales
so the operator can spot the currently scrutinized step at a glance. */
-.testing-container .compare-step-pin {
+.dispatch-container .compare-step-pin {
position: relative;
width: 34px;
height: 34px;
@@ -3565,23 +3692,23 @@
box-shadow 0.2s ease;
}
-.testing-container .compare-step-pin:hover {
+.dispatch-container .compare-step-pin:hover {
transform: scale(1.08);
z-index: 1200;
}
-.testing-container .compare-step-pin-num {
+.dispatch-container .compare-step-pin-num {
position: relative;
z-index: 1;
line-height: 1;
}
-.testing-container .compare-step-pin.is-skipped {
+.dispatch-container .compare-step-pin.is-skipped {
opacity: 0.45;
filter: grayscale(0.6);
}
-.testing-container .compare-step-pin.is-focused {
+.dispatch-container .compare-step-pin.is-focused {
transform: scale(1.22);
z-index: 1300;
box-shadow:
@@ -3592,13 +3719,13 @@
animation: compare-pin-pulse 1.6s ease-in-out infinite;
}
-.testing-container .compare-step-pin.is-focused:hover {
+.dispatch-container .compare-step-pin.is-focused:hover {
transform: scale(1.3);
}
/* Pulse halo for the focused step's drop pin. Uses a separate pseudo so the
pin itself can scale on hover without distorting the halo. */
-.testing-container .compare-step-pin.is-focused::before {
+.dispatch-container .compare-step-pin.is-focused::before {
content: '';
position: absolute;
inset: -6px;
@@ -3633,7 +3760,7 @@
/* Anomaly ring — replaces the colored outer ring with a red one when the
step is flagged (route deviation > 25% or arrival > 15 min late). */
-.testing-container .compare-step-pin.is-anomaly {
+.dispatch-container .compare-step-pin.is-anomaly {
box-shadow:
0 4px 14px rgba(220, 38, 38, 0.35),
0 0 0 1px rgba(255, 255, 255, 0.18),
@@ -3641,7 +3768,7 @@
0 0 0 5px #dc2626;
}
-.testing-container .compare-step-pin.is-anomaly.is-focused {
+.dispatch-container .compare-step-pin.is-anomaly.is-focused {
box-shadow:
0 8px 22px rgba(220, 38, 38, 0.5),
0 0 0 3px #ffffff,
@@ -3670,7 +3797,7 @@
/* Delivered checkmark — small green badge in the lower-right corner of the
drop pin. Reads as "this drop completed" without needing the status tag
that lives in the timeline + delta panel. */
-.testing-container .compare-step-pin-check {
+.dispatch-container .compare-step-pin-check {
position: absolute;
bottom: -3px;
right: -3px;
@@ -3694,7 +3821,7 @@
pixel size keeps it readable from city-level (z12) down to street-level
(z18+). Only rendered for sequenceStep === 1; subsequent steps don't
get a start marker since their origin is the previous step's drop. */
-.testing-container .compare-start-pin {
+.dispatch-container .compare-start-pin {
width: 40px;
height: 40px;
border-radius: 50%;
@@ -3710,14 +3837,14 @@
cursor: pointer;
}
-.testing-container .compare-start-pin:hover {
+.dispatch-container .compare-start-pin:hover {
transform: scale(1.1);
z-index: 1100;
box-shadow: 0 7px 20px rgba(15, 23, 42, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.7);
}
-.testing-container .compare-start-pin svg {
+.dispatch-container .compare-start-pin svg {
width: 22px;
height: 22px;
display: block;
@@ -3726,7 +3853,7 @@
/* Lightweight tooltip shown on marker hover. Replaces the older heavy popup
(which clipped and forced an auto-pan). Just a teaser; persistent details
live in the delta panel below the map. */
-.testing-container .compare-tooltip {
+.dispatch-container .compare-tooltip {
background: rgba(15, 23, 42, 0.95);
color: #f8fafc;
border: 0;
@@ -3741,33 +3868,33 @@
/* Leaflet draws a triangular tip pointing at the marker via a CSS border on
::before. Re-tint it to match the dark tooltip body. */
-.testing-container .compare-tooltip.leaflet-tooltip-top::before {
+.dispatch-container .compare-tooltip.leaflet-tooltip-top::before {
border-top-color: rgba(15, 23, 42, 0.95);
}
-.testing-container .compare-tooltip.leaflet-tooltip-bottom::before {
+.dispatch-container .compare-tooltip.leaflet-tooltip-bottom::before {
border-bottom-color: rgba(15, 23, 42, 0.95);
}
-.testing-container .compare-tooltip.leaflet-tooltip-left::before {
+.dispatch-container .compare-tooltip.leaflet-tooltip-left::before {
border-left-color: rgba(15, 23, 42, 0.95);
}
-.testing-container .compare-tooltip.leaflet-tooltip-right::before {
+.dispatch-container .compare-tooltip.leaflet-tooltip-right::before {
border-right-color: rgba(15, 23, 42, 0.95);
}
-.testing-container .cmp-tip {
+.dispatch-container .cmp-tip {
padding: 9px 12px 8px;
min-width: 200px;
max-width: 260px;
}
-.testing-container .cmp-tip-header {
+.dispatch-container .cmp-tip-header {
display: flex;
align-items: center;
gap: 9px;
min-width: 0;
}
-.testing-container .cmp-tip-step {
+.dispatch-container .cmp-tip-step {
width: 24px;
height: 24px;
border-radius: 50%;
@@ -3782,16 +3909,16 @@
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
-.testing-container .cmp-tip-step svg {
+.dispatch-container .cmp-tip-step svg {
font-size: 14px;
}
-.testing-container .cmp-tip-title-stack {
+.dispatch-container .cmp-tip-title-stack {
flex: 1;
min-width: 0;
}
-.testing-container .cmp-tip-title {
+.dispatch-container .cmp-tip-title {
font-size: 12px;
font-weight: 800;
color: #f8fafc;
@@ -3802,7 +3929,7 @@
text-overflow: ellipsis;
}
-.testing-container .cmp-tip-sub {
+.dispatch-container .cmp-tip-sub {
font-size: 10px;
font-weight: 600;
color: #cbd5e1;
@@ -3812,7 +3939,7 @@
text-overflow: ellipsis;
}
-.testing-container .cmp-tip-tag {
+.dispatch-container .cmp-tip-tag {
display: inline-flex;
align-items: center;
padding: 2px 7px;
@@ -3824,7 +3951,7 @@
flex-shrink: 0;
}
-.testing-container .cmp-tip-anomaly {
+.dispatch-container .cmp-tip-anomaly {
margin-top: 7px;
padding: 5px 8px;
border-radius: 8px;
@@ -3836,7 +3963,7 @@
letter-spacing: 0.02em;
}
-.testing-container .cmp-tip-action {
+.dispatch-container .cmp-tip-action {
margin-top: 7px;
padding-top: 6px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
@@ -3852,7 +3979,7 @@
tag) and a key/value table of order details. Leaflet's default popup is
a plain white tooltip; this overrides the wrapper/tip so the popup
matches the rest of the dispatch UI. */
-.testing-container .compare-popup .leaflet-popup-content-wrapper {
+.dispatch-container .compare-popup .leaflet-popup-content-wrapper {
padding: 0;
border-radius: 14px;
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.22), 0 0 0 1px rgba(15, 23, 42, 0.06);
@@ -3860,18 +3987,18 @@
background: #fff;
}
-.testing-container .compare-popup .leaflet-popup-content {
+.dispatch-container .compare-popup .leaflet-popup-content {
margin: 0;
width: auto !important;
min-width: 240px;
}
-.testing-container .compare-popup .leaflet-popup-tip {
+.dispatch-container .compare-popup .leaflet-popup-tip {
background: #fff;
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.18);
}
-.testing-container .compare-popup .leaflet-popup-close-button {
+.dispatch-container .compare-popup .leaflet-popup-close-button {
top: 6px;
right: 6px;
color: #94a3b8;
@@ -3880,17 +4007,17 @@
padding: 4px 6px;
}
-.testing-container .compare-popup .leaflet-popup-close-button:hover {
+.dispatch-container .compare-popup .leaflet-popup-close-button:hover {
color: #0f172a;
}
-.testing-container .cmp-pop {
+.dispatch-container .cmp-pop {
font-family: 'Inter', -apple-system, sans-serif;
color: #0f172a;
line-height: 1.35;
}
-.testing-container .cmp-pop-head {
+.dispatch-container .cmp-pop-head {
display: flex;
align-items: center;
gap: 10px;
@@ -3899,7 +4026,7 @@
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
}
-.testing-container .cmp-pop-pin {
+.dispatch-container .cmp-pop-pin {
width: 30px;
height: 30px;
border-radius: 50%;
@@ -3914,12 +4041,12 @@
flex-shrink: 0;
}
-.testing-container .cmp-pop-titles {
+.dispatch-container .cmp-pop-titles {
flex: 1;
min-width: 0;
}
-.testing-container .cmp-pop-title {
+.dispatch-container .cmp-pop-title {
font-size: 13px;
font-weight: 800;
color: #0f172a;
@@ -3928,7 +4055,7 @@
text-overflow: ellipsis;
}
-.testing-container .cmp-pop-sub {
+.dispatch-container .cmp-pop-sub {
font-size: 9.5px;
font-weight: 700;
color: #64748b;
@@ -3937,7 +4064,7 @@
margin-top: 2px;
}
-.testing-container .cmp-pop-tag {
+.dispatch-container .cmp-pop-tag {
padding: 3px 8px;
border-radius: 999px;
font-size: 9px;
@@ -3950,18 +4077,18 @@
white-space: nowrap;
}
-.testing-container .cmp-pop-tag-start {
+.dispatch-container .cmp-pop-tag-start {
background: #ecfeff;
color: #0e7490;
}
-.testing-container .cmp-pop-rows {
+.dispatch-container .cmp-pop-rows {
padding: 8px 14px 12px;
display: flex;
flex-direction: column;
}
-.testing-container .cmp-pop-row {
+.dispatch-container .cmp-pop-row {
display: flex;
justify-content: space-between;
align-items: center;
@@ -3971,51 +4098,51 @@
border-bottom: 1px dashed rgba(15, 23, 42, 0.07);
}
-.testing-container .cmp-pop-row:last-child {
+.dispatch-container .cmp-pop-row:last-child {
border-bottom: 0;
}
-.testing-container .cmp-pop-k {
+.dispatch-container .cmp-pop-k {
color: #64748b;
font-weight: 600;
white-space: nowrap;
}
-.testing-container .cmp-pop-v {
+.dispatch-container .cmp-pop-v {
color: #0f172a;
font-weight: 700;
font-variant-numeric: tabular-nums;
text-align: right;
}
-.testing-container .cmp-pop-v.is-loss {
+.dispatch-container .cmp-pop-v.is-loss {
color: #dc2626;
}
-.testing-container .cmp-pop-v.is-profit {
+.dispatch-container .cmp-pop-v.is-profit {
color: #16a34a;
}
-.testing-container .cmp-pop-coord {
+.dispatch-container .cmp-pop-coord {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10.5px;
font-weight: 600;
color: #475569;
}
-.testing-container .leaflet-container {
+.dispatch-container .leaflet-container {
background: #f1f5f9 !important;
}
/* Overlays */
-.testing-container #ov-tl {
+.dispatch-container #ov-tl {
position: absolute;
top: 16px;
left: 16px;
z-index: 1000;
}
-.testing-container .ov-card {
+.dispatch-container .ov-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
border: 1px solid var(--border);
@@ -4024,21 +4151,21 @@
box-shadow: var(--shadow-lg);
}
-.testing-container .ov-stats {
+.dispatch-container .ov-stats {
display: flex;
gap: 24px;
}
-.testing-container .osv {
+.dispatch-container .osv {
font-size: 24px;
font-weight: 800;
}
-.testing-container .osv.g {
+.dispatch-container .osv.g {
color: var(--success);
}
-.testing-container .osl {
+.dispatch-container .osl {
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
@@ -4046,7 +4173,7 @@
margin-top: 2px;
}
-.testing-container #ov-tr {
+.dispatch-container #ov-tr {
position: absolute;
top: 16px;
right: 16px;
@@ -4059,18 +4186,18 @@
/* Hide floating chips overlay when split-map Compare Mode is active,
since the operator is focused on one single rider and list is redundant. */
-.testing-container #body.compare-mode #ov-tr {
+.dispatch-container #body.compare-mode #ov-tr {
display: none !important;
}
-.testing-container #ov-br {
+.dispatch-container #ov-br {
position: absolute;
bottom: 20px;
right: 80px;
z-index: 1000;
}
-.testing-container .rchip {
+.dispatch-container .rchip {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
border: 1px solid var(--border);
@@ -4086,19 +4213,19 @@
transition: all 0.2s;
}
-.testing-container .rchip.active {
+.dispatch-container .rchip.active {
border-color: var(--accent);
background: #fff;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
}
-.testing-container .rchip-dot {
+.dispatch-container .rchip-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
-.testing-container .rchip-n {
+.dispatch-container .rchip-n {
margin-left: auto;
font-weight: 800;
color: var(--accent);
@@ -4109,12 +4236,12 @@
/* All deliveries done — flip the count to green so it pops vs the in-progress
accent color. */
-.testing-container .rchip-n.is-done {
+.dispatch-container .rchip-n.is-done {
color: #16a34a;
}
/* Markers */
-.testing-container .cmark {
+.dispatch-container .cmark {
border-radius: 50%;
border: 3px solid #fff;
display: flex;
@@ -4126,7 +4253,7 @@
letter-spacing: 0.02em;
}
-.testing-container .kitchen-mark {
+.dispatch-container .kitchen-mark {
background: var(--kitchen);
border: 3px solid #fff;
border-radius: 50%;
@@ -4143,7 +4270,7 @@
/* Focused kitchen marker — larger, brighter, with a pulsing halo so users
never lose sight of the kitchen they drilled into. */
-.testing-container .kitchen-mark.is-focused {
+.dispatch-container .kitchen-mark.is-focused {
width: 56px;
height: 56px;
font-size: 22px;
@@ -4157,7 +4284,7 @@
}
/* Popups - Clean White Look */
-.testing-container .leaflet-popup-content-wrapper {
+.dispatch-container .leaflet-popup-content-wrapper {
background: #ffffff;
color: #1e293b;
border-radius: 12px;
@@ -4166,7 +4293,7 @@
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15);
}
-.testing-container .leaflet-popup-tip-container {
+.dispatch-container .leaflet-popup-tip-container {
width: 20px;
height: 10px;
left: 50%;
@@ -4176,7 +4303,7 @@
bottom: -10px;
}
-.testing-container .leaflet-popup-tip {
+.dispatch-container .leaflet-popup-tip {
width: 14px;
height: 14px;
padding: 0;
@@ -4186,7 +4313,7 @@
box-shadow: none;
}
-.testing-container .pu-id {
+.dispatch-container .pu-id {
background: #f8fafc;
padding: 10px 14px;
font-size: 11px;
@@ -4198,13 +4325,13 @@
/* Apply rounding here instead */
}
-.testing-container .pu-rider {
+.dispatch-container .pu-rider {
padding: 12px 14px 4px;
font-size: 15px;
font-weight: 800;
}
-.testing-container .pu-row {
+.dispatch-container .pu-row {
display: flex;
justify-content: space-between;
padding: 4px 14px;
@@ -4212,18 +4339,18 @@
color: #64748b;
}
-.testing-container .pu-row:last-child {
+.dispatch-container .pu-row:last-child {
padding-bottom: 0;
}
-.testing-container .pu-row span:first-child {
+.dispatch-container .pu-row span:first-child {
color: #64748b;
font-weight: 500;
margin-right: 12px;
flex-shrink: 0;
}
-.testing-container .pu-row span:last-child {
+.dispatch-container .pu-row span:last-child {
color: #1e293b;
font-weight: 600;
text-align: right;
@@ -4233,7 +4360,7 @@
}
/* Small purple section label between groups (Timeline, Details). */
-.testing-container .pu-section-label {
+.dispatch-container .pu-section-label {
margin: 10px 14px 4px;
font-size: 10px;
font-weight: 700;
@@ -4247,7 +4374,7 @@
/* Compact vertical timeline of delivery stages. Each row has a dot + label
on the left and the time (HH:mm:ss) on the right, lined up in a tabular
monospaced look so the column reads cleanly at a glance. */
-.testing-container .pu-timeline {
+.dispatch-container .pu-timeline {
padding: 4px 14px 4px 16px;
display: flex;
flex-direction: column;
@@ -4255,7 +4382,7 @@
position: relative;
}
-.testing-container .pu-timeline::before {
+.dispatch-container .pu-timeline::before {
content: '';
position: absolute;
top: 8px;
@@ -4265,7 +4392,7 @@
background: rgba(123, 31, 162, 0.18);
}
-.testing-container .pu-tl-row {
+.dispatch-container .pu-tl-row {
display: flex;
align-items: center;
gap: 8px;
@@ -4274,7 +4401,7 @@
z-index: 1;
}
-.testing-container .pu-tl-dot {
+.dispatch-container .pu-tl-dot {
width: 8px;
height: 8px;
border-radius: 50%;
@@ -4284,31 +4411,31 @@
box-sizing: border-box;
}
-.testing-container .pu-tl-row.delivered .pu-tl-dot {
+.dispatch-container .pu-tl-row.delivered .pu-tl-dot {
background: #16a34a;
border-color: #16a34a;
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.18);
}
-.testing-container .pu-tl-label {
+.dispatch-container .pu-tl-label {
flex: 1;
color: #64748b;
font-weight: 500;
}
-.testing-container .pu-tl-row.delivered .pu-tl-label {
+.dispatch-container .pu-tl-row.delivered .pu-tl-label {
color: #16a34a;
font-weight: 700;
}
-.testing-container .pu-tl-time {
+.dispatch-container .pu-tl-time {
color: #1e293b;
font-weight: 700;
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum';
}
-.testing-container .dispatch-popup .leaflet-popup-content {
+.dispatch-container .dispatch-popup .leaflet-popup-content {
margin: 0;
width: auto !important;
}
@@ -4318,7 +4445,7 @@
min-width forces the wrapper to honor the Popup component's minWidth even
when the inner content (.leaflet-popup-content has width: auto !important)
would otherwise size to text. */
-.testing-container .dispatch-popup .leaflet-popup-content-wrapper {
+.dispatch-container .dispatch-popup .leaflet-popup-content-wrapper {
padding: 0;
border-radius: 14px;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.18);
@@ -4326,19 +4453,19 @@
min-width: 460px;
}
-.testing-container .dispatch-popup .leaflet-popup-content {
+.dispatch-container .dispatch-popup .leaflet-popup-content {
min-width: 460px;
}
/* --- Header: purple gradient with order id + status + rider --- */
-.testing-container .dispatch-popup .pu-header {
+.dispatch-container .dispatch-popup .pu-header {
background: linear-gradient(135deg, #7b1fa2 0%, #9c27b0 50%, #ab47bc 100%);
padding: 14px 16px 12px;
color: #fff;
border-radius: 12px 12px 0 0;
}
-.testing-container .dispatch-popup .pu-header-top {
+.dispatch-container .dispatch-popup .pu-header-top {
display: flex;
align-items: center;
justify-content: space-between;
@@ -4346,7 +4473,7 @@
margin-bottom: 8px;
}
-.testing-container .dispatch-popup .pu-id {
+.dispatch-container .dispatch-popup .pu-id {
/* Override the legacy pu-id styling — no separate background, sits on
the purple gradient instead. */
background: transparent;
@@ -4363,7 +4490,7 @@
text-overflow: ellipsis;
}
-.testing-container .dispatch-popup .pu-status-chip {
+.dispatch-container .dispatch-popup .pu-status-chip {
font-size: 10px;
font-weight: 800;
padding: 4px 10px;
@@ -4375,7 +4502,7 @@
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
}
-.testing-container .dispatch-popup .pu-rider {
+.dispatch-container .dispatch-popup .pu-rider {
padding: 0;
font-size: 16px;
font-weight: 800;
@@ -4386,18 +4513,18 @@
letter-spacing: -0.01em;
}
-.testing-container .dispatch-popup .pu-rider svg {
+.dispatch-container .dispatch-popup .pu-rider svg {
font-size: 18px;
opacity: 0.9;
}
-.testing-container .dispatch-popup .pu-rider span {
+.dispatch-container .dispatch-popup .pu-rider span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
-.testing-container .dispatch-popup .pu-delivery-id {
+.dispatch-container .dispatch-popup .pu-delivery-id {
margin-top: 6px;
font-size: 11px;
font-weight: 600;
@@ -4409,15 +4536,15 @@
No scroll: the popup expands to fit its content. Width is the dimension
we constrain (via leaflet's maxWidth prop) so the body grows downward as
needed for the timeline + details to render in full. */
-.testing-container .dispatch-popup .pu-body {
+.dispatch-container .dispatch-popup .pu-body {
padding: 4px 18px 16px;
}
-.testing-container .dispatch-popup .pu-section {
+.dispatch-container .dispatch-popup .pu-section {
margin-top: 12px;
}
-.testing-container .dispatch-popup .pu-section-label {
+.dispatch-container .dispatch-popup .pu-section-label {
/* Scoped override: no horizontal margin since pu-body already provides
the gutter. Sits flush with section content. */
margin: 0 0 8px;
@@ -4432,7 +4559,7 @@
/* --- Timeline (scoped override of the earlier rules so paddings match
the new pu-body gutter) --- */
-.testing-container .dispatch-popup .pu-timeline {
+.dispatch-container .dispatch-popup .pu-timeline {
padding: 4px 0 4px 4px;
display: flex;
flex-direction: column;
@@ -4440,18 +4567,18 @@
position: relative;
}
-.testing-container .dispatch-popup .pu-timeline::before {
+.dispatch-container .dispatch-popup .pu-timeline::before {
left: 7px;
}
/* --- Details grid: 2 columns of icon/label/value tiles --- */
-.testing-container .dispatch-popup .pu-details-grid {
+.dispatch-container .dispatch-popup .pu-details-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
-.testing-container .dispatch-popup .pu-detail {
+.dispatch-container .dispatch-popup .pu-detail {
display: flex;
align-items: flex-start;
gap: 8px;
@@ -4462,7 +4589,7 @@
min-width: 0;
}
-.testing-container .dispatch-popup .pu-detail-icon {
+.dispatch-container .dispatch-popup .pu-detail-icon {
width: 26px;
height: 26px;
border-radius: 7px;
@@ -4475,16 +4602,16 @@
font-size: 14px;
}
-.testing-container .dispatch-popup .pu-detail-icon svg {
+.dispatch-container .dispatch-popup .pu-detail-icon svg {
font-size: 15px;
}
-.testing-container .dispatch-popup .pu-detail-body {
+.dispatch-container .dispatch-popup .pu-detail-body {
min-width: 0;
flex: 1;
}
-.testing-container .dispatch-popup .pu-detail-label {
+.dispatch-container .dispatch-popup .pu-detail-label {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.06em;
@@ -4493,7 +4620,7 @@
margin-bottom: 2px;
}
-.testing-container .dispatch-popup .pu-detail-value {
+.dispatch-container .dispatch-popup .pu-detail-value {
font-size: 12px;
font-weight: 700;
color: #1e293b;
@@ -4504,7 +4631,7 @@
}
/* --- Distance chip row sits below the details grid --- */
-.testing-container .dispatch-popup .pu-distance-row {
+.dispatch-container .dispatch-popup .pu-distance-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
@@ -4513,7 +4640,7 @@
border-top: 1px dashed rgba(123, 31, 162, 0.18);
}
-.testing-container .dispatch-popup .pu-distance-chip {
+.dispatch-container .dispatch-popup .pu-distance-chip {
display: inline-flex;
align-items: center;
gap: 5px;
@@ -4525,16 +4652,16 @@
font-weight: 600;
}
-.testing-container .dispatch-popup .pu-distance-icon {
+.dispatch-container .dispatch-popup .pu-distance-icon {
display: inline-flex;
color: #7b1fa2;
}
-.testing-container .dispatch-popup .pu-distance-icon svg {
+.dispatch-container .dispatch-popup .pu-distance-icon svg {
font-size: 14px;
}
-.testing-container .dispatch-popup .pu-distance-label {
+.dispatch-container .dispatch-popup .pu-distance-label {
color: #64748b;
font-weight: 600;
font-size: 10px;
@@ -4542,14 +4669,14 @@
letter-spacing: 0.04em;
}
-.testing-container .dispatch-popup .pu-distance-value {
+.dispatch-container .dispatch-popup .pu-distance-value {
color: #1e293b;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
/* Kitchen Popup */
-.testing-container .kitchen-popup .kp-header {
+.dispatch-container .kitchen-popup .kp-header {
background: #f8fafc;
color: #64748b;
font-size: 10px;
@@ -4560,32 +4687,32 @@
border-radius: 12px 12px 0 0;
}
-.testing-container .kitchen-popup .kp-name {
+.dispatch-container .kitchen-popup .kp-name {
padding: 14px 14px 4px;
font-size: 16px;
font-weight: 800;
color: var(--kitchen);
}
-.testing-container .kitchen-popup .kp-stat {
+.dispatch-container .kitchen-popup .kp-stat {
display: flex;
justify-content: space-between;
padding: 8px 14px 16px;
}
-.testing-container .kitchen-popup .kp-stat-lbl {
+.dispatch-container .kitchen-popup .kp-stat-lbl {
font-size: 12px;
color: #64748b;
}
-.testing-container .kitchen-popup .kp-stat-val {
+.dispatch-container .kitchen-popup .kp-stat-val {
font-size: 16px;
font-weight: 800;
color: #1e293b;
}
/* Empty slot state — shown in the sidebar list when no orders match the selected batch */
-.testing-container .empty-slot {
+.dispatch-container .empty-slot {
display: flex;
flex-direction: column;
align-items: center;
@@ -4595,19 +4722,19 @@
text-align: center;
}
-.testing-container .empty-slot-icon {
+.dispatch-container .empty-slot-icon {
font-size: 36px;
color: var(--border);
line-height: 1;
}
-.testing-container .empty-slot-title {
+.dispatch-container .empty-slot-title {
font-size: 14px;
font-weight: 700;
color: var(--text-muted);
}
-.testing-container .empty-slot-sub {
+.dispatch-container .empty-slot-sub {
font-size: 12px;
font-weight: 500;
color: var(--border);
@@ -4615,7 +4742,7 @@
line-height: 1.5;
}
-.testing-container #desc {
+.dispatch-container #desc {
padding: 16px 20px;
font-size: 12px;
font-weight: 500;
@@ -4632,50 +4759,50 @@
/* Large laptop — subtle sidebar reduction */
@media (max-width: 1280px) {
- .testing-container #sidebar {
+ .dispatch-container #sidebar {
width: 360px;
flex-basis: 360px;
}
- .testing-container .sidebar-toggle-tab {
+ .dispatch-container .sidebar-toggle-tab {
left: 360px;
}
- .testing-container .sidebar-toggle-tab.is-collapsed {
+ .dispatch-container .sidebar-toggle-tab.is-collapsed {
left: 0;
}
}
/* Compact laptop (common 1366×768 screens) */
@media (max-width: 1180px) {
- .testing-container #sidebar {
+ .dispatch-container #sidebar {
width: 320px;
flex-basis: 320px;
}
- .testing-container .sidebar-toggle-tab {
+ .dispatch-container .sidebar-toggle-tab {
left: 320px;
}
- .testing-container .sidebar-toggle-tab.is-collapsed {
+ .dispatch-container .sidebar-toggle-tab.is-collapsed {
left: 0;
}
- .testing-container .rd-rider-name {
+ .dispatch-container .rd-rider-name {
font-size: 24px;
}
- .testing-container .rd-stat-value {
+ .dispatch-container .rd-stat-value {
font-size: 20px;
}
- .testing-container .sb-tile-value {
+ .dispatch-container .sb-tile-value {
font-size: 20px;
}
- .testing-container #hdr {
+ .dispatch-container #hdr {
padding: 0 16px;
}
- .testing-container #strat-row {
+ .dispatch-container #strat-row {
padding: 0 16px;
gap: 6px;
}
- .testing-container #batch-row {
+ .dispatch-container #batch-row {
padding: 8px 16px;
}
- .testing-container .sbt {
+ .dispatch-container .sbt {
padding: 7px 11px;
font-size: 12px;
gap: 6px;
@@ -4684,70 +4811,70 @@
/* Small laptop / 1024px */
@media (max-width: 1080px) {
- .testing-container #sidebar {
+ .dispatch-container #sidebar {
width: 290px;
flex-basis: 290px;
}
- .testing-container .sidebar-toggle-tab {
+ .dispatch-container .sidebar-toggle-tab {
left: 290px;
}
- .testing-container .sidebar-toggle-tab.is-collapsed {
+ .dispatch-container .sidebar-toggle-tab.is-collapsed {
left: 0;
}
/* Header — hide decorative city pill, tighten spacing */
- .testing-container .logo-city,
- .testing-container .logo-city-wrap {
+ .dispatch-container .logo-city,
+ .dispatch-container .logo-city-wrap {
display: none;
}
- .testing-container .logo-name {
+ .dispatch-container .logo-name {
font-size: 16px;
}
- .testing-container #clock {
+ .dispatch-container #clock {
font-size: 12px;
padding: 5px 10px;
}
- .testing-container .hdr-stats {
+ .dispatch-container .hdr-stats {
gap: 6px;
margin-right: 8px;
}
- .testing-container .strat-stat {
+ .dispatch-container .strat-stat {
padding: 5px 9px;
font-size: 11px;
gap: 4px;
}
/* Hide the verbose "Profit / Loss" text label; keep icon + value */
- .testing-container .strat-stat-label {
+ .dispatch-container .strat-stat-label {
display: none;
}
- .testing-container .live-status {
+ .dispatch-container .live-status {
font-size: 11px;
padding: 5px 8px;
}
/* Hide the "/ N today" sub-text to keep status compact */
- .testing-container .live-status-sub {
+ .dispatch-container .live-status-sub {
display: none;
}
/* Tabs — smaller */
- .testing-container .sbt {
+ .dispatch-container .sbt {
padding: 7px 10px;
font-size: 12px;
gap: 5px;
}
- .testing-container .sbt .sbt-icon {
+ .dispatch-container .sbt .sbt-icon {
width: 16px;
height: 16px;
font-size: 16px;
}
/* Batch slots — smaller pills */
- .testing-container .batch-btn {
+ .dispatch-container .batch-btn {
padding: 5px 9px;
font-size: 11px;
gap: 4px;
}
- .testing-container .batch-btn-count {
+ .dispatch-container .batch-btn-count {
min-width: 18px;
height: 16px;
font-size: 9px;
@@ -4755,51 +4882,51 @@
}
/* Sidebar content */
- .testing-container .sb-header {
+ .dispatch-container .sb-header {
padding: 14px 14px 12px;
}
- .testing-container .sb-tile-value {
+ .dispatch-container .sb-tile-value {
font-size: 18px;
}
- .testing-container .sb-tile {
+ .dispatch-container .sb-tile {
padding: 8px 10px;
gap: 8px;
}
- .testing-container .sb-tile-icon {
+ .dispatch-container .sb-tile-icon {
width: 28px;
height: 28px;
font-size: 16px;
}
- .testing-container .rcard {
+ .dispatch-container .rcard {
padding: 12px;
}
- .testing-container .rcard-name {
+ .dispatch-container .rcard-name {
font-size: 13px;
}
- .testing-container .rcard-zone {
+ .dispatch-container .rcard-zone {
font-size: 11px;
}
- .testing-container .step-wrap {
+ .dispatch-container .step-wrap {
padding: 12px;
}
- .testing-container #route-detail {
+ .dispatch-container #route-detail {
padding: 16px;
}
- .testing-container .rd-rider-name {
+ .dispatch-container .rd-rider-name {
font-size: 20px;
}
- .testing-container .rd-stat-value {
+ .dispatch-container .rd-stat-value {
font-size: 17px;
}
- .testing-container .rd-stat {
+ .dispatch-container .rd-stat {
padding: 12px 8px 10px;
}
/* Map overlay chips — narrower */
- .testing-container #ov-tr {
+ .dispatch-container #ov-tr {
width: 160px;
}
- .testing-container .rchip {
+ .dispatch-container .rchip {
padding: 6px 8px;
font-size: 11px;
}
@@ -4807,82 +4934,82 @@
/* Very small laptop / tablet landscape — 960px */
@media (max-width: 960px) {
- .testing-container #sidebar {
+ .dispatch-container #sidebar {
width: 250px;
flex-basis: 250px;
}
- .testing-container .sidebar-toggle-tab {
+ .dispatch-container .sidebar-toggle-tab {
left: 250px;
}
- .testing-container .sidebar-toggle-tab.is-collapsed {
+ .dispatch-container .sidebar-toggle-tab.is-collapsed {
left: 0;
}
/* Make strat-row horizontally scrollable if buttons overflow */
- .testing-container #strat-row {
+ .dispatch-container #strat-row {
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
- .testing-container #strat-row::-webkit-scrollbar {
+ .dispatch-container #strat-row::-webkit-scrollbar {
display: none;
}
/* Keep buttons from shrinking inside the scroll container */
- .testing-container .sbt {
+ .dispatch-container .sbt {
flex-shrink: 0;
padding: 7px 9px;
font-size: 11px;
}
/* Zone stat pills — drop the text label, keep icon + value */
- .testing-container .zone-stat-label {
+ .dispatch-container .zone-stat-label {
display: none;
}
- .testing-container .zone-stat-pill {
+ .dispatch-container .zone-stat-pill {
padding: 3px 7px;
gap: 3px;
}
- .testing-container .zone-stat-value {
+ .dispatch-container .zone-stat-value {
font-size: 12px;
}
/* Focused-rider stat tiles */
- .testing-container .rd-stats-grid {
+ .dispatch-container .rd-stats-grid {
gap: 6px;
}
- .testing-container .rd-stat {
+ .dispatch-container .rd-stat {
padding: 10px 6px 8px;
}
- .testing-container .rd-stat-value {
+ .dispatch-container .rd-stat-value {
font-size: 15px;
}
- .testing-container .rd-stat-label {
+ .dispatch-container .rd-stat-label {
font-size: 9px;
}
- .testing-container .rd-stat-icon {
+ .dispatch-container .rd-stat-icon {
font-size: 15px;
}
/* Hide map overlay rider/kitchen chip list — not enough space */
- .testing-container #ov-tr {
+ .dispatch-container #ov-tr {
display: none;
}
/* Zone card adjustments */
- .testing-container .zone-card-name {
+ .dispatch-container .zone-card-name {
font-size: 13px;
}
- .testing-container .zone-card-sub {
+ .dispatch-container .zone-card-sub {
font-size: 10px;
}
/* Trim padding in various panels */
- .testing-container #riders-panel {
+ .dispatch-container #riders-panel {
padding: 12px;
}
- .testing-container .trip-header {
+ .dispatch-container .trip-header {
padding: 10px 12px;
}
}
@@ -4894,7 +5021,7 @@
snapshot.
========================================================================= */
-.testing-container .rider-info-mode {
+.dispatch-container .rider-info-mode {
display: flex;
flex: 1;
min-height: 0;
@@ -4902,7 +5029,7 @@
background: var(--bg);
}
-.testing-container .ri-sidebar {
+.dispatch-container .ri-sidebar {
width: 320px;
min-width: 280px;
flex-shrink: 0;
@@ -4913,26 +5040,26 @@
overflow: hidden;
}
-.testing-container .ri-sb-head {
+.dispatch-container .ri-sb-head {
padding: 16px 18px 12px;
border-bottom: 1px solid var(--border, rgba(15, 23, 42, 0.08));
}
-.testing-container .ri-sb-title {
+.dispatch-container .ri-sb-title {
font-size: 20px;
font-weight: 800;
color: #1e293b;
letter-spacing: -0.01em;
}
-.testing-container .ri-sb-sub {
+.dispatch-container .ri-sb-sub {
font-size: 13px;
font-weight: 600;
color: #64748b;
margin-top: 4px;
}
-.testing-container .ri-main {
+.dispatch-container .ri-main {
flex: 1;
min-width: 0;
overflow-y: auto;
@@ -4940,17 +5067,17 @@
background: #fff;
}
-.testing-container .ri-main::-webkit-scrollbar {
+.dispatch-container .ri-main::-webkit-scrollbar {
width: 6px;
}
-.testing-container .ri-main::-webkit-scrollbar-thumb {
+.dispatch-container .ri-main::-webkit-scrollbar-thumb {
background: rgba(123, 31, 162, 0.25);
border-radius: 999px;
}
/* Placeholder when no rider has been selected yet. */
-.testing-container .ri-placeholder {
+.dispatch-container .ri-placeholder {
display: flex;
flex-direction: column;
align-items: center;
@@ -4961,7 +5088,7 @@
color: #64748b;
}
-.testing-container .ri-placeholder-icon {
+.dispatch-container .ri-placeholder-icon {
width: 64px;
height: 64px;
border-radius: 16px;
@@ -4974,18 +5101,18 @@
margin-bottom: 16px;
}
-.testing-container .ri-placeholder-icon svg {
+.dispatch-container .ri-placeholder-icon svg {
font-size: 36px;
}
-.testing-container .ri-placeholder-title {
+.dispatch-container .ri-placeholder-title {
font-size: 16px;
font-weight: 800;
color: #1e293b;
letter-spacing: -0.01em;
}
-.testing-container .ri-placeholder-sub {
+.dispatch-container .ri-placeholder-sub {
margin-top: 6px;
font-size: 12px;
font-weight: 500;
@@ -4994,12 +5121,12 @@
}
/* Search input — used in the sidebar */
-.testing-container .ri-search {
+.dispatch-container .ri-search {
padding: 12px 14px 4px;
position: relative;
}
-.testing-container .ri-search-icon {
+.dispatch-container .ri-search-icon {
position: absolute;
left: 26px;
top: 50%;
@@ -5009,7 +5136,7 @@
pointer-events: none;
}
-.testing-container .ri-search-input {
+.dispatch-container .ri-search-input {
width: 100%;
padding: 10px 12px 10px 38px;
border-radius: 10px;
@@ -5023,12 +5150,12 @@
transition: border-color 0.15s ease, background 0.15s ease;
}
-.testing-container .ri-search-input:focus {
+.dispatch-container .ri-search-input:focus {
border-color: #7b1fa2;
background: #fff;
}
-.testing-container .ri-rider-list {
+.dispatch-container .ri-rider-list {
display: flex;
flex-direction: column;
gap: 6px;
@@ -5038,16 +5165,16 @@
min-height: 0;
}
-.testing-container .ri-rider-list::-webkit-scrollbar {
+.dispatch-container .ri-rider-list::-webkit-scrollbar {
width: 6px;
}
-.testing-container .ri-rider-list::-webkit-scrollbar-thumb {
+.dispatch-container .ri-rider-list::-webkit-scrollbar-thumb {
background: rgba(123, 31, 162, 0.25);
border-radius: 999px;
}
-.testing-container .ri-rider-item {
+.dispatch-container .ri-rider-item {
display: flex;
align-items: center;
gap: 10px;
@@ -5062,28 +5189,28 @@
transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease;
}
-.testing-container .ri-rider-item:hover {
+.dispatch-container .ri-rider-item:hover {
border-color: rgba(123, 31, 162, 0.4);
background: rgba(123, 31, 162, 0.04);
transform: translateX(2px);
}
-.testing-container .ri-rider-item.active {
+.dispatch-container .ri-rider-item.active {
border-color: #7b1fa2;
background: linear-gradient(180deg, #fbf3ff 0%, #f0e0fa 100%);
box-shadow: 0 4px 12px rgba(123, 31, 162, 0.18);
}
-.testing-container .ri-rider-item.active .ri-rider-name {
+.dispatch-container .ri-rider-item.active .ri-rider-name {
color: #7b1fa2;
}
-.testing-container .ri-rider-item.active .ri-rider-arrow {
+.dispatch-container .ri-rider-item.active .ri-rider-arrow {
opacity: 1;
transform: translateX(2px);
}
-.testing-container .ri-rider-dot {
+.dispatch-container .ri-rider-dot {
width: 12px;
height: 12px;
border-radius: 50%;
@@ -5091,7 +5218,7 @@
box-shadow: 0 0 0 2px #fff, 0 0 0 3px rgba(15, 23, 42, 0.08);
}
-.testing-container .ri-rider-info-block {
+.dispatch-container .ri-rider-info-block {
flex: 1;
display: flex;
flex-direction: column;
@@ -5099,7 +5226,7 @@
min-width: 0;
}
-.testing-container .ri-rider-name {
+.dispatch-container .ri-rider-name {
font-size: 13px;
font-weight: 700;
color: #1e293b;
@@ -5108,57 +5235,57 @@
text-overflow: ellipsis;
}
-.testing-container .ri-rider-meta {
+.dispatch-container .ri-rider-meta {
font-size: 11px;
font-weight: 500;
color: #64748b;
}
-.testing-container .ri-rider-arrow {
+.dispatch-container .ri-rider-arrow {
color: #7b1fa2;
font-weight: 800;
opacity: 0.4;
transition: opacity 0.15s ease, transform 0.15s ease;
}
-.testing-container .ri-rider-item:hover .ri-rider-arrow {
+.dispatch-container .ri-rider-item:hover .ri-rider-arrow {
opacity: 1;
transform: translateX(2px);
}
-.testing-container .ri-empty {
+.dispatch-container .ri-empty {
padding: 32px 16px;
text-align: center;
font-size: 12px;
color: #64748b;
}
-.testing-container .ri-loading,
-.testing-container .ri-error {
+.dispatch-container .ri-loading,
+.dispatch-container .ri-error {
padding: 32px 16px;
text-align: center;
font-size: 13px;
color: #64748b;
}
-.testing-container .ri-error {
+.dispatch-container .ri-error {
color: #dc2626;
}
-.testing-container .ri-snap-head {
+.dispatch-container .ri-snap-head {
padding-bottom: 12px;
border-bottom: 1px dashed rgba(123, 31, 162, 0.18);
margin-bottom: 14px;
}
-.testing-container .ri-snap-name {
+.dispatch-container .ri-snap-name {
font-size: 18px;
font-weight: 800;
color: #1e293b;
letter-spacing: -0.01em;
}
-.testing-container .ri-snap-meta {
+.dispatch-container .ri-snap-meta {
display: flex;
align-items: center;
gap: 8px;
@@ -5168,7 +5295,7 @@
color: #64748b;
}
-.testing-container .ri-status {
+.dispatch-container .ri-status {
padding: 2px 10px;
border-radius: 999px;
font-size: 10px;
@@ -5179,19 +5306,19 @@
color: #475569;
}
-.testing-container .ri-status-idle {
+.dispatch-container .ri-status-idle {
background: rgba(245, 158, 11, 0.18);
color: #b45309;
}
-.testing-container .ri-status-active,
-.testing-container .ri-status-online,
-.testing-container .ri-status-ongoing {
+.dispatch-container .ri-status-active,
+.dispatch-container .ri-status-online,
+.dispatch-container .ri-status-ongoing {
background: rgba(34, 197, 94, 0.18);
color: #15803d;
}
-.testing-container .ri-status-offline {
+.dispatch-container .ri-status-offline {
background: rgba(239, 68, 68, 0.18);
color: #b91c1c;
}
@@ -5199,7 +5326,7 @@
/* Live pill — sits next to the status to signal the snapshot auto-refreshes
every 30s. Green pulsing dot when idle, brief flash + 'Updating…' label
while a refetch is in flight. */
-.testing-container .ri-live {
+.dispatch-container .ri-live {
display: inline-flex;
align-items: center;
gap: 5px;
@@ -5213,12 +5340,12 @@
text-transform: uppercase;
}
-.testing-container .ri-live.is-refetching {
+.dispatch-container .ri-live.is-refetching {
background: rgba(123, 31, 162, 0.12);
color: #7b1fa2;
}
-.testing-container .ri-live-dot {
+.dispatch-container .ri-live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
@@ -5227,7 +5354,7 @@
animation: ri-live-pulse 1.6s ease-in-out infinite;
}
-.testing-container .ri-live.is-refetching .ri-live-dot {
+.dispatch-container .ri-live.is-refetching .ri-live-dot {
background: #7b1fa2;
box-shadow: 0 0 0 0 rgba(123, 31, 162, 0.55);
}
@@ -5237,7 +5364,7 @@
50% { box-shadow: 0 0 0 5px rgba(34, 197, 94, 0); }
}
-.testing-container .ri-snap-time {
+.dispatch-container .ri-snap-time {
display: inline-flex;
align-items: center;
gap: 4px;
@@ -5248,18 +5375,18 @@
font-variant-numeric: tabular-nums;
}
-.testing-container .ri-snap-time svg {
+.dispatch-container .ri-snap-time svg {
font-size: 13px;
}
/* Stats grid — 2 columns of icon tiles, like the popup details grid */
-.testing-container .ri-snap-grid {
+.dispatch-container .ri-snap-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
-.testing-container .ri-stat {
+.dispatch-container .ri-stat {
display: flex;
align-items: center;
gap: 10px;
@@ -5269,12 +5396,12 @@
border-radius: 10px;
}
-.testing-container .ri-stat-warn {
+.dispatch-container .ri-stat-warn {
background: rgba(239, 68, 68, 0.06);
border-color: rgba(239, 68, 68, 0.22);
}
-.testing-container .ri-stat-icon {
+.dispatch-container .ri-stat-icon {
width: 32px;
height: 32px;
border-radius: 9px;
@@ -5287,21 +5414,21 @@
flex-shrink: 0;
}
-.testing-container .ri-stat-warn .ri-stat-icon {
+.dispatch-container .ri-stat-warn .ri-stat-icon {
background: rgba(239, 68, 68, 0.16);
color: #b91c1c;
}
-.testing-container .ri-stat-icon svg {
+.dispatch-container .ri-stat-icon svg {
font-size: 18px;
}
-.testing-container .ri-stat-body {
+.dispatch-container .ri-stat-body {
flex: 1;
min-width: 0;
}
-.testing-container .ri-stat-label {
+.dispatch-container .ri-stat-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.05em;
@@ -5310,7 +5437,7 @@
margin-bottom: 2px;
}
-.testing-container .ri-stat-value {
+.dispatch-container .ri-stat-value {
font-size: 13px;
font-weight: 700;
color: #1e293b;
@@ -5322,7 +5449,7 @@
gap: 6px;
}
-.testing-container .ri-stat-tag {
+.dispatch-container .ri-stat-tag {
font-size: 9px;
font-weight: 800;
padding: 2px 6px;
@@ -5336,11 +5463,11 @@
/* Coordinates footer */
/* Map section — coords header above an embedded Leaflet map showing the
rider's last reported position. */
-.testing-container .ri-map-section {
+.dispatch-container .ri-map-section {
margin-top: 14px;
}
-.testing-container .ri-coords-label {
+.dispatch-container .ri-coords-label {
font-size: 12px;
font-weight: 700;
color: #1e293b;
@@ -5357,12 +5484,12 @@
box-sizing: border-box;
}
-.testing-container .ri-coords-label svg {
+.dispatch-container .ri-coords-label svg {
color: #7b1fa2;
font-size: 16px;
}
-.testing-container .ri-map {
+.dispatch-container .ri-map {
height: 260px;
width: 100%;
border: 1px solid rgba(123, 31, 162, 0.18);
@@ -5372,7 +5499,7 @@
z-index: 0; /* Keep leaflet panes below the strat-row tooltips */
}
-.testing-container .ri-map .leaflet-container {
+.dispatch-container .ri-map .leaflet-container {
height: 100%;
width: 100%;
font-family: inherit;
@@ -5382,7 +5509,7 @@
Shows the suburb/area name reverse-geocoded from lat/lon so the operator
can read the location without opening the popup. Styled to override the
default leaflet tooltip chrome (rounded chip, brand purple). */
-.testing-container .ri-map .leaflet-tooltip.ri-area-banner {
+.dispatch-container .ri-map .leaflet-tooltip.ri-area-banner {
background: #7b1fa2;
color: #fff;
border: 0;
@@ -5395,26 +5522,26 @@
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.25);
}
-.testing-container .ri-map .leaflet-tooltip.ri-area-banner::before {
+.dispatch-container .ri-map .leaflet-tooltip.ri-area-banner::before {
border-top-color: #7b1fa2;
}
/* Mobile — collapse the sidebar above the main panel, single-column stats */
@media (max-width: 600px) {
- .testing-container .rider-info-mode {
+ .dispatch-container .rider-info-mode {
flex-direction: column;
}
- .testing-container .ri-sidebar {
+ .dispatch-container .ri-sidebar {
width: 100%;
min-width: 0;
max-height: 40vh;
border-right: 0;
border-bottom: 1px solid var(--border, rgba(15, 23, 42, 0.08));
}
- .testing-container .ri-main {
+ .dispatch-container .ri-main {
padding: 16px;
}
- .testing-container .ri-snap-grid {
+ .dispatch-container .ri-snap-grid {
grid-template-columns: 1fr;
}
}
@@ -5422,227 +5549,257 @@
/* ── Laptop Responsive Tuning (max-width: 1366px) ── */
@media (max-width: 1366px) {
/* Header adjustments */
- .testing-container #hdr {
+ .dispatch-container #hdr {
height: 48px;
padding: 0 16px;
}
- .testing-container .logo-name {
+ .dispatch-container .logo-name {
font-size: 15px;
}
- .testing-container .logo-badge {
+ .dispatch-container .logo-badge {
width: 28px;
height: 28px;
font-size: 13px;
border-radius: 6px;
}
- .testing-container .logo {
+ .dispatch-container .logo {
gap: 8px;
}
- .testing-container #clock {
+ .dispatch-container #clock {
font-size: 11px;
padding: 4px 10px;
}
- .testing-container .hdr-stats {
+ .dispatch-container .hdr-stats {
gap: 8px;
margin-right: 8px;
}
- .testing-container .strat-stat {
+ .dispatch-container .strat-stat {
padding: 4px 8px;
font-size: 11px;
gap: 4px;
}
- .testing-container .strat-stat-label {
+ .dispatch-container .strat-stat-label {
display: none; /* Hide profit/loss labels early to fit numbers */
}
- .testing-container .live-status {
+ .dispatch-container .live-status {
font-size: 11px;
padding: 4px 8px;
}
- .testing-container .live-status-sub {
+ .dispatch-container .live-status-sub {
display: none; /* Hide total orders suffix to save space */
}
- .testing-container .live-date-label {
+ .dispatch-container .live-date-label {
font-size: 11px;
gap: 6px;
}
- .testing-container .live-date-label span {
+ .dispatch-container .live-date-label span {
display: none; /* Hide the word 'Date' */
}
- .testing-container .live-date-label input[type="date"] {
+ .dispatch-container .live-date-label input[type="date"] {
font-size: 12px;
padding: 4px 8px;
}
/* Strategy Tab Row adjustments */
- .testing-container #strat-row {
+ .dispatch-container #strat-row {
height: 38px;
padding: 0 16px;
gap: 6px;
}
- .testing-container .sbt {
+ .dispatch-container .sbt {
padding: 6px 10px;
font-size: 11px;
gap: 5px;
}
- .testing-container .sbt .sbt-icon {
+ .dispatch-container .sbt .sbt-icon {
width: 15px;
height: 15px;
font-size: 15px;
}
/* Batch Slots Row adjustments */
- .testing-container #batch-row {
+ .dispatch-container #batch-row {
padding: 6px 16px;
gap: 6px;
}
- .testing-container .batch-label {
+ .dispatch-container .batch-label {
font-size: 11px;
}
- .testing-container .batch-btn {
+ .dispatch-container .batch-btn {
padding: 4px 8px;
font-size: 11px;
}
/* Sidebar Layout adjustments */
- .testing-container #sidebar {
+ .dispatch-container #sidebar {
width: 320px;
flex-basis: 320px;
}
- .testing-container .sidebar-toggle-tab {
+ .dispatch-container .sidebar-toggle-tab {
left: 320px;
}
/* Dynamic reduction in Compare Mode to keep dual maps wide enough */
- .testing-container #body.compare-mode #sidebar {
+ .dispatch-container #body.compare-mode #sidebar {
width: 250px;
flex-basis: 250px;
}
- .testing-container #body.compare-mode .sidebar-toggle-tab {
+ .dispatch-container #body.compare-mode .sidebar-toggle-tab {
left: 250px;
}
- .testing-container #body.compare-mode .sidebar-toggle-tab.is-collapsed {
+ .dispatch-container #body.compare-mode .sidebar-toggle-tab.is-collapsed {
left: 0;
}
/* Trim sidebar item paddings to increase visual density */
- .testing-container .sb-header {
+ .dispatch-container .sb-header {
padding: 10px 12px;
}
- .testing-container .sb-tile {
+ .dispatch-container .sb-tile {
padding: 6px 8px;
gap: 6px;
}
- .testing-container .sb-tile-value {
+ .dispatch-container .sb-tile-value {
font-size: 16px;
}
- .testing-container .rcard {
+ .dispatch-container .rcard {
padding: 10px;
}
- .testing-container .rcard-name {
+ .dispatch-container .rcard-name {
font-size: 12px;
}
- .testing-container .rcard-zone {
+ .dispatch-container .rcard-zone {
font-size: 10px;
}
- .testing-container .step-wrap {
+ .dispatch-container .step-wrap {
padding: 10px;
}
- .testing-container #route-detail {
+ .dispatch-container #route-detail {
padding: 12px;
}
- .testing-container .rd-rider-name {
+ .dispatch-container .rd-rider-name {
font-size: 18px;
}
- .testing-container .rd-stats-grid {
+ .dispatch-container .rd-stats-grid {
gap: 4px;
}
- .testing-container .rd-stat {
+ .dispatch-container .rd-stat {
padding: 8px 4px 6px;
}
- .testing-container .rd-stat-value {
+ .dispatch-container .rd-stat-value {
font-size: 14px;
}
- .testing-container .rd-stat-label {
+ .dispatch-container .rd-stat-label {
font-size: 9px;
}
/* Dual-map Compare Mode Header compression for 14" laptops */
- .testing-container .compare-header-v2 {
+ .dispatch-container .compare-header-v2 {
padding: 8px 12px 6px;
gap: 6px;
}
- .testing-container .compare-header-row .compare-title {
+ .dispatch-container .compare-header-row .compare-title {
font-size: 13px;
gap: 8px;
}
- .testing-container .compare-title-dot {
+ .dispatch-container .compare-title-dot {
width: 8px;
height: 8px;
}
- .testing-container .compare-title-badge {
+ .dispatch-container .compare-title-badge {
padding: 2px 6px;
font-size: 9px;
}
- .testing-container .compare-overall-btn,
- .testing-container .compare-sync-toggle {
+ .dispatch-container .compare-overall-btn,
+ .dispatch-container .compare-sync-toggle {
padding: 4px 8px;
font-size: 10px;
gap: 4px;
}
- .testing-container .compare-overall-btn svg,
- .testing-container .compare-sync-toggle svg {
+ .dispatch-container .compare-overall-btn svg,
+ .dispatch-container .compare-sync-toggle svg {
font-size: 12px;
}
/* Compare Mode Step Timeline Compression for 14" laptops */
- .testing-container .compare-timeline-wrap {
+ .dispatch-container .compare-timeline-wrap {
gap: 4px;
}
- .testing-container .compare-timeline {
- padding: 4px 12px 6px;
- flex-wrap: nowrap !important;
+ .dispatch-container .compare-timeline-container {
+ padding: 6px 10px;
+ gap: 12px;
+ border-radius: 8px;
+ }
+ .dispatch-container .compare-timeline-labels {
+ padding-right: 10px;
+ gap: 12px;
+ border-right-width: 1px;
+ }
+ .dispatch-container .compare-timeline-label {
+ font-size: 9px;
+ height: 24px; /* aligns with 24px circle height */
+ }
+ .dispatch-container .compare-timeline-scrollable {
+ flex: 1;
overflow-x: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
scrollbar-width: none; /* Hide standard Firefox scrollbar */
-ms-overflow-style: none; /* Hide IE scrollbar */
}
- .testing-container .compare-timeline::-webkit-scrollbar {
+ .dispatch-container .compare-timeline-scrollable::-webkit-scrollbar {
height: 4px;
}
- .testing-container .compare-timeline::-webkit-scrollbar-track {
+ .dispatch-container .compare-timeline-scrollable::-webkit-scrollbar-track {
background: transparent;
}
- .testing-container .compare-timeline::-webkit-scrollbar-thumb {
+ .dispatch-container .compare-timeline-scrollable::-webkit-scrollbar-thumb {
background: rgba(99, 102, 241, 0.25);
border-radius: 999px;
}
- .testing-container .compare-timeline:hover::-webkit-scrollbar-thumb {
+ .dispatch-container .compare-timeline-scrollable:hover::-webkit-scrollbar-thumb {
background: rgba(99, 102, 241, 0.5);
}
- .testing-container .compare-step {
+ /* Planned track overrides to align vertically centered since there are no ticks */
+ .dispatch-container .compare-timeline-track.is-planned .compare-step {
+ height: 24px;
+ }
+ .dispatch-container .compare-timeline-track.is-planned .compare-step-spacer {
+ margin-bottom: 0;
+ align-self: center;
+ }
+
+ /* Actual track overrides for the spacer alignment */
+ .dispatch-container .compare-timeline-track.is-actual .compare-step-spacer {
+ margin-bottom: 14px; /* Centers spacer dynamically relative to the 24px circle */
+ }
+
+ .dispatch-container .compare-step {
gap: 4px; /* Tighten spacer-circle layout vertical spacing */
}
- .testing-container .compare-step-circle {
+ .dispatch-container .compare-step-circle {
width: 24px;
height: 24px;
font-size: 10px;
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.1), 0 0 0 1px rgba(255, 255, 255, 0.6);
}
- .testing-container .compare-step.is-focused .compare-step-circle {
+ .dispatch-container .compare-step.is-focused .compare-step-circle {
transform: scale(1.1);
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.15),
0 0 0 2px #fff,
0 0 0 3px var(--step-color, #6366f1);
}
- .testing-container .compare-step-spacer {
+ .dispatch-container .compare-step-spacer {
width: 10px;
margin-bottom: 14px; /* Shift connecting lines up for 24px circles */
}
- .testing-container .compare-step-tick {
+ .dispatch-container .compare-step-tick {
font-size: 9px;
}
- .testing-container .compare-step-flag {
+ .dispatch-container .compare-step-flag {
top: -3px;
right: -3px;
width: 8px;
@@ -5651,73 +5808,73 @@
}
/* Progress Strip inside Timeline */
- .testing-container .compare-progress-strip {
+ .dispatch-container .compare-progress-strip {
margin-top: 2px;
}
- .testing-container .compare-progress-text {
+ .dispatch-container .compare-progress-text {
font-size: 9px;
}
/* Legend compacting for laptops */
- .testing-container .compare-legend {
+ .dispatch-container .compare-legend {
padding-top: 2px;
margin-top: 0;
gap: 8px;
}
- .testing-container .compare-legend-item {
+ .dispatch-container .compare-legend-item {
font-size: 9px;
}
- .testing-container .compare-legend-swatch {
+ .dispatch-container .compare-legend-swatch {
width: 10px;
height: 10px;
}
- .testing-container .compare-legend-note {
+ .dispatch-container .compare-legend-note {
display: none; /* Hide wordy GPS smoothing notes */
}
/* Bottom Delta Card panel compression for 14" laptops */
- .testing-container .compare-delta {
+ .dispatch-container .compare-delta {
padding: 8px 12px;
gap: 6px;
}
- .testing-container .compare-delta-title {
+ .dispatch-container .compare-delta-title {
margin-bottom: 4px;
gap: 8px;
}
- .testing-container .compare-delta-step-badge {
+ .dispatch-container .compare-delta-step-badge {
width: 20px;
height: 20px;
font-size: 11px;
}
- .testing-container .compare-delta-title-main {
+ .dispatch-container .compare-delta-title-main {
font-size: 12px;
}
- .testing-container .compare-delta-title-sub {
+ .dispatch-container .compare-delta-title-sub {
font-size: 9px;
}
- .testing-container .compare-delta-status {
+ .dispatch-container .compare-delta-status {
padding: 2px 6px;
font-size: 9px;
}
- .testing-container .compare-delta-grid {
+ .dispatch-container .compare-delta-grid {
gap: 6px;
}
- .testing-container .compare-delta-cell {
+ .dispatch-container .compare-delta-cell {
padding: 5px 8px 4px;
border-radius: 8px;
}
- .testing-container .compare-delta-cell-label {
+ .dispatch-container .compare-delta-cell-label {
font-size: 9px;
margin-bottom: 1px;
}
- .testing-container .compare-delta-cell-val {
+ .dispatch-container .compare-delta-cell-val {
font-size: 13px;
}
- .testing-container .compare-delta-cell-unit {
+ .dispatch-container .compare-delta-cell-unit {
font-size: 8px;
}
- .testing-container .compare-delta-cell-sub {
+ .dispatch-container .compare-delta-cell-sub {
font-size: 9px;
margin-top: 1px;
}
@@ -5726,154 +5883,188 @@
/* ── Laptop Height Tuning (max-height: 750px) ── */
@media (max-height: 750px) {
/* Let's shrink header rows even further on short screen heights */
- .testing-container #hdr {
+ .dispatch-container #hdr {
height: 42px;
}
- .testing-container #strat-row {
+ .dispatch-container #strat-row {
height: 34px;
}
- .testing-container #batch-row {
+ .dispatch-container #batch-row {
padding: 4px 16px;
}
/* Dual-map Compare Mode Header compression */
- .testing-container .compare-header-v2 {
+ .dispatch-container .compare-header-v2 {
padding: 8px 12px 6px;
gap: 6px;
}
- .testing-container .compare-header-row .compare-title {
+ .dispatch-container .compare-header-row .compare-title {
font-size: 13px;
gap: 8px;
}
- .testing-container .compare-title-dot {
+ .dispatch-container .compare-title-dot {
width: 8px;
height: 8px;
}
- .testing-container .compare-title-badge {
+ .dispatch-container .compare-title-badge {
padding: 2px 6px;
font-size: 9px;
}
- .testing-container .compare-overall-btn,
- .testing-container .compare-sync-toggle {
+ .dispatch-container .compare-overall-btn,
+ .dispatch-container .compare-sync-toggle {
padding: 4px 8px;
font-size: 10px;
gap: 4px;
}
- .testing-container .compare-overall-btn svg,
- .testing-container .compare-sync-toggle svg {
+ .dispatch-container .compare-overall-btn svg,
+ .dispatch-container .compare-sync-toggle svg {
font-size: 12px;
}
/* Compare Mode Step Timeline Compression */
- .testing-container .compare-timeline-wrap {
+ .dispatch-container .compare-timeline-wrap {
gap: 4px;
}
- .testing-container .compare-timeline {
+ .dispatch-container .compare-timeline-container {
+ padding: 6px 10px;
+ gap: 12px;
+ border-radius: 8px;
+ }
+ .dispatch-container .compare-timeline-labels {
+ padding-right: 10px;
+ gap: 12px;
+ border-right-width: 1px;
+ }
+ .dispatch-container .compare-timeline-label {
+ font-size: 9px;
+ height: 24px; /* aligns with 24px circle height */
+ }
+ .dispatch-container .compare-timeline-scrollable {
+ flex: 1;
+ overflow-x: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
padding-bottom: 2px;
}
- .testing-container .compare-step {
+
+ /* Planned track overrides to align vertically centered since there are no ticks */
+ .dispatch-container .compare-timeline-track.is-planned .compare-step {
+ height: 24px;
+ }
+ .dispatch-container .compare-timeline-track.is-planned .compare-step-spacer {
+ margin-bottom: 0;
+ align-self: center;
+ }
+
+ /* Actual track overrides for the spacer alignment */
+ .dispatch-container .compare-timeline-track.is-actual .compare-step-spacer {
+ margin-bottom: 14px; /* Centers spacer dynamically relative to the 24px circle */
+ }
+
+ .dispatch-container .compare-step {
gap: 4px; /* Reduced from 11px to bring timeline elements tighter */
}
- .testing-container .compare-step-circle {
+ .dispatch-container .compare-step-circle {
width: 24px;
height: 24px;
font-size: 10px;
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.1), 0 0 0 1px rgba(255, 255, 255, 0.6);
}
- .testing-container .compare-step.is-focused .compare-step-circle {
+ .dispatch-container .compare-step.is-focused .compare-step-circle {
transform: scale(1.1);
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.15),
0 0 0 2px #fff,
0 0 0 3px var(--step-color, #6366f1);
}
- .testing-container .compare-step-spacer {
+ .dispatch-container .compare-step-spacer {
width: 10px;
margin-bottom: 14px; /* Shift spacer dynamically up to align with 24px circles */
}
- .testing-container .compare-step-tick {
+ .dispatch-container .compare-step-tick {
font-size: 9px;
}
/* Progress Strip inside Timeline */
- .testing-container .compare-progress-strip {
+ .dispatch-container .compare-progress-strip {
margin-top: 2px;
}
- .testing-container .compare-progress-text {
+ .dispatch-container .compare-progress-text {
font-size: 9px;
}
/* Legend compacting */
- .testing-container .compare-legend {
+ .dispatch-container .compare-legend {
padding-top: 2px;
margin-top: 0;
gap: 8px;
}
- .testing-container .compare-legend-item {
+ .dispatch-container .compare-legend-item {
font-size: 9px;
}
- .testing-container .compare-legend-swatch {
+ .dispatch-container .compare-legend-swatch {
width: 10px;
height: 10px;
}
- .testing-container .compare-legend-note {
+ .dispatch-container .compare-legend-note {
display: none; /* Hide verbose Kalman note on short screens */
}
/* Bottom Delta Card panel compression */
- .testing-container .compare-delta {
+ .dispatch-container .compare-delta {
padding: 8px 12px;
gap: 6px;
}
- .testing-container .compare-delta-title {
+ .dispatch-container .compare-delta-title {
margin-bottom: 4px;
gap: 8px;
}
- .testing-container .compare-delta-step-badge {
+ .dispatch-container .compare-delta-step-badge {
width: 20px;
height: 20px;
font-size: 11px;
}
- .testing-container .compare-delta-title-main {
+ .dispatch-container .compare-delta-title-main {
font-size: 12px;
}
- .testing-container .compare-delta-title-sub {
+ .dispatch-container .compare-delta-title-sub {
font-size: 9px;
}
- .testing-container .compare-delta-status {
+ .dispatch-container .compare-delta-status {
padding: 2px 6px;
font-size: 9px;
}
/* Delta Grid - cells and labels */
- .testing-container .compare-delta-grid {
+ .dispatch-container .compare-delta-grid {
gap: 6px;
}
- .testing-container .compare-delta-cell {
+ .dispatch-container .compare-delta-cell {
padding: 5px 8px 4px;
border-radius: 8px;
}
- .testing-container .compare-delta-cell-label {
+ .dispatch-container .compare-delta-cell-label {
font-size: 9px;
margin-bottom: 1px;
}
- .testing-container .compare-delta-cell-val {
+ .dispatch-container .compare-delta-cell-val {
font-size: 13px;
}
- .testing-container .compare-delta-cell-unit {
+ .dispatch-container .compare-delta-cell-unit {
font-size: 8px;
}
- .testing-container .compare-delta-cell-sub {
+ .dispatch-container .compare-delta-cell-sub {
font-size: 9px;
margin-top: 1px;
}
}
/* Day summary toggle styles */
-.testing-container .compare-delta.is-collapsible.is-collapsed {
+.dispatch-container .compare-delta.is-collapsible.is-collapsed {
padding-bottom: 12px;
}
-.testing-container .compare-delta-toggle-icon {
+.dispatch-container .compare-delta-toggle-icon {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -5881,24 +6072,1148 @@
font-size: 18px;
transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1), color 0.15s ease;
}
-.testing-container .compare-delta-title:hover .compare-delta-toggle-icon {
+.dispatch-container .compare-delta-title:hover .compare-delta-toggle-icon {
color: var(--accent, #6366f1);
}
@media (max-width: 1366px) {
- .testing-container .compare-delta.is-collapsible.is-collapsed {
+ .dispatch-container .compare-delta.is-collapsible.is-collapsed {
padding-bottom: 8px;
}
- .testing-container .compare-delta-toggle-icon {
+ .dispatch-container .compare-delta-toggle-icon {
font-size: 15px;
}
}
@media (max-height: 750px) {
- .testing-container .compare-delta.is-collapsible.is-collapsed {
+ .dispatch-container .compare-delta.is-collapsible.is-collapsed {
padding-bottom: 8px;
}
- .testing-container .compare-delta-toggle-icon {
+ .dispatch-container .compare-delta-toggle-icon {
font-size: 15px;
}
+}
+
+/* ============================================================
+ Compare screen — new layout (takes over the body when Compare
+ is open). 50% left column is a vertical stack: actual map on
+ top, planned map on bottom. 50% right column is a scrollable
+ data panel with deviations, per-step correctness, delivery
+ times and profit.
+ ============================================================ */
+.dispatch-container #body.compare-mode {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(360px, 440px);
+ grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
+ gap: 12px;
+ padding: 12px;
+ background: linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
+}
+
+.dispatch-container #body.compare-mode #sidebar,
+.dispatch-container #body.compare-mode .sidebar-toggle-tab {
+ display: none !important;
+}
+
+/* Explicit grid placement — the actual GPS map (#compare-map-wrap) takes
+ the TOP-LEFT cell, the planned route map (#map-wrap) takes the
+ BOTTOM-LEFT cell, and the data panel spans the entire right column.
+ We use grid-row/grid-column rather than grid-template-areas so the
+ placement can't be quietly broken by a later override of the parent's
+ areas list. */
+.dispatch-container #body.compare-mode #compare-map-wrap {
+ grid-column: 1;
+ grid-row: 1;
+ flex: none;
+ min-width: 0;
+ min-height: 0;
+ margin: 0;
+ border-radius: 14px;
+ box-shadow: 0 6px 24px rgba(15, 23, 42, 0.08),
+ 0 0 0 1px rgba(15, 23, 42, 0.06);
+}
+
+.dispatch-container #body.compare-mode #map-wrap,
+.dispatch-container #body.compare-mode #map-wrap.compare-split {
+ grid-column: 1;
+ grid-row: 2;
+ flex: none;
+ min-width: 0;
+ min-height: 0;
+ margin: 0;
+ border-radius: 14px;
+ border-right: 0;
+ box-shadow: 0 6px 24px rgba(15, 23, 42, 0.08),
+ 0 0 0 1px rgba(15, 23, 42, 0.06);
+ overflow: hidden;
+}
+
+.dispatch-container #body.compare-mode .compare-planned-label {
+ background: rgba(255, 255, 255, 0.98);
+}
+
+/* Data panel ------------------------------------------------- */
+.dispatch-container .compare-data-panel {
+ grid-column: 2;
+ grid-row: 1 / 3;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ min-height: 0;
+ background: #ffffff;
+ border-radius: 14px;
+ box-shadow: 0 6px 24px rgba(15, 23, 42, 0.08),
+ 0 0 0 1px rgba(15, 23, 42, 0.06);
+ overflow: hidden;
+ animation: compare-slide-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.dispatch-container .cdp-head {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 16px;
+ border-bottom: 1px solid rgba(15, 23, 42, 0.08);
+ background: linear-gradient(135deg, #6366f1 0%, #3b82f6 100%);
+ color: #fff;
+ flex-shrink: 0;
+}
+
+.dispatch-container .cdp-head-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex: 1;
+ min-width: 0;
+}
+
+.dispatch-container .cdp-rider-dot {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4);
+}
+
+.dispatch-container .cdp-head-text {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.dispatch-container .cdp-rider-name {
+ font-size: 15px;
+ font-weight: 800;
+ color: #fff;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ letter-spacing: 0.01em;
+}
+
+.dispatch-container .cdp-head-badge {
+ font-size: 10px;
+ font-weight: 800;
+ letter-spacing: 0.1em;
+ color: rgba(255, 255, 255, 0.85);
+ text-transform: uppercase;
+}
+
+.dispatch-container .cdp-close {
+ width: 32px;
+ height: 32px;
+ border: 0;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.18);
+ color: #fff;
+ font-size: 18px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: background 0.15s ease, transform 0.15s ease;
+}
+.dispatch-container .cdp-close:hover {
+ background: rgba(255, 255, 255, 0.32);
+ transform: rotate(90deg);
+}
+
+.dispatch-container .cdp-scroll {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 14px 14px 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.dispatch-container .cdp-scroll::-webkit-scrollbar { width: 8px; }
+.dispatch-container .cdp-scroll::-webkit-scrollbar-track { background: transparent; }
+.dispatch-container .cdp-scroll::-webkit-scrollbar-thumb {
+ background: rgba(15, 23, 42, 0.14);
+ border-radius: 999px;
+}
+
+.dispatch-container .cdp-section {
+ background: #f8fafc;
+ border: 1px solid rgba(15, 23, 42, 0.06);
+ border-radius: 12px;
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.dispatch-container .cdp-section-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 11px;
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #475569;
+}
+
+.dispatch-container .cdp-section-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ border-radius: 6px;
+ background: rgba(99, 102, 241, 0.12);
+ color: #4338ca;
+ font-size: 14px;
+}
+
+.dispatch-container .cdp-section-icon.cdp-icon-warn {
+ background: rgba(239, 68, 68, 0.12);
+ color: #dc2626;
+}
+
+.dispatch-container .cdp-section-title { flex: 1; min-width: 0; }
+.dispatch-container .cdp-section-sub {
+ font-size: 10px;
+ font-weight: 700;
+ color: #94a3b8;
+ letter-spacing: 0.04em;
+ text-transform: none;
+}
+
+.dispatch-container .cdp-section-clear {
+ border: 0;
+ background: rgba(99, 102, 241, 0.1);
+ color: #4338ca;
+ padding: 3px 9px;
+ border-radius: 999px;
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ cursor: pointer;
+ text-transform: none;
+}
+.dispatch-container .cdp-section-clear:hover {
+ background: rgba(99, 102, 241, 0.2);
+}
+
+/* Day overview tiles ---------------------------------------- */
+.dispatch-container .cdp-tiles {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.dispatch-container .cdp-tile {
+ background: #fff;
+ border: 1px solid rgba(15, 23, 42, 0.07);
+ border-radius: 10px;
+ padding: 10px 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+.dispatch-container .cdp-tile:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06);
+}
+
+.dispatch-container .cdp-tile.is-warn {
+ background: linear-gradient(180deg, #fff7ed 0%, #fff 100%);
+ border-color: rgba(249, 115, 22, 0.25);
+}
+
+.dispatch-container .cdp-tile.is-loss {
+ background: linear-gradient(180deg, #fef2f2 0%, #fff 100%);
+ border-color: rgba(239, 68, 68, 0.25);
+}
+
+.dispatch-container .cdp-tile.is-gain {
+ background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
+ border-color: rgba(16, 185, 129, 0.25);
+}
+
+.dispatch-container .cdp-tile-label {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 10px;
+ font-weight: 800;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: #64748b;
+}
+.dispatch-container .cdp-tile-label svg {
+ font-size: 13px;
+}
+
+.dispatch-container .cdp-tile-value {
+ font-size: 20px;
+ font-weight: 800;
+ color: #0f172a;
+ line-height: 1.1;
+ letter-spacing: -0.01em;
+}
+.dispatch-container .cdp-tile-value.is-over { color: #dc2626; }
+.dispatch-container .cdp-tile-value.is-under { color: #16a34a; }
+
+.dispatch-container .cdp-tile-unit {
+ font-size: 12px;
+ font-weight: 700;
+ color: #94a3b8;
+ margin-left: 2px;
+}
+
+.dispatch-container .cdp-tile-sub {
+ font-size: 11px;
+ font-weight: 600;
+ color: #94a3b8;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Deviations list ------------------------------------------- */
+.dispatch-container .cdp-dev-list,
+.dispatch-container .cdp-step-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.dispatch-container .cdp-dev-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+ background: #fff;
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ border-radius: 10px;
+ cursor: pointer;
+ transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
+}
+.dispatch-container .cdp-dev-item:hover {
+ transform: translateX(2px);
+ box-shadow: 0 3px 10px rgba(239, 68, 68, 0.12);
+}
+.dispatch-container .cdp-dev-item.is-focused {
+ border-color: rgba(239, 68, 68, 0.6);
+ background: #fef2f2;
+}
+
+.dispatch-container .cdp-dev-num {
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-size: 12px;
+ font-weight: 800;
+ flex-shrink: 0;
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.6),
+ 0 1px 3px rgba(15, 23, 42, 0.15);
+}
+
+.dispatch-container .cdp-dev-body {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.dispatch-container .cdp-dev-title {
+ font-size: 12px;
+ font-weight: 700;
+ color: #0f172a;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dispatch-container .cdp-dev-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.dispatch-container .cdp-dev-chip {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 7px;
+ font-size: 10px;
+ font-weight: 700;
+ border-radius: 999px;
+ background: rgba(239, 68, 68, 0.1);
+ color: #dc2626;
+}
+.dispatch-container .cdp-dev-chip.is-over {
+ background: rgba(239, 68, 68, 0.12);
+ color: #dc2626;
+}
+
+/* Per-step list --------------------------------------------- */
+.dispatch-container .cdp-step {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 10px;
+ background: #fff;
+ border: 1px solid rgba(15, 23, 42, 0.07);
+ border-radius: 10px;
+ cursor: pointer;
+ transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
+}
+.dispatch-container .cdp-step:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
+ border-color: rgba(99, 102, 241, 0.3);
+}
+
+.dispatch-container .cdp-step.is-focused {
+ border-color: rgba(99, 102, 241, 0.65);
+ background: linear-gradient(180deg, #eef2ff 0%, #fff 100%);
+ box-shadow: 0 4px 14px rgba(99, 102, 241, 0.18);
+}
+
+.dispatch-container .cdp-step.is-correct {
+ border-color: rgba(16, 185, 129, 0.25);
+}
+
+.dispatch-container .cdp-step.is-anomaly {
+ border-color: rgba(239, 68, 68, 0.4);
+ background: linear-gradient(180deg, #fef2f2 0%, #fff 100%);
+}
+
+.dispatch-container .cdp-step.is-skipped {
+ opacity: 0.65;
+}
+
+.dispatch-container .cdp-step.is-loading {
+ opacity: 0.7;
+}
+
+.dispatch-container .cdp-step-num {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-size: 13px;
+ font-weight: 800;
+ flex-shrink: 0;
+ position: relative;
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.6),
+ 0 1px 3px rgba(15, 23, 42, 0.15);
+}
+
+.dispatch-container .cdp-step-check,
+.dispatch-container .cdp-step-flag {
+ position: absolute;
+ bottom: -3px;
+ right: -3px;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #fff;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.2);
+}
+.dispatch-container .cdp-step-check { color: #16a34a; }
+.dispatch-container .cdp-step-flag { color: #dc2626; }
+
+.dispatch-container .cdp-step-body {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.dispatch-container .cdp-step-title-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+
+.dispatch-container .cdp-step-title {
+ font-size: 13px;
+ font-weight: 700;
+ color: #0f172a;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ min-width: 0;
+}
+
+.dispatch-container .cdp-step-status {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 7px;
+ font-size: 9px;
+ font-weight: 800;
+ border-radius: 999px;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ flex-shrink: 0;
+}
+
+.dispatch-container .cdp-step-sub {
+ font-size: 11px;
+ font-weight: 600;
+ color: #94a3b8;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dispatch-container .cdp-step-deltas {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px 10px;
+ margin-top: 2px;
+}
+
+.dispatch-container .cdp-step-delta {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ font-size: 11px;
+ font-weight: 700;
+ color: #475569;
+}
+.dispatch-container .cdp-step-delta svg {
+ font-size: 13px;
+ color: #94a3b8;
+}
+.dispatch-container .cdp-step-delta small {
+ font-size: 10px;
+ font-weight: 700;
+ color: #94a3b8;
+}
+.dispatch-container .cdp-step-delta small.is-over { color: #dc2626; }
+.dispatch-container .cdp-step-delta.is-over { color: #dc2626; }
+.dispatch-container .cdp-step-delta.is-over svg { color: #dc2626; }
+.dispatch-container .cdp-step-delta.is-under { color: #16a34a; }
+.dispatch-container .cdp-step-delta.is-under svg { color: #16a34a; }
+
+/* Responsive — narrow screens collapse to a single column with the
+ data panel below the two maps so the maps stay usable on tablets. */
+@media (max-width: 1100px) {
+ .dispatch-container #body.compare-mode {
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-rows: minmax(220px, 1fr) minmax(220px, 1fr) auto;
+ }
+ .dispatch-container #body.compare-mode #compare-map-wrap {
+ grid-column: 1;
+ grid-row: 1;
+ }
+ .dispatch-container #body.compare-mode #map-wrap,
+ .dispatch-container #body.compare-mode #map-wrap.compare-split {
+ grid-column: 1;
+ grid-row: 2;
+ }
+ .dispatch-container .compare-data-panel {
+ grid-column: 1;
+ grid-row: 3;
+ max-height: 50vh;
+ }
+}
+
+/* Hide filter chrome when Compare takes over the screen — view-mode
+ tabs (#strat-row) and the slot picker (#batch-row) would clutter
+ the dedicated compare view, and the right-side data panel already
+ carries everything the operator needs. */
+.dispatch-container.compare-open #strat-row,
+.dispatch-container.compare-open #batch-row {
+ display: none !important;
+}
+
+/* Compliance score gauge ------------------------------------ */
+.dispatch-container .cdp-score-section {
+ background: linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%);
+ border-color: rgba(99, 102, 241, 0.18);
+ padding: 14px;
+}
+
+.dispatch-container .cdp-score-wrap {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.dispatch-container .cdp-score-ring {
+ width: 84px;
+ height: 84px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4px 14px rgba(15, 23, 42, 0.1);
+ transition: background 0.5s ease;
+}
+
+.dispatch-container .cdp-score-inner {
+ width: 70px;
+ height: 70px;
+ border-radius: 50%;
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1px;
+}
+
+.dispatch-container .cdp-score-value {
+ font-size: 26px;
+ font-weight: 900;
+ line-height: 1;
+ letter-spacing: -0.02em;
+}
+
+.dispatch-container .cdp-score-unit {
+ font-size: 10px;
+ font-weight: 700;
+ color: #94a3b8;
+ letter-spacing: 0.05em;
+}
+
+.dispatch-container .cdp-score-body {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.dispatch-container .cdp-score-label {
+ font-size: 11px;
+ font-weight: 800;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.dispatch-container .cdp-score-title {
+ font-size: 15px;
+ font-weight: 800;
+ color: #0f172a;
+ letter-spacing: -0.01em;
+}
+
+.dispatch-container .cdp-score-sub {
+ font-size: 11px;
+ font-weight: 600;
+ color: #64748b;
+}
+
+/* KPI row ---------------------------------------------------- */
+.dispatch-container .cdp-kpi-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
+ gap: 8px;
+}
+
+.dispatch-container .cdp-kpi {
+ background: #fff;
+ border: 1px solid rgba(15, 23, 42, 0.07);
+ border-radius: 9px;
+ padding: 8px 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.dispatch-container .cdp-kpi-label {
+ font-size: 9px;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: #94a3b8;
+}
+
+.dispatch-container .cdp-kpi-value {
+ font-size: 15px;
+ font-weight: 800;
+ color: #0f172a;
+ letter-spacing: -0.01em;
+}
+
+.dispatch-container .cdp-kpi-unit {
+ font-size: 10px;
+ font-weight: 700;
+ color: #94a3b8;
+ margin-left: 2px;
+}
+
+/* Highlights (best / worst) --------------------------------- */
+.dispatch-container .cdp-highlights {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.dispatch-container .cdp-highlight {
+ display: flex;
+ gap: 10px;
+ padding: 10px;
+ border-radius: 10px;
+ background: #fff;
+ border: 1px solid rgba(15, 23, 42, 0.07);
+ cursor: pointer;
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+.dispatch-container .cdp-highlight:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08);
+}
+.dispatch-container .cdp-highlight.is-best {
+ background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
+ border-color: rgba(16, 185, 129, 0.3);
+}
+.dispatch-container .cdp-highlight.is-worst {
+ background: linear-gradient(180deg, #fef2f2 0%, #fff 100%);
+ border-color: rgba(239, 68, 68, 0.3);
+}
+
+.dispatch-container .cdp-highlight-num {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-size: 12px;
+ font-weight: 800;
+ flex-shrink: 0;
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.7),
+ 0 1px 3px rgba(15, 23, 42, 0.15);
+}
+
+.dispatch-container .cdp-highlight-body {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.dispatch-container .cdp-highlight-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 9px;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+.dispatch-container .cdp-highlight.is-best .cdp-highlight-label { color: #16a34a; }
+.dispatch-container .cdp-highlight.is-worst .cdp-highlight-label { color: #dc2626; }
+
+.dispatch-container .cdp-highlight-title {
+ font-size: 12px;
+ font-weight: 700;
+ color: #0f172a;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dispatch-container .cdp-highlight-meta {
+ font-size: 11px;
+ font-weight: 600;
+ color: #64748b;
+}
+
+/* Trips breakdown ------------------------------------------- */
+.dispatch-container .cdp-trips {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.dispatch-container .cdp-trip {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 10px 12px;
+ background: #fff;
+ border: 1px solid rgba(15, 23, 42, 0.07);
+ border-left: 3px solid rgba(99, 102, 241, 0.6);
+ border-radius: 10px;
+}
+
+.dispatch-container .cdp-trip-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.dispatch-container .cdp-trip-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 8px;
+ font-size: 10px;
+ font-weight: 800;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ border-radius: 999px;
+ background: rgba(99, 102, 241, 0.12);
+ color: #4338ca;
+}
+
+.dispatch-container .cdp-trip-meta {
+ font-size: 11px;
+ font-weight: 700;
+ color: #64748b;
+}
+
+.dispatch-container .cdp-trip-stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px 14px;
+}
+
+.dispatch-container .cdp-trip-stats span {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ font-weight: 700;
+ color: #475569;
+}
+.dispatch-container .cdp-trip-stats span svg {
+ font-size: 13px;
+ color: #94a3b8;
+}
+.dispatch-container .cdp-trip-stats span small {
+ font-size: 10px;
+ font-weight: 700;
+ color: #94a3b8;
+}
+.dispatch-container .cdp-trip-stats span.is-over {
+ color: #dc2626;
+}
+.dispatch-container .cdp-trip-stats span.is-over svg {
+ color: #dc2626;
+}
+
+/* Route sequence comparison -------------------------------- */
+.dispatch-container .cdp-section-head-clickable {
+ cursor: pointer;
+ user-select: none;
+}
+.dispatch-container .cdp-section-head-clickable:hover .cdp-section-title {
+ color: #4338ca;
+}
+
+.dispatch-container .cdp-seq-status {
+ font-size: 10px;
+ font-weight: 800;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ padding: 2px 8px;
+ border-radius: 999px;
+ flex-shrink: 0;
+}
+.dispatch-container .cdp-seq-status.is-good {
+ background: rgba(16, 185, 129, 0.12);
+ color: #16a34a;
+}
+.dispatch-container .cdp-seq-status.is-warn {
+ background: rgba(239, 68, 68, 0.12);
+ color: #dc2626;
+}
+
+.dispatch-container .cdp-seq-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ color: #94a3b8;
+ transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+}
+.dispatch-container .cdp-seq-toggle.is-open {
+ transform: rotate(180deg);
+ color: #4338ca;
+}
+
+.dispatch-container .cdp-seq {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ background: #fff;
+ border-radius: 10px;
+ border: 1px solid rgba(15, 23, 42, 0.07);
+ padding: 12px;
+ animation: cdp-seq-in 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+@keyframes cdp-seq-in {
+ from { opacity: 0; transform: translateY(-3px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.dispatch-container .cdp-seq-diffs {
+ list-style: none;
+ margin: 4px 0 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.dispatch-container .cdp-seq-diff {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+ border-radius: 8px;
+ background: linear-gradient(180deg, #fef2f2 0%, #fff 100%);
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ cursor: pointer;
+ transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
+}
+.dispatch-container .cdp-seq-diff:hover {
+ transform: translateX(2px);
+ box-shadow: 0 3px 10px rgba(239, 68, 68, 0.12);
+}
+.dispatch-container .cdp-seq-diff.is-focused {
+ border-color: rgba(239, 68, 68, 0.6);
+ box-shadow: 0 3px 12px rgba(239, 68, 68, 0.2);
+}
+
+.dispatch-container .cdp-seq-diff-num {
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-size: 12px;
+ font-weight: 800;
+ flex-shrink: 0;
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.7),
+ 0 1px 3px rgba(15, 23, 42, 0.15);
+}
+
+.dispatch-container .cdp-seq-diff-body {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.dispatch-container .cdp-seq-diff-title {
+ font-size: 12px;
+ font-weight: 700;
+ color: #0f172a;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dispatch-container .cdp-seq-diff-sub {
+ font-size: 11px;
+ font-weight: 600;
+ color: #64748b;
+}
+
+.dispatch-container .cdp-seq-diff-tag {
+ font-size: 11px;
+ font-weight: 800;
+ padding: 3px 8px;
+ border-radius: 999px;
+ background: rgba(239, 68, 68, 0.12);
+ color: #dc2626;
+ flex-shrink: 0;
+}
+
+.dispatch-container .cdp-seq-good {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ font-weight: 700;
+ color: #16a34a;
+ padding: 6px 10px;
+ background: rgba(16, 185, 129, 0.08);
+ border-radius: 8px;
+}
+.dispatch-container .cdp-seq-good svg {
+ font-size: 16px;
+}
+
+/* Cascade-aware sequence diff groups — when N consecutive shifted steps
+ share the same delta, they collapse into one summary card. Click expands
+ the card to reveal its individual diff rows, indented under the group. */
+.dispatch-container .cdp-seq-diff.is-group {
+ background: linear-gradient(180deg, #eef2ff 0%, #fff 100%);
+ border-color: rgba(99, 102, 241, 0.3);
+ position: relative;
+ padding-right: 40px;
+}
+.dispatch-container .cdp-seq-diff.is-group:hover {
+ border-color: rgba(99, 102, 241, 0.55);
+ box-shadow: 0 3px 10px rgba(99, 102, 241, 0.15);
+}
+.dispatch-container .cdp-seq-diff.is-group.is-expanded {
+ border-color: rgba(99, 102, 241, 0.6);
+ background: linear-gradient(180deg, #e0e7ff 0%, #f5f7ff 100%);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.dispatch-container .cdp-seq-group-num {
+ position: relative;
+ width: 30px;
+ height: 30px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+.dispatch-container .cdp-seq-group-num-bg {
+ position: absolute;
+ inset: 0;
+ border-radius: 8px;
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.7),
+ 0 1px 3px rgba(15, 23, 42, 0.15);
+}
+.dispatch-container .cdp-seq-group-num-label {
+ position: relative;
+ color: #fff;
+ font-size: 11px;
+ font-weight: 800;
+ letter-spacing: -0.01em;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
+}
+
+.dispatch-container .cdp-seq-group-delta {
+ display: inline-flex;
+ align-items: center;
+ padding: 1px 7px;
+ margin-left: 4px;
+ font-size: 11px;
+ font-weight: 800;
+ border-radius: 999px;
+ background: rgba(99, 102, 241, 0.15);
+ color: #4338ca;
+ vertical-align: 1px;
+}
+
+.dispatch-container .cdp-seq-group-toggle {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ border-radius: 6px;
+ color: #4338ca;
+ background: rgba(255, 255, 255, 0.7);
+ font-size: 18px;
+ transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+ pointer-events: none;
+}
+.dispatch-container .cdp-seq-group-toggle.is-open {
+ transform: translateY(-50%) rotate(180deg);
+ background: rgba(99, 102, 241, 0.18);
+}
+
+/* Children container — wraps a nested so list semantics stay valid
+ (we render an wrapper around the nested list per group). */
+.dispatch-container .cdp-seq-group-children-wrap {
+ list-style: none;
+ margin: -2px 0 0;
+ padding: 0;
+ background: rgba(99, 102, 241, 0.05);
+ border-left: 2px solid rgba(99, 102, 241, 0.35);
+ border-right: 1px solid rgba(99, 102, 241, 0.2);
+ border-bottom: 1px solid rgba(99, 102, 241, 0.2);
+ border-bottom-left-radius: 10px;
+ border-bottom-right-radius: 10px;
+ margin-left: 4px;
+ animation: cdp-seq-group-in 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+@keyframes cdp-seq-group-in {
+ from { opacity: 0; transform: translateY(-3px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.dispatch-container .cdp-seq-group-children {
+ list-style: none;
+ margin: 0;
+ padding: 8px 8px 8px 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+/* Nested diff rows inside an expanded group — smaller, tighter, with a
+ subtle left rail so they read as belonging to the group above. */
+.dispatch-container .cdp-seq-diff.is-nested {
+ padding: 6px 8px;
+ background: #fff;
+ border-color: rgba(99, 102, 241, 0.15);
+ border-radius: 8px;
+}
+.dispatch-container .cdp-seq-diff.is-nested .cdp-seq-diff-num {
+ width: 22px;
+ height: 22px;
+ font-size: 11px;
+}
+.dispatch-container .cdp-seq-diff.is-nested .cdp-seq-diff-title {
+ font-size: 11.5px;
+}
+.dispatch-container .cdp-seq-diff.is-nested .cdp-seq-diff-sub {
+ font-size: 10.5px;
+}
+.dispatch-container .cdp-seq-diff.is-nested .cdp-seq-diff-tag {
+ font-size: 10px;
+ padding: 2px 6px;
}
\ No newline at end of file
diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js
index 9a1cfae..220cd30 100644
--- a/src/pages/nearle/dispatch/Dispatch.js
+++ b/src/pages/nearle/dispatch/Dispatch.js
@@ -35,29 +35,29 @@ import {
MdSearch,
MdChevronLeft,
MdChevronRight,
- MdLocalMall
+ MdLocalMall,
+ MdCheckCircle,
+ MdErrorOutline,
+ MdWarning,
+ MdClose,
+ MdFormatListBulleted,
+ MdTimer
} from 'react-icons/md';
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs } from '../../api/api';
+import {
+ STATUS_STYLES,
+ getStatusStyle,
+ FINAL_STATUSES,
+ SKIPPED_STATUSES,
+ STEP_PALETTE,
+ stepColor
+} from './dispatchShared';
+import CompareDataPanel from './CompareDataPanel';
import './Dispatch.css';
// Phosphor "motorcycle" (filled) — clean side-view bike that reads well at small sizes.
const MOTORBIKE_SVG = ` `;
-const STATUS_STYLES = {
- created: { label: 'Created', bg: '#3b82f6', fg: '#fff' },
- pending: { label: 'Pending', bg: '#f59e0b', fg: '#fff' },
- accepted: { label: 'Accepted', bg: '#8b5cf6', fg: '#fff' },
- arrived: { label: 'Arrived', bg: '#ea580c', fg: '#fff' },
- picked: { label: 'Picked', bg: '#0ea5e9', fg: '#fff' },
- active: { label: 'Active', bg: '#0ea5e9', fg: '#fff' },
- delivered: { label: 'Delivered', bg: '#22c55e', fg: '#fff' },
- skipped: { label: 'Skipped', bg: '#94a3b8', fg: '#fff' },
- cancelled: { label: 'Cancelled', bg: '#ef4444', fg: '#fff' }
-};
-
-const getStatusStyle = (status) =>
- STATUS_STYLES[String(status || '').toLowerCase()] || { label: status || 'Unknown', bg: '#64748b', fg: '#fff' };
-
const toNum = (v) => {
const n = parseFloat(v);
return Number.isFinite(n) ? n : NaN;
@@ -436,8 +436,6 @@ function splitPolylineByDrops(polyline, drops) {
return segments;
}
-const FINAL_STATUSES = new Set(['delivered']);
-const SKIPPED_STATUSES = new Set(['cancelled', 'skipped']);
// --- Marker popup helpers ---
// Strip the date portion from an API timestamp — operators viewing today's
@@ -571,31 +569,10 @@ L.Icon.Default.mergeOptions({
const RIDER_COLORS = ['#0055FF', '#00D82C', '#FF6B00', '#9D00FF', '#FF00A8', '#00C2B2', '#FF9900', '#FF0000'];
-// Per-step palette used by the Compare map (polylines, timeline dots, drop
-// pins, delta panel). Wider and more deliberately spaced around the hue
-// wheel than RIDER_COLORS so a 10-delivery rider day reads as 10 visibly
-// different lines — operators don't have to second-guess which polyline
-// is step 7 vs step 2. Tuned darker than RIDER_COLORS so the lines stay
-// legible on the light OSM tile background.
-const STEP_PALETTE = [
- '#2563eb', // blue-600
- '#dc2626', // red-600
- '#16a34a', // green-600
- '#ea580c', // orange-600
- '#9333ea', // purple-600
- '#0891b2', // cyan-600
- '#ca8a04', // yellow-600
- '#db2777', // pink-600
- '#0f766e', // teal-700
- '#7c3aed', // violet-600
- '#65a30d', // lime-600
- '#0284c7', // sky-600
- '#b91c1c', // red-700
- '#15803d', // green-700
- '#a16207', // yellow-700
- '#86198f' // fuchsia-800
-];
-const stepColor = (i) => STEP_PALETTE[((i % STEP_PALETTE.length) + STEP_PALETTE.length) % STEP_PALETTE.length];
+// STATUS_STYLES, getStatusStyle, FINAL_STATUSES, SKIPPED_STATUSES,
+// STEP_PALETTE, stepColor — moved to ./dispatchShared.js so the
+// extracted CompareDataPanel component can import them without forcing
+// a circular dependency on Dispatch.js.
const MapController = ({ focusedItem, viewMode, orders, kitchens, locationKey }) => {
const map = useMap();
@@ -973,7 +950,18 @@ const Dispatch = ({
// per-rider). Kept as a single state flag so we don't entangle it with
// viewMode/focused* logic.
const [compareOpen, setCompareOpen] = useState(false);
- const [daySummaryOpen, setDaySummaryOpen] = useState(false);
+ // Default-open toggle for the "Route sequence" section in the compare data
+ // panel — shows planned vs actual visit order with out-of-order steps flagged.
+ const [sequenceOpen, setSequenceOpen] = useState(true);
+ // Default-open toggle for the planned/actual timeline strip above the
+ // compare actual-map. Lets the operator collapse the dual step rows when
+ // they want maximum map real estate.
+ const [compareTimelineOpen, setCompareTimelineOpen] = useState(true);
+ // Set of route-sequence "diff group" indices that are currently expanded.
+ // Cascade-aware: when N consecutive steps share the same shift amount,
+ // they're collapsed into one summary row by default; clicking it adds the
+ // run's index to this set so the individual steps reveal.
+ const [expandedSeqGroups, setExpandedSeqGroups] = useState(() => new Set());
// In controlled mode the parent owns selectedRiderId, so handleRiderFocus only
// fires onRiderSelect — focusedRider won't update until the parent re-renders.
// This ref lets us defer setCompareOpen(true) until focusedRider is confirmed.
@@ -1010,6 +998,20 @@ const Dispatch = ({
const syncSourceRef = useRef(null);
const syncLastInputAtRef = useRef(0);
+ // Stable Canvas renderer for the planned (left) map. Leaflet's default
+ // canvas (used when `preferCanvas` is true) only draws ~10% beyond the
+ // viewport — the moment a drag carries a polyline edge outside that
+ // padding, the off-area pixels are blank until `moveend` triggers a
+ // redraw, which makes polylines look "broken" mid-drag. Bumping padding
+ // to 1.5 makes the canvas ~4× viewport area, so dragging within a screen
+ // of travel keeps every polyline drawn without re-raster gaps. The
+ // actual (right) map's renderer is created later, after focusedRider is
+ // in scope, since it remounts per rider.
+ const plannedMapRendererRef = useRef(null);
+ if (!plannedMapRendererRef.current) {
+ plannedMapRendererRef.current = L.canvas({ padding: 1.5, tolerance: 5 });
+ }
+
// Pull the partners/getriderlogs feed for the currently selected hub + date.
// This endpoint returns the exact live GPS position for every rider at the
// hub (latitude/longitude/logdate/status). We render those positions as
@@ -1335,6 +1337,16 @@ const Dispatch = ({
? (selectedRiderId ? (riders.find((r) => r.id === selectedRiderId) || null) : null)
: internalFocusedRider;
+ // Per-rider canvas renderer for the actual (right) map in Compare mode.
+ // The actual map remounts on rider change (via key={`compare-${rider.id}`})
+ // so we recreate the renderer in lock-step to keep its map binding fresh.
+ // Same padding as the planned map (1.5 → ~4× viewport area) so polylines
+ // don't visually break at the canvas edge during drag.
+ const actualMapRenderer = useMemo(
+ () => L.canvas({ padding: 1.5, tolerance: 5 }),
+ [focusedRider?.id]
+ );
+
// Single setter used by every interactive site in the UI. In uncontrolled mode it
// updates local state; in controlled mode it only notifies the parent.
const handleRiderFocus = useCallback(
@@ -1566,6 +1578,18 @@ const Dispatch = ({
return { plannedKm, actualKm, kmDeltaPct, anomalies, late, onTime };
}, [compareDeltas]);
+ const plannedOrdered = useMemo(() => {
+ return [...compareDeltas].sort(
+ (a, b) => (a.order?.step || a.sequenceStep) - (b.order?.step || b.sequenceStep)
+ );
+ }, [compareDeltas]);
+
+ const actualOrdered = useMemo(() => {
+ return [...compareDeltas].sort(
+ (a, b) => a.sequenceStep - b.sequenceStep
+ );
+ }, [compareDeltas]);
+
// When Compare is open and a step is focused, override MapController's
// focusedItem with a synthetic single-order item so the planned (left)
// map zooms onto the same delivery the operator is scrutinizing. Without
@@ -1587,7 +1611,7 @@ const Dispatch = ({
// stuck on a step that may not exist in their day.
useEffect(() => {
setFocusedCompareStep(null);
- setDaySummaryOpen(false);
+ setExpandedSeqGroups(new Set());
}, [compareOpen, focusedRider?.id]);
// Mirror pan/zoom from whichever map the user is driving. Called by
@@ -2446,7 +2470,7 @@ const Dispatch = ({
};
return (
-
+
{!embedded && (
@@ -3567,7 +3591,7 @@ const Dispatch = ({
scrollWheelZoom
style={{ height: '100%', width: '100%' }}
zoomControl={false}
- preferCanvas
+ renderer={plannedMapRendererRef.current}
inertia
inertiaDeceleration={2400}
inertiaMaxSpeed={2000}
@@ -3864,65 +3888,138 @@ const Dispatch = ({
{compareSyncEnabled ? : }
{compareSyncEnabled ? 'Sync' : 'Unlinked'}
+ setCompareTimelineOpen((v) => !v)}
+ title={compareTimelineOpen
+ ? 'Hide planned/actual timeline'
+ : 'Show planned/actual timeline'}
+ aria-expanded={compareTimelineOpen}
+ >
+
+
{/* Step timeline — every delivery as a tappable dot in chronological
order. The operator can drill into any step to scrutinize that
single delivery on both maps. Filled = delivered, ring = pending,
- dim = cancelled/skipped, ring with spinner = GPS still loading. */}
+ dim = cancelled/skipped, ring with spinner = GPS still loading.
+ Whole strip (timeline + progress + legend) collapses via the
+ header chevron — open by default. */}
+ {compareTimelineOpen && (
+ <>
-
- {compareDeltas.map((d, i) => {
- const statusLow = String(d.orderstatus || '').toLowerCase();
- const isDelivered = FINAL_STATUSES.has(statusLow);
- const isSkipped = SKIPPED_STATUSES.has(statusLow);
- const isFocused = focusedCompareStep === d.sequenceStep;
- const isLoading = d.isLoading && d.coordsCount === 0;
- const isNoData = !d.isLoading && d.coordsCount === 0;
- const color = stepColor(i);
- const cls = [
- 'compare-step',
- isFocused && 'is-focused',
- isDelivered && 'is-delivered',
- isSkipped && 'is-skipped',
- !isDelivered && !isSkipped && 'is-pending',
- isLoading && 'is-loading',
- isNoData && 'is-no-data',
- d.anomaly && 'is-anomaly'
- ].filter(Boolean).join(' ');
- return (
-
- {i > 0 && }
-
- setFocusedCompareStep((prev) =>
- prev === d.sequenceStep ? null : d.sequenceStep
- )
- }
- title={
- `Step ${d.sequenceStep}` +
- (d.deliverycustomer ? ` · ${d.deliverycustomer}` : '') +
- (d.actualTs ? ` · ${d.actualTs.format('hh:mm A')}` : '') +
- (d.anomaly ? ' · deviation flagged' : '')
- }
- >
-
- {isLoading ? : d.sequenceStep}
-
- {d.actualTs && (
-
- {d.actualTs.format('HH:mm')}
-
- )}
- {d.anomaly && }
-
-
- );
- })}
+
+
+
+ {/* Planned Row */}
+
+ {plannedOrdered.map((d, i) => {
+ const statusLow = String(d.orderstatus || '').toLowerCase();
+ const isDelivered = FINAL_STATUSES.has(statusLow);
+ const isSkipped = SKIPPED_STATUSES.has(statusLow);
+ const isFocused = focusedCompareStep === d.sequenceStep;
+ const isLoading = d.isLoading && d.coordsCount === 0;
+ const isNoData = !d.isLoading && d.coordsCount === 0;
+ const plannedStepNum = d.order?.step || d.sequenceStep;
+ const color = stepColor(plannedStepNum - 1);
+ const cls = [
+ 'compare-step',
+ isFocused && 'is-focused',
+ isDelivered && 'is-delivered',
+ isSkipped && 'is-skipped',
+ !isDelivered && !isSkipped && 'is-pending',
+ isLoading && 'is-loading',
+ isNoData && 'is-no-data',
+ d.anomaly && 'is-anomaly'
+ ].filter(Boolean).join(' ');
+ return (
+
+ {i > 0 && }
+
+ setFocusedCompareStep((prev) =>
+ prev === d.sequenceStep ? null : d.sequenceStep
+ )
+ }
+ title={
+ `Planned Step ${plannedStepNum}` +
+ (d.deliverycustomer ? ` · ${d.deliverycustomer}` : '') +
+ (d.anomaly ? ' · deviation flagged' : '')
+ }
+ >
+
+ {isLoading ? : plannedStepNum}
+
+
+
+ );
+ })}
+
+
+ {/* Actual Row */}
+
+ {actualOrdered.map((d, i) => {
+ const statusLow = String(d.orderstatus || '').toLowerCase();
+ const isDelivered = FINAL_STATUSES.has(statusLow);
+ const isSkipped = SKIPPED_STATUSES.has(statusLow);
+ const isFocused = focusedCompareStep === d.sequenceStep;
+ const isLoading = d.isLoading && d.coordsCount === 0;
+ const isNoData = !d.isLoading && d.coordsCount === 0;
+ const plannedStepNum = d.order?.step || d.sequenceStep;
+ const color = stepColor(plannedStepNum - 1);
+ const cls = [
+ 'compare-step',
+ isFocused && 'is-focused',
+ isDelivered && 'is-delivered',
+ isSkipped && 'is-skipped',
+ !isDelivered && !isSkipped && 'is-pending',
+ isLoading && 'is-loading',
+ isNoData && 'is-no-data',
+ d.anomaly && 'is-anomaly'
+ ].filter(Boolean).join(' ');
+ return (
+
+ {i > 0 && }
+
+ setFocusedCompareStep((prev) =>
+ prev === d.sequenceStep ? null : d.sequenceStep
+ )
+ }
+ title={
+ `Actual Visit ${i + 1} (Planned Step ${plannedStepNum})` +
+ (d.deliverycustomer ? ` · ${d.deliverycustomer}` : '') +
+ (d.actualTs ? ` · ${d.actualTs.format('hh:mm A')}` : '') +
+ (d.anomaly ? ' · deviation flagged' : '')
+ }
+ >
+
+ {isLoading ? : plannedStepNum}
+
+ {d.actualTs && (
+
+ {d.actualTs.format('HH:mm')}
+
+ )}
+ {d.anomaly && }
+
+
+ );
+ })}
+
+
@@ -3970,10 +4067,20 @@ const Dispatch = ({
);
})()}
+ >
+ )}
);
})()}
+ {/* Floating badge — mirrors the "Planned Route" pill on the
+ bottom map. Tells the operator at a glance that the top
+ map is the rider's actual GPS trail, not the planned
+ route. The pulsing dot reinforces "live data". */}
+
+
+ Actual GPS
+
- {/* Delta panel — when a step is focused, shows planned-vs-actual
- numbers for just that delivery (distance, route deviation %, time
- variance). When no step is focused, rolls up the same numbers
- across the whole day. Anomaly cells turn red when a step ran
- >25% longer than planned or arrived >15 min late. */}
- {(() => {
- const focused =
- focusedCompareStep != null
- ? compareDeltas.find((d) => d.sequenceStep === focusedCompareStep)
- : null;
-
- if (focused) {
- const color = stepColor(focused.sequenceStep - 1);
- const kmDeltaSign = focused.kmDelta >= 0 ? '+' : '';
- const kmDeltaCls = focused.anomaly
- ? 'is-over'
- : focused.kmDelta < -0.1
- ? 'is-under'
- : '';
- const timeDeltaCls =
- focused.timeDeltaMin != null
- ? focused.timeDeltaMin > 10
- ? 'is-over'
- : focused.timeDeltaMin < -2
- ? 'is-under'
- : ''
- : '';
- const statusStyle = getStatusStyle(focused.orderstatus);
- return (
-
-
-
- {focused.sequenceStep}
-
-
-
- {focused.deliverycustomer || `Step ${focused.sequenceStep}`}
-
-
- {focused.pickupcustomer ? `from ${focused.pickupcustomer} · ` : ''}
- Order #{focused.orderid}
-
-
- {focused.orderstatus && (
-
- {statusStyle.label}
-
- )}
-
-
-
- Distance
-
- {focused.actualKm.toFixed(2)}{' '}
- km
-
-
- planned {focused.plannedKm.toFixed(2)} km
-
-
-
- Δ Route
-
- {kmDeltaSign}
- {focused.kmDelta.toFixed(2)} km
-
-
- {focused.kmDeltaPct != null
- ? `${kmDeltaSign}${focused.kmDeltaPct.toFixed(0)}% vs plan`
- : 'no planned km'}
-
-
-
- Time
-
- {focused.timeDeltaMin != null
- ? `${focused.timeDeltaMin > 0 ? '+' : ''}${focused.timeDeltaMin} min`
- : '—'}
-
-
- {focused.actualTs && focused.expectedTs
- ? `${focused.actualTs.format('HH:mm')} vs ${focused.expectedTs.format('HH:mm')}`
- : focused.actualTs
- ? `delivered ${focused.actualTs.format('HH:mm')}`
- : 'in flight'}
-
-
-
-
- );
- }
-
- const sum = compareSummary;
- const deltaCls =
- sum.kmDeltaPct == null
- ? ''
- : sum.kmDeltaPct > 25
- ? 'is-over'
- : sum.kmDeltaPct < -5
- ? 'is-under'
- : '';
- const total = sum.onTime + sum.late;
- return (
-
-
setDaySummaryOpen((v) => !v)}
- style={{ cursor: 'pointer', userSelect: 'none' }}
- title={daySummaryOpen ? 'Collapse Day Summary' : 'Expand Day Summary'}
- >
-
-
-
-
-
- Day summary
-
-
-
-
-
- {daySummaryOpen
- ? 'Click to collapse summary'
- : 'Click to expand summary · Click any step above to scrutinize'}
-
-
-
- {daySummaryOpen && (
-
-
- Total distance
-
- {sum.actualKm.toFixed(1)}{' '}
- km
-
-
- planned {sum.plannedKm.toFixed(1)} km
-
-
-
0 ? ' is-anomaly' : ''}`}>
- Route deviation
-
- {sum.kmDeltaPct != null
- ? `${sum.kmDeltaPct > 0 ? '+' : ''}${sum.kmDeltaPct.toFixed(0)}%`
- : '—'}
-
-
- {sum.anomalies > 0
- ? `${sum.anomalies} step${sum.anomalies > 1 ? 's' : ''} flagged`
- : 'within plan'}
-
-
-
- On-time
-
- {sum.onTime}
- {total > 0 && (
- /{total}
- )}
-
-
- {sum.late > 0 ? `${sum.late} late` : 'all on schedule'}
-
-
-
- )}
-
- );
- })()}
-
)}
+
+ {/* Right-side data panel — full-height column in Compare mode. Renders
+ day-overview tiles (distance, deviation, on-time, profit), the
+ focused-step delta when a step is selected, a deviations list of
+ anomaly steps, and a per-step list showing whether each step was
+ followed correctly (delivered + within plan), the delivery time
+ vs expected, and the order profit. Clicking any step row mirrors
+ the timeline click — sets focusedCompareStep so both maps zoom
+ onto that delivery. */}
+ {compareOpen && focusedRider && (
+
setCompareOpen(false)}
+ />
+ )}
)}
diff --git a/src/pages/nearle/dispatch/dispatchShared.js b/src/pages/nearle/dispatch/dispatchShared.js
new file mode 100644
index 0000000..dfdc118
--- /dev/null
+++ b/src/pages/nearle/dispatch/dispatchShared.js
@@ -0,0 +1,65 @@
+// Shared constants and pure helpers for the Dispatch page and its
+// extracted sub-components (CompareDataPanel, etc.). Lives outside
+// Dispatch.js so we don't create a circular import between the host
+// component and the child views.
+
+// Status palette — single source of truth for the status pill colors
+// rendered on rider cards, order rows, step lists, and tooltips.
+export const STATUS_STYLES = {
+ created: { label: 'Created', bg: '#3b82f6', fg: '#fff' },
+ pending: { label: 'Pending', bg: '#f59e0b', fg: '#fff' },
+ accepted: { label: 'Accepted', bg: '#8b5cf6', fg: '#fff' },
+ arrived: { label: 'Arrived', bg: '#ea580c', fg: '#fff' },
+ picked: { label: 'Picked', bg: '#0ea5e9', fg: '#fff' },
+ active: { label: 'Active', bg: '#0ea5e9', fg: '#fff' },
+ delivered: { label: 'Delivered', bg: '#22c55e', fg: '#fff' },
+ skipped: { label: 'Skipped', bg: '#94a3b8', fg: '#fff' },
+ cancelled: { label: 'Cancelled', bg: '#ef4444', fg: '#fff' }
+};
+
+export const getStatusStyle = (status) =>
+ STATUS_STYLES[String(status || '').toLowerCase()] || {
+ label: status || 'Unknown',
+ bg: '#64748b',
+ fg: '#fff'
+ };
+
+// Order-status sets used for completion / skipped decisions across the
+// rider list, the planned-route renderer, and the compare data panel.
+export const FINAL_STATUSES = new Set(['delivered']);
+export const SKIPPED_STATUSES = new Set(['cancelled', 'skipped']);
+
+// Per-step palette — wider and more deliberately spaced than the rider
+// palette so a 10-stop day reads as 10 distinct colors on the compare
+// map's polylines + pins.
+export const STEP_PALETTE = [
+ '#2563eb', // blue-600
+ '#dc2626', // red-600
+ '#16a34a', // green-600
+ '#ea580c', // orange-600
+ '#9333ea', // purple-600
+ '#0891b2', // cyan-600
+ '#ca8a04', // yellow-600
+ '#db2777', // pink-600
+ '#0f766e', // teal-700
+ '#7c3aed', // violet-600
+ '#65a30d', // lime-600
+ '#0284c7', // sky-600
+ '#b91c1c', // red-700
+ '#15803d', // green-700
+ '#a16207', // yellow-700
+ '#86198f' // fuchsia-800
+];
+
+export const stepColor = (i) =>
+ STEP_PALETTE[((i % STEP_PALETTE.length) + STEP_PALETTE.length) % STEP_PALETTE.length];
+
+// Pure helper — converts 1, 2, 3, 21 → "1st", "2nd", "3rd", "21st". Used
+// by the compare data panel for the route-sequence diff list ("Visited
+// 4th · planned 2nd").
+export const ordinal = (n) => {
+ if (n == null) return '';
+ const s = ['th', 'st', 'nd', 'rd'];
+ const v = n % 100;
+ return n + (s[(v - 20) % 10] || s[v] || s[0]);
+};