- Set up class-based dark mode in Tailwind config - Add pre-hydration dark mode application to avoid theme flash on initial load - Implement accessible dark mode toggle in settings page - Update all UI surfaces (headers, bottom nav, maps) with dark mode color schemes - Add comprehensive global CSS overrides for arbitrary Tailwind classes - Add brand accent strips to header and bottom nav for consistent visual framing - Refine map waypoint styling: reduce dot size, update label positioning and styling
776 lines
No EOL
23 KiB
CSS
776 lines
No EOL
23 KiB
CSS
@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 `<html class="dark">` (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;
|
||
} |