WFDASHDRIVER-APP/app/globals.css
SJCoetz 3c8fc91b3e feat: add complete dark mode support with settings toggle
- 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
2026-07-07 18:28:19 +02:00

776 lines
No EOL
23 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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