@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; } /* ============================================================= Dark mode — same brand, inverted surfaces. Triggered by `` (set from AppSettings.darkMode in app/layout.tsx). Surface scale inverts: page bg becomes deep ink, cards become slightly lighter ink, and the brand orange stays exactly the same so CTAs, active states, and waypoints keep their identity. ============================================================= */ .dark { --foreground-rgb: 242, 241, 237; /* cream text */ --background-rgb: 18, 17, 14; /* near-black ink */ --surface-100: #1f1e19; --surface-200: #181710; /* page bg */ --surface-300: #24231d; /* button default */ --surface-400: #2d2b24; /* card */ --surface-500: #38362e; /* Greys remap so any `text-gray-*` etc. that survives the arbitrary-value re-skin reads correctly on dark. */ --gray-50: #1f1e19; --gray-100: #24231d; --gray-200: #2d2b24; --gray-300: #38362e; --gray-400: #5a574d; --gray-500: #8a857b; --gray-600: #b8b5ac; --gray-700: #d6d4ce; --gray-800: #ebeae5; --gray-900: #f2f1ed; } /* Dark-mode override for the major surfaces. The component code uses arbitrary values like `bg-[#f2f1ed]` and `bg-[#e6e5e0]` — those don't read Tailwind dark: variants. Instead we override via descendant selectors so the same JSX lights up without rewriting every Tailwind class. Scope: page background, card surfaces, modals, header, bottom nav, search input, and the sticky surfaces. We intentionally do NOT override text-foreground colors per-component — those use rgb(var(--foreground-rgb)) via the body class and just flip automatically. */ .dark body { color: rgb(var(--foreground-rgb)); background: rgb(var(--background-rgb)); } .dark .bg-\[\#f2f1ed\] { background-color: var(--surface-200); } .dark .bg-\[\#e6e5e0\] { background-color: var(--surface-400); } .dark .bg-\[\#ebeae5\] { background-color: var(--surface-300); } .dark .bg-\[\#f7f7f4\] { background-color: var(--surface-100); } .dark .bg-\[\#e1e0db\] { background-color: var(--surface-500); } /* Header / nav glass surfaces */ .dark header.bg-\[\#e6e5e0\], .dark nav.bg-\[\#e6e5e0\]\/95 { background-color: rgba(24, 23, 16, 0.92); } /* Hairline borders — keep them visible but invert */ .dark .border-\[\rgba\(38\2c 37\2c 30\2c 0\.1\)\] { border-color: rgba(242, 241, 237, 0.08); } .dark .border-\[\rgba\(38\2c 37\2c 30\2c 0\.08\)\] { border-color: rgba(242, 241, 237, 0.06); } .dark .border-\[\rgba\(38\2c 37\2c 30\2c 0\.05\)\] { border-color: rgba(242, 241, 237, 0.04); } .dark .border-\[\rgba\(38\2c 37\2c 30\2c 0\.15\)\] { border-color: rgba(242, 241, 237, 0.15); } /* Translucent text — the 55% / 40% / 60% / 70% inks used across the app. In dark mode they need to flip to translucent cream. */ .dark .text-\[\#26251e\] { color: rgb(var(--foreground-rgb)); } .dark .text-\[\rgba\(38\2c 37\2c 30\2c 0\.55\)\] { color: rgba(242, 241, 237, 0.6); } .dark .text-\[\rgba\(38\2c 37\2c 30\2c 0\.7\)\] { color: rgba(242, 241, 237, 0.75); } .dark .text-\[\rgba\(38\2c 37\2c 30\2c 0\.6\)\] { color: rgba(242, 241, 237, 0.65); } .dark .text-\[\rgba\(38\2c 37\2c 30\2c 0\.4\)\] { color: rgba(242, 241, 237, 0.4); } .dark .text-\[\rgba\(38\2c 37\2c 30\2c 0\.45\)\] { color: rgba(242, 241, 237, 0.5); } /* Active-tab "dark pill" (`bg-[#26251e] text-[#f2f1ed]`) — in dark mode the original colors ARE the dark surface, so the pill visually disappears. Invert by making it the lighter card surface with dark ink text. */ .dark .bg-\[\#26251e\]\/95 { background-color: rgba(242, 241, 237, 0.08); } /* The active filter chip / toggle button uses solid `bg-[#26251e]` with cream text. In dark mode we invert the whole chip — cream pill, dark ink text — so it reads as the selected state on the dark header. We do this only when paired with `text-[#f2f1ed]` (the active-state pattern); the same `bg-[#26251e]` on its own is used for the settings icon tile which stays dark on purpose. */ .dark .bg-\[\#26251e\].text-\[\#f2f1ed\], .dark .bg-\[\#26251e\].text-white { background-color: #ebeae5; color: #181710; } /* Inputs / form controls */ .dark input.bg-\[\#f2f1ed\] { background-color: var(--surface-300); color: rgb(var(--foreground-rgb)); } /* Map markers and map UI keep brand orange but the waypoint dot's cream halo (`#f2f1ed`) needs to flip to dark to stay visible. */ .dark .waypoint-div-icon .waypoint-dot { border-color: var(--surface-200); } .dark .waypoint-div-icon .waypoint-dot-badge { background: var(--foreground-rgb); color: var(--background-rgb); border-color: var(--surface-200); } 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); } /* ============================================================= Brand accent strip — sits at the very top of the sticky header (and the very top of the bottom nav, flipped direction). A 3px brand-orange line gives both surfaces a clear visual edge against the cream page bg, so they read as a frame rather than blending into the content. In dark mode the strip stays the same orange so the brand mark keeps anchoring the chrome. ============================================================= */ .brand-accent-top { position: absolute; top: 0; left: 0; right: 0; height: 3px; background: #f54e00; box-shadow: 0 1px 0 rgba(245, 78, 0, 0.3); z-index: 1; pointer-events: none; } .brand-accent-bottom { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: #f54e00; box-shadow: 0 -1px 0 rgba(245, 78, 0, 0.3); z-index: 1; pointer-events: none; } /* 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, always visible (was hover-only; changed so drivers can read stop names at a glance without tapping). The cream-on-dark chip keeps the label readable over busy map tiles, and the same chip is used by cluster labels so the visual grammar stays consistent. */ .waypoint-div-icon .waypoint-dot-name { position: absolute; top: calc(50% + 16px); left: 50%; transform: translateX(-50%); max-width: 160px; padding: 3px 8px; background: rgba(38, 37, 30, 0.92); color: #f2f1ed; font-family: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 11px; font-weight: 600; letter-spacing: -0.005em; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; pointer-events: none; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); } .dark .waypoint-div-icon .waypoint-dot-name { background: rgba(242, 241, 237, 0.94); color: #181710; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5); } /* Hover still highlights — small lift on the chip, not the show/hide animation we used before. */ .waypoint-div-icon:hover .waypoint-dot-name { transform: translateX(-50%) translateY(-1px); } /* 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; }