Turo (iOS) — Jetpack Compose Implementation Guide
Companion to DESIGN.md — the framework-neutral spec. This file ports Turo’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables for the signature photo hero, host card, and sticky Book bar, navigation, motion, and haptics.
Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps Turo’s visual identity (the full-bleed car photo as the product, Turo Purple as the booking action, Turo Teal as the trust accent, the host card, the pinned Book bar, the near-black canvas) while making everything idiomatic Android —
NavigationBarinstead of a UITabBar,HorizontalPagerfor the carousel,dp/spinstead ofpt.
Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, Coil 2.6+ for car photos and host avatars, and androidx.compose.foundation.pager. No color extraction — Turo’s palette is the fixed purple/teal brand. The dark scheme is the primary catalog rendering; a full light scheme is provided.
1. Color Tokens
// ui/theme/TuroColors.kt
import androidx.compose.ui.graphics.Color
object TuroColors {
// Brand
val Purple = Color(0xFF593BFB) // booking action
val PurpleBright = Color(0xFF7C5CFF) // link / gradient hi
val PurplePressed = Color(0xFF4A2FD6)
val Teal = Color(0xFF5CE0B8) // trust / value
val TealDeep = Color(0xFF1FB890)
// Canvas & Surfaces (Light)
val Canvas = Color(0xFFFFFFFF)
val Surface1 = Color(0xFFF6F5FA)
val Surface2 = Color(0xFFFFFFFF)
val Pressed = Color(0xFFECEAF4)
val Divider = Color(0xFFE6E4EF)
// Canvas & Surfaces (Dark) — near-black so photos pop
val DarkCanvas = Color(0xFF0F0F12)
val DarkSurface1 = Color(0xFF1A1A1F)
val DarkSurface2 = Color(0xFF25252C)
val DarkDivider = Color(0xFF2D2D35)
// Text
val TextPrimary = Color(0xFF15131F)
val TextSecondary = Color(0xFF6B6878)
val TextTertiary = Color(0xFF9C99AB)
val DarkTextPrimary = Color(0xFFF2F1F6)
val DarkTextSecondary = Color(0xFFA4A2B3)
val DarkTextTertiary = Color(0xFF6F6D7E)
val OnPurple = Color(0xFFFFFFFF)
val OnTeal = Color(0xFF06231B)
// Semantic
val Success = Color(0xFF1FB890)
val SuccessDark = Color(0xFF5CE0B8)
val Error = Color(0xFFF5536B)
val Star = Color(0xFFFFB400)
}
Wire it into both schemes. Turo’s dark canvas is near-black #0F0F12 — never a tinted gray — so photography pops.
// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
private val TuroLight = lightColorScheme(
primary = TuroColors.Purple, // booking action
onPrimary = TuroColors.OnPurple,
secondary = TuroColors.Teal,
onSecondary = TuroColors.OnTeal,
background = TuroColors.Canvas,
onBackground = TuroColors.TextPrimary,
surface = TuroColors.Surface2,
onSurface = TuroColors.TextPrimary,
surfaceVariant = TuroColors.Surface1,
outline = TuroColors.Divider,
error = TuroColors.Error,
)
private val TuroDark = darkColorScheme(
primary = TuroColors.Purple, // identical across themes
onPrimary = TuroColors.OnPurple,
secondary = TuroColors.Teal,
onSecondary = TuroColors.OnTeal,
background = TuroColors.DarkCanvas,
onBackground = TuroColors.DarkTextPrimary,
surface = TuroColors.DarkSurface1,
onSurface = TuroColors.DarkTextPrimary,
surfaceVariant = TuroColors.DarkSurface2,
outline = TuroColors.DarkDivider,
error = TuroColors.Error,
)
@Composable
fun TuroTheme(
dark: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) = MaterialTheme(
colorScheme = if (dark) TuroDark else TuroLight,
typography = TuroTypography,
content = content,
)
2. Typography (M3)
Turo’s brand face is a geometric humanist sans. License it and drop the TTFs in res/font/, or ship Manrope (SIL OFL) — keep the named ramp identical. Prices/counts are tabular (fontFeatureSettings = "tnum").
// ui/theme/TuroType.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val TuroSans = FontFamily(
Font(R.font.turo_regular, FontWeight.Normal),
Font(R.font.turo_medium, FontWeight.Medium),
Font(R.font.turo_semibold, FontWeight.SemiBold),
Font(R.font.turo_bold, FontWeight.Bold),
Font(R.font.turo_extrabold, FontWeight.ExtraBold),
)
private const val TNUM = "tnum"
// Named ramp — mirrors DESIGN.md §3 (pt → sp 1:1)
object TuroText {
val Display = TextStyle(TuroSans, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 37.sp, letterSpacing = (-0.4).sp)
val PriceHero = TextStyle(TuroSans, fontWeight = FontWeight.ExtraBold, fontSize = 26.sp, lineHeight = 29.sp, letterSpacing = (-0.3).sp, fontFeatureSettings = TNUM)
val CarTitle = TextStyle(TuroSans, fontWeight = FontWeight.ExtraBold, fontSize = 23.sp, lineHeight = 28.sp, letterSpacing = (-0.3).sp)
val Section = TextStyle(TuroSans, fontWeight = FontWeight.Bold, fontSize = 18.sp, lineHeight = 24.sp, letterSpacing = (-0.1).sp)
val Body = TextStyle(TuroSans, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp)
val BodyEmph = TextStyle(TuroSans, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, lineHeight = 24.sp)
val CardTitle = TextStyle(TuroSans, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 20.sp)
val CardPrice = TextStyle(TuroSans, fontWeight = FontWeight.ExtraBold, fontSize = 14.sp, lineHeight = 17.sp, fontFeatureSettings = TNUM)
val Meta = TextStyle(TuroSans, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp)
val Eyebrow = TextStyle(TuroSans, fontWeight = FontWeight.Bold, fontSize = 13.sp, lineHeight = 16.sp, letterSpacing = 0.5.sp)
val Caption = TextStyle(TuroSans, fontWeight = FontWeight.Medium, fontSize = 12.sp, lineHeight = 16.sp)
val Button = TextStyle(TuroSans, fontWeight = FontWeight.ExtraBold, fontSize = 16.sp, lineHeight = 16.sp, letterSpacing = 0.1.sp)
val Tab = TextStyle(TuroSans, fontWeight = FontWeight.SemiBold, fontSize = 10.sp, lineHeight = 11.sp, letterSpacing = 0.1.sp)
val RatingNum = TextStyle(TuroSans, fontWeight = FontWeight.Bold, fontSize = 13.sp, lineHeight = 13.sp, fontFeatureSettings = TNUM)
}
val TuroTypography = Typography(
displayLarge = TuroText.Display,
headlineLarge = TuroText.CarTitle,
titleMedium = TuroText.Section,
bodyLarge = TuroText.Body,
labelSmall = TuroText.Tab,
)
3. Signature Components
Photo Hero (full-bleed HorizontalPager)
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
import coil.compose.AsyncImage
@Composable
fun CarPhotoHero(
photos: List<String>,
isFavorite: Boolean,
onToggleFavorite: () -> Unit,
onBack: () -> Unit,
onShare: () -> Unit,
) {
val pager = rememberPagerState { photos.size }
Box(Modifier.fillMaxWidth().height(260.dp).background(TuroColors.DarkSurface2)) {
HorizontalPager(state = pager, modifier = Modifier.fillMaxSize()) { page ->
AsyncImage(
model = photos[page], contentDescription = "Photo ${page + 1} of ${photos.size}",
modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop,
)
}
// Page dots — active grows to an 18dp pill
Row(
Modifier.align(Alignment.BottomCenter).padding(bottom = 14.dp),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
repeat(photos.size) { i ->
val w by animateDpAsState(if (i == pager.currentPage) 18.dp else 6.dp, label = "dot")
Box(Modifier.size(width = w, height = 6.dp).clip(RoundedCornerShape(3.dp))
.background(if (i == pager.currentPage) Color.White else Color.White.copy(alpha = 0.45f)))
}
}
// Floating controls
Row(
Modifier.align(Alignment.TopCenter).fillMaxWidth().padding(horizontal = 16.dp, vertical = 56.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
HeroBtn(Icons.Filled.ArrowBack, "Back", onBack)
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
HeroBtn(Icons.Filled.IosShare, "Share", onShare)
HeroBtn(
if (isFavorite) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
"Favorite", onToggleFavorite,
tint = if (isFavorite) TuroColors.Teal else Color.White,
)
}
}
}
}
@Composable
private fun HeroBtn(icon: androidx.compose.ui.graphics.vector.ImageVector,
desc: String, onClick: () -> Unit, tint: Color = Color.White) {
Box(
Modifier.size(38.dp).clip(CircleShape).background(Color(0x8C0F0F12))
.clickable { onClick() },
contentAlignment = Alignment.Center,
) { Icon(icon, desc, tint = tint, modifier = Modifier.size(18.dp)) }
}
Specs Strip
import androidx.compose.foundation.border
import androidx.compose.material3.Text
data class CarSpec(val icon: androidx.compose.ui.graphics.vector.ImageVector, val value: String, val label: String)
@Composable
fun SpecsStrip(specs: List<CarSpec>, modifier: Modifier = Modifier) {
Row(modifier.padding(horizontal = 20.dp), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
specs.forEach { s ->
Column(
Modifier.weight(1f).clip(RoundedCornerShape(12.dp))
.background(TuroColors.DarkSurface1)
.border(1.dp, TuroColors.DarkDivider, RoundedCornerShape(12.dp))
.padding(vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(s.icon, null, tint = TuroColors.Teal, modifier = Modifier.size(18.dp))
Text(s.value, style = TuroText.RatingNum, color = TuroColors.DarkTextPrimary)
Text(s.label, style = TuroText.Caption.copy(fontSize = 10.sp), color = TuroColors.DarkTextSecondary)
}
}
}
}
Host Card
import androidx.compose.ui.graphics.Brush
@Composable
fun HostCard(
initials: String, name: String, allStar: Boolean, subline: String,
onClick: () -> Unit, modifier: Modifier = Modifier,
) {
Row(
modifier
.padding(horizontal = 20.dp)
.clip(RoundedCornerShape(14.dp))
.background(TuroColors.DarkSurface1)
.border(1.dp, TuroColors.DarkDivider, RoundedCornerShape(14.dp))
.clickable { onClick() }
.padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Box(
Modifier.size(46.dp).clip(CircleShape)
.background(Brush.linearGradient(listOf(TuroColors.PurpleBright, TuroColors.Teal))),
contentAlignment = Alignment.Center,
) { Text(initials, style = TuroText.CardTitle.copy(fontWeight = FontWeight.ExtraBold, fontSize = 17.sp), color = Color.White) }
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Text(name, style = TuroText.CardTitle, color = TuroColors.DarkTextPrimary)
if (allStar) {
Row(
Modifier.clip(RoundedCornerShape(50)).background(TuroColors.Teal.copy(alpha = 0.16f))
.padding(horizontal = 8.dp, vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(Icons.Filled.Star, null, tint = TuroColors.Teal, modifier = Modifier.size(9.dp))
Text("ALL-STAR HOST", style = TuroText.Caption.copy(fontWeight = FontWeight.ExtraBold, fontSize = 10.sp), color = TuroColors.Teal)
}
}
}
Text(subline, style = TuroText.Caption, color = TuroColors.DarkTextSecondary)
}
Icon(Icons.Filled.ChevronRight, null, tint = TuroColors.DarkTextTertiary, modifier = Modifier.size(14.dp))
}
}
Sticky Price + Book Bar
@Composable
fun BookBar(
dailyRate: String, estTotal: String,
onTotalClick: () -> Unit, onContinue: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier
.fillMaxWidth()
.background(Color(0xF50F0F12)) // blurred-glass analog; Android has no live blur
.drawBehind {
drawLine(TuroColors.DarkDivider, androidx.compose.ui.geometry.Offset(0f, 0f),
androidx.compose.ui.geometry.Offset(size.width, 0f), 0.5.dp.toPx())
}
.navigationBarsPadding()
.padding(horizontal = 20.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Row(verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text(dailyRate, style = TuroText.PriceHero.copy(fontSize = 20.sp), color = TuroColors.DarkTextPrimary)
Text("/ day", style = TuroText.Caption.copy(fontSize = 13.sp), color = TuroColors.DarkTextSecondary)
}
Text(estTotal, style = TuroText.Caption.copy(fontSize = 11.sp,
textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline),
color = TuroColors.Teal, modifier = Modifier.clickable { onTotalClick() })
}
androidx.compose.material3.Button(
onClick = onContinue,
shape = RoundedCornerShape(10.dp),
colors = ButtonDefaults.buttonColors(containerColor = TuroColors.Purple, contentColor = TuroColors.OnPurple),
modifier = Modifier.height(44.dp),
) { Text("Continue", style = TuroText.Button) }
}
}
Map Price Pin
enum class PinState { Default, Visited, Selected }
@Composable
fun MapPricePin(price: String, state: PinState = PinState.Default, modifier: Modifier = Modifier) {
val bg = when (state) { PinState.Default -> TuroColors.Purple; PinState.Selected -> Color.White; PinState.Visited -> TuroColors.DarkSurface2 }
val fg = when (state) { PinState.Default -> Color.White; PinState.Selected -> TuroColors.Purple; PinState.Visited -> TuroColors.DarkTextSecondary }
Box(
modifier
.shadow(8.dp, RoundedCornerShape(50), spotColor = Color.Black.copy(alpha = 0.5f))
.clip(RoundedCornerShape(50))
.background(bg)
.then(if (state == PinState.Visited) Modifier.border(1.dp, TuroColors.DarkDivider, RoundedCornerShape(50)) else Modifier)
.padding(horizontal = 12.dp, vertical = 7.dp),
) { Text(price, style = TuroText.RatingNum.copy(fontWeight = FontWeight.ExtraBold), color = fg) }
}
Primary Button
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.interaction.MutableInteractionSource
@Composable
fun TuroPrimaryButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
Button(
onClick = onClick,
interactionSource = interaction,
shape = RoundedCornerShape(10.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (pressed) TuroColors.PurplePressed else TuroColors.Purple,
contentColor = TuroColors.OnPurple,
),
modifier = modifier.fillMaxWidth().height(52.dp),
) { Text(title, style = TuroText.Button) }
}
4. Navigation
Turo has a 5-tab bottom strip; active state is a Turo Purple tint with no Material pill (Turo has none).
@Composable
fun TuroBottomBar(selected: Int, onSelect: (Int) -> Unit) {
NavigationBar(containerColor = TuroColors.DarkCanvas, tonalElevation = 0.dp) {
val items = listOf(
"Search" to Icons.Filled.Search,
"Trips" to Icons.Filled.DirectionsCar,
"Favorites" to Icons.Filled.FavoriteBorder,
"Inbox" to Icons.Filled.ChatBubbleOutline,
"More" to Icons.Filled.Menu,
)
items.forEachIndexed { i, (label, icon) ->
NavigationBarItem(
selected = selected == i,
onClick = { onSelect(i) },
icon = { Icon(icon, label, Modifier.size(22.dp)) },
label = { Text(label, style = TuroText.Tab) },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = TuroColors.Purple, // Turo Purple active
selectedTextColor = TuroColors.Purple,
unselectedIconColor = TuroColors.DarkTextTertiary,
unselectedTextColor = TuroColors.DarkTextTertiary,
indicatorColor = Color.Transparent, // no pill — Turo has none
),
)
}
}
}
The top bar over the photo hero is just the floating circular controls (no app bar); a non-photo screen uses a transparent CenterAlignedTopAppBar over the canvas. The price-breakdown, filters, and date picker are ModalBottomSheets; the listing-open card→hero transition uses a shared-element/AnimatedContent bounds transform.
5. Motion
Turo motion is calm — the photo carries the richness. The favorite pop and the card→hero expand are the expressive moments; everything else is quiet 120–320ms ease-out.
| Moment | Compose recipe |
|---|---|
| Carousel page dot morph | animateDpAsState width 6 → 18 tween(200); soft selection haptic on currentPage change |
| Favorite tap | animateFloatAsState/keyframes scale 1 → 1.25 → 1; color crossfade to Teal; light haptic |
| Listing open | shared-element Modifier.sharedBounds / AnimatedContent — card photo → 260.dp hero tween(320, EaseOut) |
| Map pin select | animateFloatAsState scale 1 → 1.12 tween(150) + recolor to white selected |
| Filter chip toggle | animateColorAsState(tween(150)) to Purple |
| Book bar CTA press | collectIsPressedAsState → Purple → PurplePressed + scale 0.98 tween(120) |
| Tab change | instant tint to Purple; no indicator slide (no pill exists) |
| Bottom sheet | ModalBottomSheet slide + tween(320) scrim |
| Page navigation | Nav slide push tween(300) |
| Booking confirmed | success haptic + check animation |
// Favorite pop — the expressive Turo moment
val favScale by animateFloatAsState(
targetValue = if (favPulse) 1.25f else 1f,
animationSpec = tween(140), label = "fav",
finishedListener = { favPulse = false },
)
LaunchedEffect(isFavorite) {
if (isFavorite) haptics.performHapticFeedback(HapticFeedbackType.LongPress)
}
Haptics: LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) for the favorite/booking confirm; LocalView.current.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) for the carousel paging tick and CTA press.
6. Icons
The closest first-party set is androidx.compose.material:material-icons-extended. Host avatars are Coil-loaded images, not icons.
| Purpose | SF Symbol (iOS) | Material Icon (Compose) |
|---|---|---|
| Search (tab) | magnifyingglass |
Icons.Filled.Search |
| Trips (tab) | car.fill |
Icons.Filled.DirectionsCar |
| Favorites (tab) | heart / heart.fill |
Icons.Filled.FavoriteBorder / Favorite |
| Inbox (tab) | bubble.left |
Icons.Filled.ChatBubbleOutline |
| More (tab) | line.3.horizontal |
Icons.Filled.Menu |
| Back | chevron.left |
Icons.Filled.ArrowBack |
| Share | square.and.arrow.up |
Icons.Filled.IosShare |
| Favorite | heart / heart.fill |
Icons.Filled.FavoriteBorder / Favorite |
| Star rating | star.fill |
Icons.Filled.Star |
| Instant book | bolt.fill |
Icons.Filled.Bolt |
| Fuel / electric | fuelpump.fill |
Icons.Filled.LocalGasStation / ElectricCar |
| Seats | person.2.fill |
Icons.Filled.AirlineSeatReclineNormal |
| Transmission | gearshape.fill |
Icons.Filled.Settings |
| Doors | car.side |
Icons.Filled.DirectionsCar |
| Location | mappin.and.ellipse |
Icons.Filled.LocationOn |
| Trip dates | calendar |
Icons.Filled.CalendarMonth |
| Chevron (row) | chevron.right |
Icons.Filled.ChevronRight |
| Filters | slider.horizontal.3 |
Icons.Filled.Tune |
7. Minimum SDK & Accessibility Notes
- minSdk 24 (Compose floor is 21;
HorizontalPager, shared-element, modern motion comfortable at 24).compileSdk 34+, Compose BOM2024.09+, Kotlin2.0+. - Edge-to-edge: call
enableEdgeToEdge(); over the photo hero the system bars are transparent with light icons; pin the Book bar above the nav bar withModifier.navigationBarsPadding(); floating controls offset for the camera cutout. - Tabular numerals: keep
fontFeatureSettings = "tnum"on prices, est. totals, trip counts, and ratings (already in the ramp). - Font scaling:
sphonors the user’s font scale — keep it on display, car title, sections, body, card titles, prices. Pin layout-critical text (tab labels, badge eyebrows, rating number) by deriving fromdpor wrapping inCompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)); the photo hero never scales. - One Purple per screen:
TuroColors.Purpleis the single booking-CTA fill; trust badges/savings useTuroColors.Teal/secondary. Enforce in review. - Photos sacred: never tint or color-block Coil car images — only thin top/bottom legibility gradients via a
Brush.verticalGradientoverlay. - TalkBack: the photo hero gets
contentDescription“Photo 1 of 8, Tesla Model 3”; the host card a full trust summary (“Host Marcus J., All-Star Host, 214 trips, responds in about 10 minutes”); the Book CTA a label including rate + est. total. Pair the All-Star badge and trip status with icon + text — never color alone. - Touch targets: Material guidance is 48.dp. The primary/Book CTA is 52.dp (44.dp compact); give the 38.dp floating controls a 48.dp ripple bound; cards are full-card clickable; map pins ≥ 44.dp hit.
- Contrast: white on
#593BFBand#06231Bon#5CE0B8pass WCAG AA; on the near-black canvas use#F2F1F6text and the brightened#7C5CFFlink. - Reduce motion: when
Settings.Global.ANIMATOR_DURATION_SCALE == 0, disable the card→hero expand and the favorite pop (substitute crossfades); keep the success haptic. - Dark mode: the canvas is near-black
#0F0F12, NOT a tinted gray, so photos pop;#15131Ftext becomes#F2F1F6. Shadows read poorly on near-black, so the 1.dpDarkDividerborder carries card elevation. Do not enable Material YoudynamicColorScheme()— Turo’s purple/teal identity must hold regardless of wallpaper.