916 lines
30 KiB
CSS
916 lines
30 KiB
CSS
@tailwind base;
|
||
@tailwind components;
|
||
@tailwind utilities;
|
||
|
||
@layer base {
|
||
:root {
|
||
/* Light theme - Clean with subtle neon accents */
|
||
--background: 0 0% 100%;
|
||
--foreground: 0 0% 10%;
|
||
|
||
--card: 0 0% 98%;
|
||
--card-foreground: 0 0% 10%;
|
||
|
||
--popover: 0 0% 100%;
|
||
--popover-foreground: 0 0% 10%;
|
||
|
||
/* Darker neon red for light mode */
|
||
--primary: 349 85% 45%;
|
||
--primary-foreground: 0 0% 100%;
|
||
|
||
--secondary: 0 0% 95%;
|
||
--secondary-foreground: 0 0% 10%;
|
||
|
||
--muted: 0 0% 96%;
|
||
--muted-foreground: 0 0% 40%;
|
||
|
||
/* Darker neon lime for light mode.
|
||
Foreground is near-black (~9.5:1 on lime) — white (2.35:1) fails WCAG AA,
|
||
and lime is the "go"/CTA color, so its filled state must stay readable. */
|
||
--accent: 75 80% 40%;
|
||
--accent-foreground: 0 0% 10%;
|
||
|
||
--destructive: 0 84% 60%;
|
||
--destructive-foreground: 0 0% 100%;
|
||
|
||
--border: 0 0% 90%;
|
||
--input: 0 0% 95%;
|
||
--ring: 349 85% 45%;
|
||
|
||
--radius: 1rem;
|
||
|
||
--sidebar-background: 0 0% 98%;
|
||
--sidebar-foreground: 0 0% 10%;
|
||
--sidebar-primary: 349 85% 45%;
|
||
--sidebar-primary-foreground: 0 0% 100%;
|
||
--sidebar-accent: 0 0% 95%;
|
||
--sidebar-accent-foreground: 0 0% 10%;
|
||
--sidebar-border: 0 0% 90%;
|
||
--sidebar-ring: 349 85% 45%;
|
||
|
||
/* Custom gradient colors */
|
||
--gradient-start: 349 85% 45%;
|
||
--gradient-end: 75 80% 40%;
|
||
}
|
||
|
||
.dark {
|
||
/* Pure black THUG2 aesthetic */
|
||
--background: 0 0% 0%;
|
||
--foreground: 0 0% 100%;
|
||
|
||
--card: 0 0% 5%;
|
||
--card-foreground: 0 0% 100%;
|
||
|
||
--popover: 0 0% 5%;
|
||
--popover-foreground: 0 0% 100%;
|
||
|
||
/* Neon red primary */
|
||
--primary: 349 100% 50%;
|
||
--primary-foreground: 0 0% 100%;
|
||
|
||
--secondary: 0 0% 10%;
|
||
--secondary-foreground: 0 0% 100%;
|
||
|
||
--muted: 0 0% 10%;
|
||
--muted-foreground: 0 0% 60%;
|
||
|
||
/* Neon lime accent */
|
||
--accent: 75 100% 60%;
|
||
--accent-foreground: 0 0% 0%;
|
||
|
||
--destructive: 0 84% 60%;
|
||
--destructive-foreground: 0 0% 100%;
|
||
|
||
--border: 349 100% 50% / 0.3;
|
||
--input: 0 0% 10%;
|
||
--ring: 349 100% 50%;
|
||
|
||
--sidebar-background: 0 0% 0%;
|
||
--sidebar-foreground: 0 0% 100%;
|
||
--sidebar-primary: 349 100% 50%;
|
||
--sidebar-primary-foreground: 0 0% 100%;
|
||
--sidebar-accent: 0 0% 10%;
|
||
--sidebar-accent-foreground: 0 0% 100%;
|
||
--sidebar-border: 349 100% 50% / 0.3;
|
||
--sidebar-ring: 349 100% 50%;
|
||
}
|
||
}
|
||
|
||
@layer base {
|
||
* {
|
||
@apply border-border;
|
||
}
|
||
|
||
body {
|
||
@apply bg-background text-foreground;
|
||
}
|
||
}
|
||
|
||
/* Leaflet map overrides */
|
||
.leaflet-container {
|
||
height: 100%;
|
||
width: 100%;
|
||
border-radius: var(--radius);
|
||
z-index: 0;
|
||
}
|
||
|
||
/* Push Leaflet bottom controls (attribution, zoom) above the mobile nav bar.
|
||
The nav bar is ~60px tall + safe area inset. This prevents "Map data © OpenStreetMap"
|
||
from overlapping the navigation buttons on mobile. */
|
||
.leaflet-bottom {
|
||
bottom: env(safe-area-inset-bottom, 0px);
|
||
padding-bottom: 64px;
|
||
}
|
||
|
||
/* The map page's toolbar floats over the top of the map on mobile, which would
|
||
otherwise sit on top of Leaflet's default top-left zoom control. Push that
|
||
control clear of it (desktop keeps the toolbar in flow, so no offset). */
|
||
@media (max-width: 767px) {
|
||
.ss-map-floating-toolbar .leaflet-top.leaflet-left {
|
||
margin-top: 4.25rem;
|
||
}
|
||
}
|
||
|
||
.leaflet-popup-content-wrapper {
|
||
border-radius: calc(var(--radius) - 4px);
|
||
box-shadow: 0 10px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||
}
|
||
|
||
.leaflet-popup-content {
|
||
margin: 0;
|
||
}
|
||
|
||
/* THUG2-inspired neon effects */
|
||
@keyframes neon-glow {
|
||
0%, 100% { text-shadow: 0 0 10px currentColor, 0 0 20px currentColor, 0 0 30px currentColor, 0 0 40px currentColor; }
|
||
50% { text-shadow: 0 0 5px currentColor, 0 0 10px currentColor, 0 0 15px currentColor, 0 0 20px currentColor; }
|
||
}
|
||
|
||
/* ── Display font utilities ───────────────────────────────────────────────── */
|
||
/* Bebas Neue — used for all major headlines */
|
||
.font-display {
|
||
font-family: 'Bebas Neue', 'Kanit', 'Impact', 'Arial Black', sans-serif;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
/* Barlow Condensed — used for subheadings, labels, nav */
|
||
.font-condensed {
|
||
font-family: 'Barlow Condensed', 'Kanit', 'Arial Narrow', sans-serif;
|
||
}
|
||
|
||
.neon-header {
|
||
animation: neon-glow 2s ease-in-out infinite;
|
||
font-family: 'Bebas Neue', 'Kanit', 'Impact', 'Arial Black', sans-serif;
|
||
letter-spacing: 0.06em;
|
||
}
|
||
|
||
.neon-red {
|
||
color: #ff0040;
|
||
text-shadow:
|
||
0 0 10px #ff0040,
|
||
0 0 20px #ff0040,
|
||
0 0 30px #ff0040,
|
||
0 0 40px #ff0040,
|
||
2px 2px 4px rgba(0,0,0,0.9);
|
||
}
|
||
|
||
.neon-lime {
|
||
color: #ccff00;
|
||
text-shadow:
|
||
0 0 10px #ccff00,
|
||
0 0 20px #ccff00,
|
||
0 0 30px #ccff00,
|
||
0 0 40px #ccff00,
|
||
2px 2px 4px rgba(0,0,0,0.9);
|
||
}
|
||
|
||
.neon-orange {
|
||
color: #ff6600;
|
||
text-shadow:
|
||
0 0 10px #ff6600,
|
||
0 0 20px #ff6600,
|
||
0 0 30px #ff6600;
|
||
}
|
||
|
||
.gradient-text {
|
||
background: linear-gradient(135deg, #ff0040 0%, #ff6600 50%, #ccff00 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(30px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.animate-slideUp {
|
||
animation: slideUp 0.6s ease-out;
|
||
}
|
||
|
||
.animate-pulse-slow {
|
||
animation: pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||
}
|
||
|
||
@keyframes stagger {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.stagger-children > * {
|
||
animation: stagger 0.5s ease-out backwards;
|
||
}
|
||
|
||
.stagger-children > *:nth-child(1) { animation-delay: 0.1s; }
|
||
.stagger-children > *:nth-child(2) { animation-delay: 0.2s; }
|
||
.stagger-children > *:nth-child(3) { animation-delay: 0.3s; }
|
||
.stagger-children > *:nth-child(4) { animation-delay: 0.4s; }
|
||
.stagger-children > *:nth-child(5) { animation-delay: 0.5s; }
|
||
.stagger-children > *:nth-child(6) { animation-delay: 0.6s; }
|
||
|
||
.dark .leaflet-tile-pane {
|
||
filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(0.9);
|
||
}
|
||
|
||
.dark .leaflet-control-zoom a {
|
||
background-color: hsl(var(--card));
|
||
color: hsl(var(--foreground));
|
||
border-color: hsl(var(--border));
|
||
}
|
||
|
||
.dark .leaflet-control-zoom a:hover {
|
||
background-color: hsl(var(--muted));
|
||
}
|
||
|
||
.dark .leaflet-popup-content-wrapper,
|
||
.dark .leaflet-popup-tip {
|
||
background-color: hsl(var(--card));
|
||
color: hsl(var(--foreground));
|
||
}
|
||
|
||
/* Custom scrollbar */
|
||
::-webkit-scrollbar {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: hsl(var(--muted));
|
||
border-radius: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: hsl(var(--muted-foreground) / 0.3);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: hsl(var(--muted-foreground) / 0.5);
|
||
}
|
||
|
||
/* On touch/phone screens, hide the visible scrollbar entirely for a clean,
|
||
app-like feel. Swipe-to-scroll still works — only the bar is hidden. */
|
||
@media (max-width: 767px) {
|
||
html,
|
||
body {
|
||
scrollbar-width: none; /* Firefox */
|
||
-ms-overflow-style: none; /* IE/old Edge */
|
||
}
|
||
::-webkit-scrollbar {
|
||
width: 0;
|
||
height: 0;
|
||
display: none; /* Chrome/Safari/WebKit */
|
||
}
|
||
}
|
||
|
||
/* Animations */
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(8px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
@keyframes pulse-glow {
|
||
0%, 100% { box-shadow: 0 0 0 0 hsl(var(--primary) / 0.4); }
|
||
50% { box-shadow: 0 0 0 8px hsl(var(--primary) / 0); }
|
||
}
|
||
|
||
/* "You are here" user-location marker pulse halo */
|
||
@keyframes ssPulse {
|
||
0% { transform: scale(0.5); opacity: 0.8; }
|
||
70% { transform: scale(1.6); opacity: 0; }
|
||
100% { transform: scale(1.6); opacity: 0; }
|
||
}
|
||
|
||
/* Pure-opacity reveal for the app shell under the departing splash — no
|
||
translate, so it never nudges a full-bleed map on reveal. */
|
||
@keyframes appReveal {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
.animate-app-reveal {
|
||
animation: appReveal 0.5s ease-out both;
|
||
}
|
||
|
||
/* ── Cold-start splash screen ── */
|
||
@keyframes splashPulse {
|
||
0%, 100% { transform: scale(1); }
|
||
50% { transform: scale(1.04); }
|
||
}
|
||
@keyframes splashGlow {
|
||
0%, 100% { opacity: 0.7; transform: translate(-50%, -50%) scale(1); }
|
||
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
|
||
}
|
||
@keyframes splashSpin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
@keyframes splashRise {
|
||
from { opacity: 0; transform: translateY(14px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
@keyframes splashShine {
|
||
0% { transform: translateX(-160%); }
|
||
100% { transform: translateX(420%); }
|
||
}
|
||
@keyframes splashFade {
|
||
from { opacity: 0; transform: translateY(3px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* ── Map skeleton "ghost pins" (shown on a cold, empty, loading map) ──
|
||
The pin gently rises + breathes; the shadow under it pulses like a radar
|
||
ping. Kept subtle so it reads as "loading", never as real content. */
|
||
@keyframes ghostPin {
|
||
0%, 100% { transform: translateY(0) scale(1); opacity: 0.55; }
|
||
50% { transform: translateY(-6px) scale(1.04); opacity: 0.9; }
|
||
}
|
||
@keyframes ghostPing {
|
||
0%, 100% { transform: translateX(-50%) translateY(-0.25rem) scaleX(1); opacity: 0.35; }
|
||
50% { transform: translateX(-50%) translateY(-0.25rem) scaleX(1.6); opacity: 0.12; }
|
||
}
|
||
.animate-ghost-pin {
|
||
animation: ghostPin 2.4s ease-in-out infinite;
|
||
}
|
||
.animate-ghost-ping {
|
||
animation: ghostPing 2.4s ease-in-out infinite;
|
||
}
|
||
|
||
/* ── Live map markers: crisp pins that feel alive ─────────────────────────────
|
||
A new pin drops in with a quick spring; hovering/tapping lifts it and pulses
|
||
its neon halo once. Purely presentational, GPU-friendly (transform/opacity),
|
||
and fully disabled under reduced-motion. */
|
||
@keyframes ssPinDrop {
|
||
0% { transform: translateY(-14px) scale(0.6); opacity: 0; }
|
||
60% { transform: translateY(2px) scale(1.06); opacity: 1; }
|
||
100% { transform: translateY(0) scale(1); opacity: 1; }
|
||
}
|
||
@keyframes ssPinPulse {
|
||
0% { filter: drop-shadow(0 0 2px rgba(255,0,64,0.0)); }
|
||
50% { filter: drop-shadow(0 0 10px rgba(255,0,64,0.9)); }
|
||
100% { filter: drop-shadow(0 0 2px rgba(255,0,64,0.0)); }
|
||
}
|
||
|
||
/* The teardrop wrapper inside our divIcon. */
|
||
.skate-spot-marker .ss-pin {
|
||
transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
transform-origin: 50% 100%;
|
||
will-change: transform;
|
||
}
|
||
@media (prefers-reduced-motion: no-preference) {
|
||
.skate-spot-marker .ss-pin {
|
||
animation: ssPinDrop 0.32s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||
}
|
||
/* Lift + one halo pulse on hover/focus/press. Leaflet keeps the marker
|
||
focusable, so this also covers keyboard + tap. */
|
||
.skate-spot-marker:hover .ss-pin,
|
||
.skate-spot-marker:focus .ss-pin,
|
||
.skate-spot-marker:active .ss-pin {
|
||
transform: translateY(-4px) scale(1.12);
|
||
animation: ssPinPulse 0.6s ease-out;
|
||
}
|
||
}
|
||
|
||
/* Indeterminate top loading bar for the map ("Loading spots"). A short segment
|
||
sweeps left→right on a faint track, so it reads as active work without
|
||
pretending to know real progress. Disabled under reduced-motion. */
|
||
@keyframes ssLoadBar {
|
||
0% { transform: translateX(-120%); }
|
||
100% { transform: translateX(420%); }
|
||
}
|
||
.ss-loadbar {
|
||
animation: ssLoadBar 1.1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.ss-loadbar { animation: none; width: 100%; opacity: 0.7; }
|
||
}
|
||
|
||
.animate-fadeIn {
|
||
animation: fadeIn 0.4s ease-out forwards;
|
||
}
|
||
|
||
.animate-slideUp {
|
||
animation: slideUp 0.5s ease-out forwards;
|
||
}
|
||
|
||
.animate-pulse-glow {
|
||
animation: pulse-glow 2s infinite;
|
||
}
|
||
|
||
/* Stagger animation for lists */
|
||
.stagger-children > * {
|
||
opacity: 0;
|
||
animation: fadeIn 0.4s ease-out forwards;
|
||
}
|
||
|
||
.stagger-children > *:nth-child(1) { animation-delay: 0ms; }
|
||
.stagger-children > *:nth-child(2) { animation-delay: 50ms; }
|
||
.stagger-children > *:nth-child(3) { animation-delay: 100ms; }
|
||
.stagger-children > *:nth-child(4) { animation-delay: 150ms; }
|
||
.stagger-children > *:nth-child(5) { animation-delay: 200ms; }
|
||
.stagger-children > *:nth-child(6) { animation-delay: 250ms; }
|
||
.stagger-children > *:nth-child(7) { animation-delay: 300ms; }
|
||
.stagger-children > *:nth-child(8) { animation-delay: 350ms; }
|
||
|
||
/* Gradient text */
|
||
.gradient-text {
|
||
background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--accent)) 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
/* Glass effect */
|
||
.glass {
|
||
background: hsl(var(--card) / 0.8);
|
||
backdrop-filter: blur(12px);
|
||
-webkit-backdrop-filter: blur(12px);
|
||
}
|
||
|
||
/* Safe area insets for iOS notched devices */
|
||
.h-safe-area-bottom {
|
||
height: env(safe-area-inset-bottom, 0px);
|
||
}
|
||
|
||
.pb-safe {
|
||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||
}
|
||
|
||
.pt-safe {
|
||
padding-top: env(safe-area-inset-top, 0px);
|
||
}
|
||
|
||
.safe-area-top {
|
||
padding-top: env(safe-area-inset-top, 0px);
|
||
}
|
||
|
||
.safe-area-bottom {
|
||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||
}
|
||
|
||
/* Bottom navigation height token — used for full-bleed pages (map, etc.) */
|
||
:root {
|
||
/* Mobile header: 3.5rem (56px) */
|
||
--header-mobile: 3.5rem;
|
||
/* Mobile bottom nav: 58px base + safe area */
|
||
--bottom-nav: calc(3.625rem + env(safe-area-inset-bottom, 0px));
|
||
/* Desktop footer: py-6 (3rem) + 1px border + ~1 line of content ≈ 5rem.
|
||
Full-bleed pages (map) subtract this so the footer stays in view without
|
||
scrolling. */
|
||
--footer-desktop: 5rem;
|
||
}
|
||
|
||
/* Full-bleed page — removes Layout bottom padding so the page can manage its own height */
|
||
.page-fullbleed > main {
|
||
padding-bottom: 0 !important;
|
||
}
|
||
|
||
.safe-area-left {
|
||
padding-left: env(safe-area-inset-left, 0px);
|
||
}
|
||
|
||
.safe-area-right {
|
||
padding-right: env(safe-area-inset-right, 0px);
|
||
}
|
||
|
||
/* Mobile bottom nav spacing - add padding to content to avoid overlap */
|
||
@media (max-width: 768px) {
|
||
.mobile-nav-padding {
|
||
padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
}
|
||
|
||
/* Prevent horizontal overflow on mobile */
|
||
html, body {
|
||
overflow-x: hidden;
|
||
max-width: 100vw;
|
||
/* Allow normal vertical scrolling — do NOT set overscroll-behavior: none here
|
||
as it blocks touch scroll on Android browsers */
|
||
}
|
||
|
||
/* Better tap highlight on mobile — subtle red glow */
|
||
a, button {
|
||
-webkit-tap-highlight-color: rgba(255, 0, 64, 0.08);
|
||
}
|
||
|
||
/* Improve touch targets for mobile — WCAG minimum 44×44px */
|
||
@media (max-width: 768px) {
|
||
button, [role="button"] {
|
||
min-height: 44px;
|
||
min-width: 44px;
|
||
}
|
||
}
|
||
|
||
/* Fix input zoom on iOS — font-size must be ≥16px to prevent auto-zoom */
|
||
input, select, textarea {
|
||
font-size: max(16px, 1em);
|
||
}
|
||
|
||
/* Prevent text size adjustments after orientation changes on iOS */
|
||
html {
|
||
-webkit-text-size-adjust: 100%;
|
||
text-size-adjust: 100%;
|
||
}
|
||
|
||
/* Better scrolling containers — prevent pull-to-refresh on inner scrollable areas */
|
||
.scroll-container {
|
||
overscroll-behavior-y: contain;
|
||
}
|
||
|
||
/* Touch-friendly horizontal scroll rows (e.g. filter chips) */
|
||
.scroll-x {
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
overscroll-behavior-x: contain;
|
||
scroll-snap-type: x proximity;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
.scroll-x > * {
|
||
scroll-snap-align: start;
|
||
}
|
||
|
||
/* Hide scrollbar utility - useful for horizontal scrolling filters */
|
||
.scrollbar-hide {
|
||
-ms-overflow-style: none; /* IE and Edge */
|
||
scrollbar-width: none; /* Firefox */
|
||
}
|
||
.scrollbar-hide::-webkit-scrollbar {
|
||
display: none; /* Chrome, Safari and Opera */
|
||
}
|
||
|
||
/* Extra small breakpoint for very small phones (375px+) */
|
||
@media (min-width: 375px) {
|
||
.xs\:inline { display: inline; }
|
||
.xs\:flex { display: flex; }
|
||
.xs\:hidden { display: none; }
|
||
.xs\:flex-row { flex-direction: row; }
|
||
.xs\:px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||
.xs\:-mx-4 { margin-left: -1rem; margin-right: -1rem; }
|
||
.xs\:text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||
.xs\:text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||
.xs\:gap-2 { gap: 0.5rem; }
|
||
}
|
||
|
||
/* ─── Global UI/UX Polish ────────────────────────────────────────────────── */
|
||
|
||
/* Smooth page transitions */
|
||
@media (prefers-reduced-motion: no-preference) {
|
||
.page-enter {
|
||
animation: fadeIn 0.25s ease-out;
|
||
}
|
||
}
|
||
|
||
/* ── Focus rings — accessible, on-brand ────────────────────────────────── */
|
||
:focus-visible {
|
||
outline: 2px solid #ff0040;
|
||
outline-offset: 2px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* ── Card hover micro-interactions ─────────────────────────────────────── */
|
||
.card-hover {
|
||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||
}
|
||
.card-hover:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 32px rgba(255,0,64,0.12), 0 2px 8px rgba(0,0,0,0.4);
|
||
}
|
||
|
||
/* ── Button ripple feel ─────────────────────────────────────────────────── */
|
||
button, [role="button"], a {
|
||
transition-property: color, background-color, border-color, box-shadow, transform, opacity;
|
||
transition-duration: 150ms;
|
||
transition-timing-function: ease;
|
||
}
|
||
|
||
/* ── Modal / dialog backdrop improvements ──────────────────────────────── */
|
||
[data-radix-portal] [data-state="open"][data-overlay] {
|
||
backdrop-filter: blur(4px);
|
||
-webkit-backdrop-filter: blur(4px);
|
||
}
|
||
|
||
/* ── Sheet / drawer slide animations ───────────────────────────────────── */
|
||
[data-radix-dialog-content],
|
||
[data-radix-sheet-content] {
|
||
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
|
||
}
|
||
|
||
/* ── Toast notification improvements ──────────────────────────────────── */
|
||
[data-radix-toast-root] {
|
||
backdrop-filter: blur(12px);
|
||
-webkit-backdrop-filter: blur(12px);
|
||
}
|
||
|
||
/* ── Skeleton loading shimmer ──────────────────────────────────────────── */
|
||
@keyframes skeleton-shimmer {
|
||
0% { background-position: -200% 0; }
|
||
100% { background-position: 200% 0; }
|
||
}
|
||
|
||
.skeleton-shimmer {
|
||
background: linear-gradient(
|
||
90deg,
|
||
rgba(255,255,255,0.03) 0%,
|
||
rgba(255,255,255,0.08) 50%,
|
||
rgba(255,255,255,0.03) 100%
|
||
);
|
||
background-size: 200% 100%;
|
||
animation: skeleton-shimmer 1.5s infinite;
|
||
}
|
||
|
||
/* ── Improved empty state cards ────────────────────────────────────────── */
|
||
.empty-state {
|
||
border: 2px dashed rgba(255,255,255,0.08);
|
||
border-radius: 16px;
|
||
padding: 3rem 2rem;
|
||
text-align: center;
|
||
background: rgba(255,255,255,0.01);
|
||
}
|
||
|
||
/* ── Better dropdown / select styling ─────────────────────────────────── */
|
||
select {
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat;
|
||
background-position: right 0.75rem center;
|
||
padding-right: 2.5rem;
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
}
|
||
|
||
/* ── Map container full height ─────────────────────────────────────────── */
|
||
.map-full-height {
|
||
height: calc(100dvh - 4rem);
|
||
}
|
||
@media (max-width: 768px) {
|
||
.map-full-height {
|
||
height: calc(100dvh - 3.5rem - env(safe-area-inset-bottom, 0px) - 56px);
|
||
}
|
||
}
|
||
|
||
/* ── Globe container ───────────────────────────────────────────────────── */
|
||
.globe-container canvas {
|
||
touch-action: none; /* prevents scroll interference on mobile */
|
||
}
|
||
|
||
/* ── Typography hierarchy helpers ─────────────────────────────────────── */
|
||
.text-balance {
|
||
text-wrap: balance;
|
||
}
|
||
.text-pretty {
|
||
text-wrap: pretty;
|
||
}
|
||
|
||
/* ── Accessibility — reduced motion ────────────────────────────────────── */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*, *::before, *::after {
|
||
animation-duration: 0.001ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.001ms !important;
|
||
}
|
||
}
|
||
|
||
/* ─── SkateSpots Rider Card ──────────────────────────────────────────────── */
|
||
|
||
/* Credit-card aspect ratio: 85.6mm × 54mm = 1.586:1 */
|
||
.rider-card {
|
||
width: 360px;
|
||
height: 227px;
|
||
border-radius: 14px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
font-family: 'Inter Variable', 'Inter', 'Kanit', system-ui, sans-serif;
|
||
box-shadow: 0 8px 40px rgba(0,0,0,0.6), 0 2px 8px rgba(0,0,0,0.4);
|
||
}
|
||
|
||
/* Holographic shimmer overlay — appears on hover/focus for screen,
|
||
hidden on print so the card prints cleanly */
|
||
@keyframes holo-shift {
|
||
0% { background-position: 0% 50%; }
|
||
50% { background-position: 100% 50%; }
|
||
100% { background-position: 0% 50%; }
|
||
}
|
||
|
||
.rider-card-holo::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
border-radius: inherit;
|
||
background: linear-gradient(
|
||
125deg,
|
||
transparent 20%,
|
||
rgba(255,255,255,0.08) 30%,
|
||
rgba(255,255,255,0.18) 40%,
|
||
rgba(255,0,64,0.08) 50%,
|
||
rgba(204,255,0,0.10) 60%,
|
||
rgba(255,255,255,0.08) 70%,
|
||
transparent 80%
|
||
);
|
||
background-size: 300% 300%;
|
||
animation: holo-shift 4s ease infinite;
|
||
pointer-events: none;
|
||
z-index: 10;
|
||
mix-blend-mode: screen;
|
||
}
|
||
|
||
/* ── Force background graphics to print on all card themes ──
|
||
Browsers suppress backgrounds by default. -webkit-print-color-adjust: exact
|
||
overrides this for Chrome/Safari. color-adjust: exact covers Firefox/Edge. */
|
||
.rider-card,
|
||
.rider-card *,
|
||
[class^="card-theme-"],
|
||
[class^="card-theme-"] * {
|
||
-webkit-print-color-adjust: exact !important;
|
||
print-color-adjust: exact !important;
|
||
color-adjust: exact !important;
|
||
}
|
||
|
||
/* ── Theme: THUG (default — black + neon red) ── */
|
||
.card-theme-thug {
|
||
background: linear-gradient(135deg, #0a0a0a 0%, #1a0008 60%, #0d0d0d 100%) !important;
|
||
border: 1.5px solid rgba(255,0,64,0.5);
|
||
box-shadow: 0 0 30px rgba(255,0,64,0.2), inset 0 1px 0 rgba(255,255,255,0.05);
|
||
color: #fff !important;
|
||
}
|
||
.card-theme-thug .card-accent { color: #ff0040 !important; }
|
||
.card-theme-thug .card-accent2 { color: #ccff00 !important; }
|
||
.card-theme-thug .card-stripe { background: linear-gradient(90deg, #ff0040, #ff6600) !important; }
|
||
.card-theme-thug .card-muted { color: rgba(255,255,255,0.6) !important; }
|
||
.card-theme-thug .card-bg-chip { background: rgba(255,0,64,0.25) !important; border: 1px solid rgba(255,0,64,0.5) !important; }
|
||
|
||
/* ── Theme: NEON LIME ── */
|
||
.card-theme-lime {
|
||
background: linear-gradient(135deg, #050f00 0%, #0a1a00 60%, #050f00 100%) !important;
|
||
border: 1.5px solid rgba(204,255,0,0.5);
|
||
box-shadow: 0 0 30px rgba(204,255,0,0.15), inset 0 1px 0 rgba(255,255,255,0.05);
|
||
color: #fff !important;
|
||
}
|
||
.card-theme-lime .card-accent { color: #ccff00 !important; }
|
||
.card-theme-lime .card-accent2 { color: #ff6600 !important; }
|
||
.card-theme-lime .card-stripe { background: linear-gradient(90deg, #ccff00, #88ff00) !important; }
|
||
.card-theme-lime .card-muted { color: rgba(255,255,255,0.6) !important; }
|
||
.card-theme-lime .card-bg-chip { background: rgba(204,255,0,0.2) !important; border: 1px solid rgba(204,255,0,0.5) !important; }
|
||
|
||
/* ── Theme: CHROME (silver/steel) ── */
|
||
.card-theme-chrome {
|
||
background: linear-gradient(135deg, #1a1a1a 0%, #2e2e2e 40%, #111 100%) !important;
|
||
border: 1.5px solid rgba(200,200,200,0.4);
|
||
box-shadow: 0 0 20px rgba(180,180,180,0.1), inset 0 1px 0 rgba(255,255,255,0.15);
|
||
color: #e8e8e8 !important;
|
||
}
|
||
.card-theme-chrome .card-accent { color: #c8c8c8 !important; }
|
||
.card-theme-chrome .card-accent2 { color: #ff6600 !important; }
|
||
.card-theme-chrome .card-stripe { background: linear-gradient(90deg, #888, #ccc, #888) !important; }
|
||
.card-theme-chrome .card-muted { color: rgba(232,232,232,0.6) !important; }
|
||
.card-theme-chrome .card-bg-chip { background: rgba(200,200,200,0.15) !important; border: 1px solid rgba(200,200,200,0.3) !important; }
|
||
|
||
/* ── Theme: WOODGRAIN ── */
|
||
.card-theme-wood {
|
||
background: linear-gradient(160deg, #3d2008 0%, #5c3210 30%, #2e1804 60%, #4a2a0e 100%) !important;
|
||
border: 1.5px solid rgba(160,100,40,0.6);
|
||
box-shadow: 0 0 20px rgba(120,70,20,0.2), inset 0 1px 0 rgba(255,200,100,0.1);
|
||
color: #f5e6c8 !important;
|
||
}
|
||
.card-theme-wood .card-accent { color: #f5c842 !important; }
|
||
.card-theme-wood .card-accent2 { color: #ff6600 !important; }
|
||
.card-theme-wood .card-stripe { background: linear-gradient(90deg, #8b5e3c, #d4a05a, #8b5e3c) !important; }
|
||
.card-theme-wood .card-muted { color: rgba(245,230,200,0.65) !important; }
|
||
.card-theme-wood .card-bg-chip { background: rgba(160,100,40,0.3) !important; border: 1px solid rgba(160,100,40,0.6) !important; }
|
||
|
||
/* ── Theme: MIDNIGHT BLUE ── */
|
||
.card-theme-midnight {
|
||
background: linear-gradient(135deg, #020818 0%, #050e2a 60%, #030b1a 100%) !important;
|
||
border: 1.5px solid rgba(100,160,255,0.4);
|
||
box-shadow: 0 0 30px rgba(60,120,255,0.15), inset 0 1px 0 rgba(100,160,255,0.08);
|
||
color: #d0e4ff !important;
|
||
}
|
||
.card-theme-midnight .card-accent { color: #64a0ff !important; }
|
||
.card-theme-midnight .card-accent2 { color: #ccff00 !important; }
|
||
.card-theme-midnight .card-stripe { background: linear-gradient(90deg, #3060cc, #64a0ff) !important; }
|
||
.card-theme-midnight .card-muted { color: rgba(208,228,255,0.6) !important; }
|
||
.card-theme-midnight .card-bg-chip { background: rgba(100,160,255,0.15) !important; border: 1px solid rgba(100,160,255,0.4) !important; }
|
||
|
||
/* ── Theme: SUNSET (orange/pink) ── */
|
||
.card-theme-sunset {
|
||
background: linear-gradient(135deg, #120006 0%, #1a0808 40%, #0e0810 100%) !important;
|
||
border: 1.5px solid rgba(255,80,0,0.5);
|
||
box-shadow: 0 0 30px rgba(255,60,0,0.2), inset 0 1px 0 rgba(255,150,50,0.08);
|
||
color: #ffd0a0 !important;
|
||
}
|
||
.card-theme-sunset .card-accent { color: #ff6020 !important; }
|
||
.card-theme-sunset .card-accent2 { color: #ff0080 !important; }
|
||
.card-theme-sunset .card-stripe { background: linear-gradient(90deg, #ff0040, #ff6020, #ffaa00) !important; }
|
||
.card-theme-sunset .card-muted { color: rgba(255,208,160,0.65) !important; }
|
||
.card-theme-sunset .card-bg-chip { background: rgba(255,80,0,0.2) !important; border: 1px solid rgba(255,80,0,0.5) !important; }
|
||
|
||
/* ── Custom accent: overrides the theme's primary accent (applied on top of
|
||
any theme via --ss-accent / --ss-accent2 set inline on the card). Placed
|
||
last so it wins at equal specificity. ── */
|
||
.card-custom-accent .card-accent { color: var(--ss-accent) !important; }
|
||
.card-custom-accent .card-stripe { background: linear-gradient(90deg, var(--ss-accent), var(--ss-accent2, var(--ss-accent))) !important; }
|
||
.card-custom-accent .card-bg-chip { border-color: var(--ss-accent) !important; }
|
||
|
||
/* ── Card back side ── */
|
||
.rider-card-back {
|
||
background: #0b0b0b !important;
|
||
color: #e8e8e8 !important;
|
||
}
|
||
|
||
/* ── Print: the card is rendered to PNG via html2canvas before printing.
|
||
The @media print block below is only a safety fallback for direct Ctrl+P.
|
||
It will never look as good as the canvas approach. ── */
|
||
@media print {
|
||
@page { size: 85.6mm 54mm; margin: 0; }
|
||
body > * { display: none !important; }
|
||
.rider-card-print-fallback {
|
||
display: block !important;
|
||
position: fixed !important;
|
||
top: 0; left: 0;
|
||
width: 85.6mm !important;
|
||
height: 54mm !important;
|
||
}
|
||
}
|
||
|
||
/* ── Dark basemap without a keyed tile provider ──
|
||
We use free OpenStreetMap standard tiles and darken them client-side with a
|
||
CSS filter so the map matches the app's dark UI. Applied via the tileLayer
|
||
`className: 'skatespots-dark-tiles'` in SpotMap.tsx. */
|
||
.skatespots-dark-tiles {
|
||
filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(0.9) saturate(0.7);
|
||
}
|
||
|
||
/* ── Calculator sheet tables ──────────────────────────────────────────────
|
||
The deck-cost tables carry 5–8 columns (name + numbers + per-board + row
|
||
actions). That cannot fit a 390px phone: values get clipped ("900" → "90")
|
||
and columns fall off the edge. On phones each row becomes a stacked card
|
||
(label left, control right); on sm+ it stays a real table. `td::before`
|
||
pulls the column title from `data-label`. */
|
||
@media (max-width: 639px) {
|
||
.ss-sheet-table, .ss-sheet-table tbody, .ss-sheet-table tr, .ss-sheet-table td { display: block; width: 100%; }
|
||
.ss-sheet-table thead { display: none; }
|
||
.ss-sheet-table tr {
|
||
margin-bottom: 0.5rem;
|
||
padding: 0.5rem 0.625rem;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
border-radius: 0.75rem;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
}
|
||
.ss-sheet-table td {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.75rem;
|
||
padding: 0.2rem 0;
|
||
border: 0;
|
||
}
|
||
.ss-sheet-table td[data-label]::before {
|
||
content: attr(data-label);
|
||
flex: 0 0 auto;
|
||
color: #6b7280;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
}
|
||
.ss-sheet-table td > input,
|
||
.ss-sheet-table td > div { max-width: 62%; }
|
||
.ss-sheet-table td.ss-cell-name { margin-bottom: 0.35rem; }
|
||
.ss-sheet-table td.ss-cell-name > input { max-width: 100%; }
|
||
.ss-sheet-table td.ss-cell-actions { justify-content: flex-end; padding-top: 0.35rem; }
|
||
}
|