@tailwind base; @tailwind components; @tailwind utilities; /* ============================================================= Cursor brand bindings — warm cream + warm brown + orange accent Overrides Tailwind's default orange scale so every `bg-orange-*` `text-orange-*` `border-orange-*` resolves to Cursor's tokens. ============================================================= */ :root { --foreground-rgb: 38, 37, 30; /* Cursor Dark #26251e */ --background-rgb: 242, 241, 237; /* Cursor Cream #f2f1ed */ --primary: 245, 78, 0; /* Cursor Orange #f54e00 */ --primary-light: 255, 122, 41; /* Cursor Orange tinted */ --success: 31, 138, 101; /* #1f8a65 */ --warning: 234, 179, 8; /* #eab308 */ --danger: 207, 45, 86; /* #cf2d56 */ --info: 159, 187, 224; /* #9fbbe0 */ --nav-height: 4rem; /* Cursor surface scale (from DESIGN.md §Color Palette) */ --surface-100: #f7f7f4; --surface-200: #f2f1ed; /* page bg */ --surface-300: #ebeae5; /* button default */ --surface-400: #e6e5e0; /* card */ --surface-500: #e1e0db; /* Tailwind orange-50..900 → Cursor warm scale. This is the global re-skin: every `bg-orange-600` etc. below resolves to a warm cream/brown, with `--primary` reserved for the single orange accent role (CTAs, active filter, link). */ --orange-50: #f7f7f4; /* surface 100 */ --orange-100: #ebeae5; /* surface 300 */ --orange-200: #e6e5e0; /* surface 400 */ --orange-300: #d6d4ce; /* cream darken */ --orange-400: #b8b5ac; /* muted warm */ --orange-500: #6b6760; /* secondary text */ --orange-600: #26251e; /* Cursor Dark (identity surface) */ --orange-700: #1a1914; /* hover over dark */ --orange-800: #1a1914; --orange-900: #0f0e0a; } /* Red and green are reserved for status (problem / success). Keep them at their Tailwind defaults — we just darken the lightest stops so the bg-* uses don't clash with the cream canvas. */ :root { --red-50: #fbeaef; --red-100: #f3c4d2; --red-500: #cf2d56; /* matches --danger */ --red-600: #b3224a; --red-700: #8a1b3a; --green-50: #e6f1ec; --green-100: #c2dfd0; --green-500: #1f8a65; /* matches --success */ --green-600: #18714f; --green-700: #125a3e; --amber-50: #fcf3d8; --amber-100: #f6e3a4; --amber-500: #d49103; --amber-700: #8a5a02; --violet-50: #ece8f3; --violet-100: #d4cae3; --violet-500: #6c5a99; --violet-700: #4a3d6b; } /* Re-skin greys to warm so text reads as part of the same family as the cream surface. */ :root { --gray-50: #f7f7f4; --gray-100: #ebeae5; --gray-200: #e6e5e0; --gray-300: #d6d4ce; --gray-400: #b8b5ac; --gray-500: #8a857b; --gray-600: #6b6760; --gray-700: #4a4742; --gray-800: #2e2c28; --gray-900: #26251e; } html, body { max-width: 100vw; overflow-x: hidden; height: 100%; -webkit-tap-highlight-color: transparent; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { color: rgb(var(--foreground-rgb)); background: rgb(var(--background-rgb)); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; overscroll-behavior-y: none; -webkit-user-select: none; user-select: none; /* Smooth page transitions */ scroll-behavior: smooth; } /* Mobile viewport height fix (100vh is wrong on mobile) */ @supports (-webkit-touch-callout: none) { .h-screen { height: -webkit-fill-available; } .min-h-screen { min-height: -webkit-fill-available; } } /* Safe area insets for notched phones */ .safe-top { padding-top: env(safe-area-inset-top); } .safe-bottom { padding-bottom: env(safe-area-inset-bottom); } /* Content must account for bottom nav */ .pb-nav { padding-bottom: calc(var(--nav-height) + env(safe-area-inset-bottom, 0px) + 1rem); } /* Hide scrollbar but keep scroll functionality */ .no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } /* Touch action optimization */ .touch-manipulation { touch-action: manipulation; } /* Prevent text selection on interactive elements */ button, a, nav { -webkit-user-select: none; user-select: none; } /* Allow text selection in content areas */ .selectable { -webkit-user-select: text; user-select: text; } /* Leaflet container fix */ .leaflet-container { width: 100%; height: 100%; z-index: 1; font-family: inherit; } /* Supplier item-name tooltip — Cursor-harmonized: warm cream surface with a hairline border and tight radius matching the rest of the app. Default Leaflet tooltip is white with a hard tip — wrong tone. */ .supplier-tooltip { background: #e6e5e0 !important; color: #26251e !important; border: 1px solid rgba(38, 37, 30, 0.1) !important; border-radius: 6px !important; font-family: system-ui, -apple-system, sans-serif !important; padding: 4px 8px !important; box-shadow: 0 4px 14px rgba(0,0,0,0.18) !important; white-space: nowrap !important; max-width: 240px !important; overflow: hidden !important; text-overflow: ellipsis !important; } .supplier-tooltip::before { border-top-color: #e6e5e0 !important; } /* ─────────────────────────────────────────────────────────────────── Waypoint pill — the new unified marker shape. Leaflet wraps each divIcon in a `.leaflet-marker-icon` container that ships with `text-align: center` and a default white background. We override so the pill: • renders the actual pill (no default white box behind it) • centers on the lat/lng (Leaflet applies the iconAnchor already) • never gets selected or dragged-text by accident • keeps its high z-index so it stacks above polylines ─────────────────────────────────────────────────────────────── */ .waypoint-div-icon { background: transparent !important; border: none !important; user-select: none !important; -webkit-user-select: none !important; } /* ────────────────────────────────────────────────────────────────── Waypoint map marker — consistent circle for every stop. Three reasons: 1. At 10+ stops pill-shaped icons crowded and collided. A small identical circle scales gracefully at every density. 2. Driver reads shape — a denser region of dots = more pickups, not "many different icon types to parse." 3. Cluster count lives in a small corner badge so the silhouette of "1 stop" vs "9 stops at same supplier" stays identical. ─────────────────────────────────────────────────────────────── */ .waypoint-div-icon { background: transparent !important; border: none !important; user-select: none !important; -webkit-user-select: none !important; } .waypoint-div-icon .waypoint-dot-wrap { position: relative; cursor: pointer; display: block; pointer-events: auto; } /* Soft halo: sits behind the dot, gives it a faint presence on the map without competing with the dot's silhouette. */ .waypoint-div-icon .waypoint-dot-shadow { position: absolute; top: 50%; left: 50%; width: 28px; height: 28px; margin: -14px 0 0 -14px; border-radius: 50%; background: rgba(245, 78, 0, 0.18); filter: blur(2px); transition: transform 150ms ease, opacity 150ms ease; } /* The dot itself — 26px circle, brand orange fill, cream ring for contrast on light tiles, subtle dark ring for depth on darks. */ .waypoint-div-icon .waypoint-dot { position: absolute; top: 50%; left: 50%; width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%; background: #f54e00; border: 3px solid #f2f1ed; box-shadow: 0 0 0 1px rgba(38, 37, 30, 0.25), 0 4px 10px rgba(38, 37, 30, 0.35); transition: transform 150ms ease, box-shadow 150ms ease; } /* Cluster count badge — small pill in the top-right corner of the hit area. Only ever shown on multi-point clusters; for singletons we leave the slot empty so the silhouette stays minimal. */ .waypoint-div-icon .waypoint-dot-badge { position: absolute; top: 0; right: 0; min-width: 18px; height: 18px; padding: 0 5px; display: inline-flex; align-items: center; justify-content: center; background: #26251e; color: #f2f1ed; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; font-weight: 700; letter-spacing: 0.02em; border-radius: 9999px; border: 2px solid #f2f1ed; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); pointer-events: none; /* clicks pass through to the dot */ } /* Singleton name label — sits below the dot, only when there's exactly one stop here. Hidden when a count badge is present so the dot's footprint stays small in dense regions. */ .waypoint-div-icon .waypoint-dot-name { position: absolute; top: calc(50% + 16px); left: 50%; transform: translateX(-50%); max-width: 140px; padding: 2px 6px; background: rgba(38, 37, 30, 0.78); color: #f2f1ed; font-family: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 10px; font-weight: 600; letter-spacing: -0.005em; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; pointer-events: none; opacity: 0; transition: opacity 150ms ease; } .waypoint-div-icon:hover .waypoint-dot-name { opacity: 1; } /* Hover state — dot scales up 1.15× and lifts, badge pops slightly. */ .waypoint-div-icon:hover .waypoint-dot, .waypoint-div-icon:focus-visible .waypoint-dot { transform: scale(1.15); box-shadow: 0 0 0 1px rgba(38, 37, 30, 0.3), 0 7px 16px rgba(245, 78, 0, 0.4), 0 4px 8px rgba(38, 37, 30, 0.3); } .waypoint-div-icon:hover .waypoint-dot-shadow { transform: scale(1.4); opacity: 0.6; } /* Keyboard focus: the hit box is the only focusable surface. */ .waypoint-div-icon:focus-visible { outline: none; } .waypoint-div-icon:focus-visible .waypoint-dot { box-shadow: 0 0 0 3px rgba(245, 78, 0, 0.4), 0 0 0 1px rgba(38, 37, 30, 0.3), 0 4px 10px rgba(38, 37, 30, 0.35); } /* Active press: dot squashes slightly to confirm the click. */ .waypoint-div-icon:active .waypoint-dot { transform: scale(0.95); } /* Respect prefers-reduced-motion: drop the easing and transforms so users sensitive to motion still see the waypoints clearly. */ @media (prefers-reduced-motion: reduce) { .waypoint-div-icon .waypoint-dot, .waypoint-div-icon .waypoint-dot-shadow, .waypoint-div-icon .waypoint-dot-name { transition: none !important; } .waypoint-div-icon:hover .waypoint-dot, .waypoint-div-icon:focus-visible .waypoint-dot, .waypoint-div-icon:hover .waypoint-dot-shadow { transform: none !important; } } /* ======================================== Animations ======================================== */ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @keyframes slideUp { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slideDown { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes scaleIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } .animate-fade-in { animation: fadeIn 0.3s ease-out forwards; } .animate-slide-up { animation: slideUp 0.35s cubic-bezier(0.32, 0.72, 0, 1) forwards; } .animate-slide-down { animation: slideDown 0.3s ease-out forwards; } .animate-scale-in { animation: scaleIn 0.2s ease-out forwards; } /* Stagger animation delay helper */ .stagger-1 { animation-delay: 0.05s; } .stagger-2 { animation-delay: 0.1s; } .stagger-3 { animation-delay: 0.15s; } .stagger-4 { animation-delay: 0.2s; } .stagger-5 { animation-delay: 0.25s; } /* ======================================== Card Styles ======================================== */ .card-shadow { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.1); } .card-shadow-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); } .card-shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); } /* Pressed/active state for cards */ .card-press { transition: transform 0.15s ease, box-shadow 0.15s ease; } .card-press:active { transform: scale(0.985); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.08); } /* ======================================== Status Badges ======================================== */ .status-pending { @apply bg-gray-100 text-gray-700 border-gray-300; } .status-assigned { @apply bg-blue-100 text-blue-700 border-blue-300; } .status-collected { @apply bg-green-100 text-green-700 border-green-300; } .status-problem { @apply bg-red-100 text-red-700 border-red-300; } /* ======================================== Loading Skeleton ======================================== */ .skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; } /* ======================================== Modal Overlay ======================================== */ .modal-overlay { backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } /* ======================================== Input Focus Rings ======================================== */ input:focus, textarea:focus, select:focus { outline: none; box-shadow: 0 0 0 2px rgba(245, 78, 0, 0.3); } /* ======================================== Bottom Sheet (iOS-style modal) ======================================== */ .bottom-sheet { border-radius: 1rem 1rem 0 0; box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); } @media (min-width: 640px) { .bottom-sheet { border-radius: 1rem; } } /* ======================================== Custom scrollbar (for when needed) ======================================== */ .custom-scrollbar::-webkit-scrollbar { width: 4px; } .custom-scrollbar::-webkit-scrollbar-track { background: transparent; } .custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.15); border-radius: 2px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.25); } /* ======================================== Pull-to-refresh visual feedback ======================================== */ .pull-indicator { transition: transform 0.2s ease-out; } /* ======================================== Accessibility: keyboard focus + reduced motion ======================================== */ /* Visible keyboard focus across the whole app. Only paints for keyboard users (`:focus-visible`), never on tap/click. */ *:focus { outline: none; } *:focus-visible { outline: 2px solid #f54e00; outline-offset: 2px; border-radius: 6px; } /* Pressable controls get the focus ring tighter to the control. */ button:focus-visible, a:focus-visible, [role="button"]:focus-visible { outline-offset: 2px; } /* On dark surfaces (Cursor Dark #26251e) the orange ring is invisible; use a white halo instead. */ .bg-orange-600:focus-visible, button[class*="bg-[#26251e]"]:focus-visible, button[class*="bg-gray-900"]:focus-visible, header button:focus-visible, header input:focus-visible { outline-color: #ffffff; box-shadow: 0 0 0 4px rgba(245, 78, 0, 0.35); } /* Respect users who opted out of motion. Keep opacity/state transitions; strip translate/scale/rotate motion. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } .skeleton { animation: none; background: #ececec; } .press-feedback:active, .press-feedback-sm:active, .card-press:active { transform: none; } } /* Screen-reader-only utility for visually hidden labels. */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } /* Live region for toasts: visible label is already present, this keeps the polite announcer quiet for status updates. */ [role="status"] { /* No visual change; just a hook for SR. */ } /* ======================================== Haptic-like press feedback ======================================== */ .press-feedback { transition: transform 0.1s ease, opacity 0.1s ease; } .press-feedback:active { transform: scale(0.96); opacity: 0.9; } .press-feedback-sm { transition: transform 0.1s ease, opacity 0.1s ease; } .press-feedback-sm:active { transform: scale(0.98); opacity: 0.9; }