WFDASHDRIVER-APP/app/globals.css

630 lines
17 KiB
CSS
Raw Normal View History

@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;
}