United (iOS) — Jetpack Compose Implementation Guide
Companion to DESIGN.md — the framework-neutral spec. This file ports United’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables for the boarding pass + interactive seat map, navigation, motion, and haptics.
Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (the two-blue United Blue / Rhapsody Blue brand, the perforated boarding pass, the interactive seat map, refined functional motion) while making everything idiomatic Android —
NavigationBarinstead of a UITabBar,sp/dpinstead ofpt.
Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+. No color extraction — United’s palette is a fixed two-blue brand system. United is light-mode-first; a full deep-navy dark scheme is provided.
1. Color Tokens (Compose + M3)
// ui/theme/UAColors.kt
import androidx.compose.ui.graphics.Color
object UAColors {
// Brand
val Blue = Color(0xFF002244)
val BlueMid = Color(0xFF1B3D6E)
val Rhapsody = Color(0xFF1414FF)
val RhapsodyPress = Color(0xFF0E0EC8)
val RhapsodyBright = Color(0xFF4A4AFF)
val PremierGold = Color(0xFFC2A14D)
val PremierGoldBright = Color(0xFFD8B863)
// Canvas & Surfaces (Light)
val Canvas = Color(0xFFFFFFFF)
val SurfaceGray = Color(0xFFF3F5FA)
val SurfacePressed = Color(0xFFE5EAF3)
val DividerLight = Color(0xFFD4DCE9)
// Canvas & Surfaces (Dark) — deep navy, NOT pure black
val DarkCanvas = Color(0xFF0A1424)
val DarkSurface1 = Color(0xFF111E33)
val DarkSurface2 = Color(0xFF1A2A45)
val DarkDivider = Color(0xFF243652)
// Text
val TextPrimary = Color(0xFF0C1F38)
val TextSecondary = Color(0xFF566884)
val TextTertiary = Color(0xFF8493AC)
val DarkTextPrimary = Color(0xFFE9EEF6)
val DarkTextSecondary = Color(0xFF97A6BE)
val DarkTextTertiary = Color(0xFF5E7090)
// Flight status (functional, constant)
val OnTime = Color(0xFF1E8E5A)
val OnTimeDark = Color(0xFF4FD18C)
val Boarding = Color(0xFF1414FF)
val BoardingDark = Color(0xFF7A7AFF)
val Delayed = Color(0xFFC98A1E)
val DelayedDark = Color(0xFFE0A943)
val Canceled = Color(0xFFD8434F)
val CanceledDark = Color(0xFFF0808A)
}
Wire it into both schemes. United is light-first; the dark scheme uses the signature #0A1424 deep navy, never true black, so the brand navy survives.
// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.MaterialTheme
private val UALight = lightColorScheme(
primary = UAColors.Rhapsody, // action accent
onPrimary = Color.White,
secondary = UAColors.Blue, // brand/surface
onSecondary = Color.White,
background = UAColors.Canvas,
onBackground = UAColors.TextPrimary,
surface = UAColors.SurfaceGray,
onSurface = UAColors.TextPrimary,
surfaceVariant = UAColors.SurfacePressed,
outline = UAColors.DividerLight,
error = UAColors.Canceled,
)
private val UADark = darkColorScheme(
primary = UAColors.Rhapsody,
onPrimary = Color.White,
secondary = UAColors.BlueMid,
onSecondary = Color.White,
background = UAColors.DarkCanvas,
onBackground = UAColors.DarkTextPrimary,
surface = UAColors.DarkSurface1,
onSurface = UAColors.DarkTextPrimary,
surfaceVariant = UAColors.DarkSurface2,
outline = UAColors.DarkDivider,
error = UAColors.CanceledDark,
)
@Composable
fun UATheme(
dark: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) = MaterialTheme(
colorScheme = if (dark) UADark else UALight,
typography = UATypography,
content = content,
)
2. Typography (M3)
United uses a clean humanist corporate sans; Open Sans (Apache 2.0) is the closest free analog. Drop the TTFs in res/font/. Data is bold and big; the weight ladder is 400/600/700/800.
// ui/theme/UAType.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 OpenSans = FontFamily(
Font(R.font.opensans_regular, FontWeight.Normal),
Font(R.font.opensans_medium, FontWeight.Medium),
Font(R.font.opensans_semibold, FontWeight.SemiBold),
Font(R.font.opensans_bold, FontWeight.Bold),
Font(R.font.opensans_extrabold, FontWeight.ExtraBold),
)
object UAText {
val RouteIATA = TextStyle(OpenSans, fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 34.sp, letterSpacing = (-0.5).sp)
val ScreenTitle = TextStyle(OpenSans, fontWeight = FontWeight.ExtraBold, fontSize = 26.sp, lineHeight = 30.sp, letterSpacing = (-0.3).sp)
val Section = TextStyle(OpenSans, fontWeight = FontWeight.Bold, fontSize = 22.sp, lineHeight = 26.sp, letterSpacing = (-0.2).sp)
val PaxName = TextStyle(OpenSans, fontWeight = FontWeight.Bold, fontSize = 18.sp, lineHeight = 22.sp)
val DataLarge = TextStyle(OpenSans, fontWeight = FontWeight.ExtraBold, fontSize = 22.sp, lineHeight = 24.sp)
val Body = TextStyle(OpenSans, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp)
val DataInline = TextStyle(OpenSans, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 20.sp)
val Meta = TextStyle(OpenSans, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp)
val Eyebrow = TextStyle(OpenSans, fontWeight = FontWeight.Bold, fontSize = 12.sp, lineHeight = 12.sp, letterSpacing = 0.6.sp)
val CellLabel = TextStyle(OpenSans, fontWeight = FontWeight.Bold, fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.6.sp)
val Button = TextStyle(OpenSans, fontWeight = FontWeight.Bold, fontSize = 16.sp, lineHeight = 16.sp, letterSpacing = 0.2.sp)
val Tab = TextStyle(OpenSans, fontWeight = FontWeight.SemiBold, fontSize = 10.sp, lineHeight = 10.sp, letterSpacing = 0.1.sp)
val StatusPill = TextStyle(OpenSans, fontWeight = FontWeight.Bold, fontSize = 12.sp, lineHeight = 12.sp, letterSpacing = 0.2.sp)
}
val UATypography = Typography(
headlineLarge = UAText.ScreenTitle,
headlineMedium = UAText.Section,
titleMedium = UAText.PaxName,
bodyMedium = UAText.Body,
labelSmall = UAText.Tab,
)
3. Signature Components (@Composable)
Boarding Pass (United Blue header + perforation)
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
@Composable
fun UABoardingPass(
paxName: String, gate: String, seat: String, boards: String,
premierGroup: String, dark: Boolean = true,
) {
Column(
Modifier
.clip(RoundedCornerShape(16.dp))
.background(UAColors.DarkSurface1)
.border(1.dp, UAColors.DarkDivider, RoundedCornerShape(16.dp)),
) {
Row(
Modifier.fillMaxWidth().background(UAColors.Blue)
.padding(horizontal = 18.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text("◍ UNITED", style = UAText.RouteIATA.copy(fontSize = 13.sp), color = Color.White)
Text(premierGroup, style = UAText.Eyebrow, color = UAColors.PremierGoldBright)
}
Column(Modifier.padding(18.dp)) {
Text("PASSENGER", style = UAText.CellLabel, color = UAColors.DarkTextSecondary)
Text(paxName.uppercase(), style = UAText.PaxName, color = UAColors.DarkTextPrimary,
modifier = Modifier.padding(top = 2.dp))
Row(Modifier.padding(top = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
PassCell("GATE", gate, UAColors.DarkTextPrimary, Modifier.weight(1f))
PassCell("SEAT", seat, UAColors.DarkTextPrimary, Modifier.weight(1f))
PassCell("BOARDS", boards, UAColors.PremierGoldBright, Modifier.weight(1f))
}
Perforation(Modifier.padding(vertical = 16.dp))
BarcodeStripe(dark, Modifier.fillMaxWidth().height(54.dp))
}
}
}
@Composable
private fun PassCell(label: String, value: String, valueColor: Color, modifier: Modifier = Modifier) {
Column(modifier) {
Text(label, style = UAText.CellLabel, color = UAColors.DarkTextTertiary)
Text(value, style = UAText.DataLarge, color = valueColor, modifier = Modifier.padding(top = 3.dp))
}
}
/** Dashed line with circular notch cut-outs at the edges — the "tear here" affordance. */
@Composable
fun Perforation(modifier: Modifier = Modifier) {
Box(modifier.fillMaxWidth().height(20.dp), contentAlignment = Alignment.Center) {
Box(
Modifier.fillMaxWidth().height(1.dp).drawBehind {
drawLine(
color = UAColors.DarkDivider,
start = Offset(0f, size.height / 2),
end = Offset(size.width, size.height / 2),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 10f)),
)
},
)
Box(Modifier.size(20.dp).offset(x = (-28).dp).clip(androidx.compose.foundation.shape.CircleShape).background(UAColors.DarkCanvas)
.align(Alignment.CenterStart))
Box(Modifier.size(20.dp).offset(x = 28.dp).clip(androidx.compose.foundation.shape.CircleShape).background(UAColors.DarkCanvas)
.align(Alignment.CenterEnd))
}
}
/** High-contrast bars; inverts to light bars in dark mode so a gate scanner reads a dimmed screen. */
@Composable
fun BarcodeStripe(dark: Boolean, modifier: Modifier = Modifier) {
val bar = if (dark) UAColors.DarkTextPrimary else UAColors.TextPrimary
Row(modifier.clip(RoundedCornerShape(8.dp))) {
repeat(64) { i ->
Box(Modifier.width(if (i % 3 == 0) 3.dp else 1.dp).fillMaxHeight().background(bar))
Spacer(Modifier.width(2.dp))
}
}
}
Interactive Seat Map
import androidx.compose.foundation.clickable
import androidx.compose.runtime.*
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
enum class SeatKind { Standard, EconPlus, Taken, Selected, Aisle }
@Composable
fun SeatMap(rows: List<List<SeatKind>>) {
var selected by remember { mutableStateOf("7A") }
val haptics = LocalHapticFeedback.current
Column(
Modifier
.clip(RoundedCornerShape(16.dp)).background(UAColors.DarkSurface1)
.border(1.dp, UAColors.DarkDivider, RoundedCornerShape(16.dp)).padding(18.dp),
) {
Text("Choose your seat", style = UAText.DataInline.copy(fontWeight = FontWeight.Bold),
color = UAColors.DarkTextPrimary)
Text("Economy Plus · Row 7–9 · $selected selected", style = UAText.Meta,
color = UAColors.DarkTextSecondary, modifier = Modifier.padding(top = 4.dp, bottom = 16.dp))
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
rows.forEach { row ->
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
row.forEach { kind ->
SeatCell(kind, Modifier.weight(1f)) {
if (kind == SeatKind.Standard || kind == SeatKind.EconPlus) {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
}
}
}
}
}
}
Row(Modifier.padding(top = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Legend(UAColors.Rhapsody.copy(alpha = 0.4f), "Econ+")
Legend(UAColors.Rhapsody, "Selected")
Legend(UAColors.DarkCanvas, "Taken")
}
}
}
@Composable
private fun SeatCell(kind: SeatKind, modifier: Modifier = Modifier, onTap: () -> Unit) {
if (kind == SeatKind.Aisle) { Box(modifier.aspectRatio(1f)); return }
val fill = when (kind) {
SeatKind.Selected -> UAColors.Rhapsody
SeatKind.EconPlus -> UAColors.Rhapsody.copy(alpha = 0.18f)
SeatKind.Taken -> UAColors.DarkCanvas
else -> UAColors.DarkSurface2
}
val border = when (kind) {
SeatKind.Selected -> UAColors.RhapsodyBright
SeatKind.EconPlus -> UAColors.RhapsodyBright.copy(alpha = 0.4f)
else -> UAColors.DarkDivider
}
Box(
modifier
.aspectRatio(1f)
.clip(RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp, bottomStart = 3.dp, bottomEnd = 3.dp))
.background(fill)
.border(1.dp, border, RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp, bottomStart = 3.dp, bottomEnd = 3.dp))
.clickable { onTap() },
)
}
@Composable
private fun Legend(c: Color, t: String) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Box(Modifier.size(10.dp).clip(RoundedCornerShape(3.dp)).background(c))
Text(t, style = UAText.Eyebrow.copy(fontWeight = FontWeight.Normal, fontSize = 11.sp),
color = UAColors.DarkTextSecondary)
}
}
Flight Detail Header + Buttons + Status Chip
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.graphics.Brush
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Flight
@Composable
fun FlightDetailHeader(
flightNo: String, origin: String, originCity: String, dest: String,
destCity: String, dur: String, depRow: String, arrRow: String, status: String,
) {
Column(
Modifier
.fillMaxWidth()
.background(Brush.linearGradient(listOf(UAColors.Blue, Color(0xFF04152C))))
.padding(horizontal = 18.dp).padding(top = 8.dp, bottom = 18.dp),
) {
Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) {
Text(flightNo.uppercase(), style = UAText.Eyebrow.copy(letterSpacing = 1.sp),
color = Color(0xFF7FA0CE))
StatusChip(StatusKind.OnTime, status)
}
Row(Modifier.padding(top = 14.dp)) {
Column {
Text(origin, style = UAText.RouteIATA, color = Color.White)
Text(originCity, style = UAText.Meta, color = Color(0xFF9FBADC))
}
Column(Modifier.weight(1f).padding(top = 8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Icon(Icons.Filled.Flight, null, tint = Color.White, modifier = Modifier.size(20.dp))
Text(dur, style = UAText.Meta.copy(fontSize = 11.sp), color = Color(0xFF9FBADC))
}
Column(horizontalAlignment = Alignment.End) {
Text(dest, style = UAText.RouteIATA, color = Color.White)
Text(destCity, style = UAText.Meta, color = Color(0xFF9FBADC))
}
}
Row(Modifier.fillMaxWidth().padding(top = 6.dp), Arrangement.SpaceBetween) {
Text(depRow, style = UAText.DataInline, color = Color(0xFFCFE0F2))
Text(arrRow, style = UAText.DataInline, color = Color(0xFFCFE0F2))
}
}
}
enum class StatusKind { OnTime, Boarding, Delayed, Canceled }
@Composable
fun StatusChip(kind: StatusKind, label: String) {
val (fg, bg) = when (kind) {
StatusKind.OnTime -> UAColors.OnTimeDark to UAColors.OnTime.copy(alpha = 0.18f)
StatusKind.Boarding -> UAColors.BoardingDark to UAColors.Boarding.copy(alpha = 0.22f)
StatusKind.Delayed -> UAColors.DelayedDark to UAColors.Delayed.copy(alpha = 0.18f)
StatusKind.Canceled -> UAColors.CanceledDark to UAColors.Canceled.copy(alpha = 0.18f)
}
Text(label, style = UAText.StatusPill, color = fg,
modifier = Modifier.clip(CircleShape).background(bg).padding(horizontal = 14.dp, vertical = 7.dp))
}
@Composable
fun UAPrimaryButton(title: String, onClick: () -> Unit) {
androidx.compose.material3.Button(
onClick = onClick,
shape = RoundedCornerShape(4.dp), // crisp, low-radius
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
containerColor = UAColors.Rhapsody, contentColor = Color.White),
modifier = Modifier.fillMaxWidth().height(48.dp),
) { Text(title, style = UAText.Button) }
}
4. Navigation
United has minimal chrome: a 5-tab bottom strip. On Android model it as a NavigationBar — no tint pill, active is the Rhapsody-blue icon + label.
import androidx.compose.material3.*
import androidx.compose.material.icons.filled.*
@Composable
fun UABottomBar(selected: Int, onSelect: (Int) -> Unit) {
NavigationBar(containerColor = UAColors.DarkCanvas, tonalElevation = 0.dp) {
val items = listOf(
"Home" to Icons.Filled.Home,
"Book" to Icons.Filled.Search,
"Flights" to Icons.Filled.Flight,
"Trips" to Icons.Filled.Receipt,
"MileagePlus" to Icons.Filled.AccountCircle,
)
items.forEachIndexed { i, (label, icon) ->
NavigationBarItem(
selected = selected == i,
onClick = { onSelect(i) },
icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(22.dp)) },
label = { Text(label, style = UAText.Tab) },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = UAColors.RhapsodyBright,
selectedTextColor = UAColors.RhapsodyBright,
unselectedIconColor = UAColors.DarkTextTertiary,
unselectedTextColor = UAColors.DarkTextTertiary,
indicatorColor = Color.Transparent, // no Material pill — United has none
),
)
}
}
}
Use a NavHost for Home/Book/Flights/Trips/MileagePlus; a ModalBottomSheet for fare details and the upgrade list; push transitions are a 300ms horizontal slide.
5. Motion
United motion is refined — never decorative bounce. Shadows signal “the thing you act on / hold up at the gate”.
| Moment | Compose recipe |
|---|---|
| Aircraft glide (header) | animateFloatAsState 0 → 1 tween(900, easing = FastOutSlowInEasing) |
| Seat select | animateFloatAsState scale 0.9 → 1 spring(dampingRatio = 0.6f) + light haptic |
| Status change | pill color animateColorAsState(tween(250)); affected row tween(300); soft haptic |
| Boarding pass open | AnimatedVisibility scale-in spring(dampingRatio = 0.8f); raise screen brightness |
| Bag tracker advance | active dot fill tween(300) + light haptic on scan event |
| Tab switch | Crossfade(tween(150)) |
| Page navigation | Nav slide push tween(300) |
val seatScale by animateFloatAsState(if (selected) 1f else 0.9f,
animationSpec = spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessMedium), label = "seat")
val aircraftProgress by animateFloatAsState(if (visible) 1f else 0f,
animationSpec = tween(900, easing = FastOutSlowInEasing), label = "aircraftGlide")
Haptics: LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) for the soft impact on seat select and status change; on gate change use view.performHapticFeedback(HapticFeedbackConstants.CONFIRM). Brightness: cache and raise via WindowManager.LayoutParams.screenBrightness = 1f while the full-screen pass is shown; restore on dismiss. Auto-refresh is silent.
6. Icons
Closest first-party set is androidx.compose.material:material-icons-extended.
| Purpose | SF Symbol (iOS) | Material Icon (Compose) |
|---|---|---|
| Home (tab) | house.fill |
Icons.Filled.Home |
| Book (tab) | magnifyingglass |
Icons.Filled.Search |
| Flights (tab) | airplane |
Icons.Filled.Flight |
| Trips (tab) | rectangle.stack.fill |
Icons.Filled.Receipt |
| MileagePlus (tab) | person.crop.circle.fill |
Icons.Filled.AccountCircle |
| Aircraft (header) | airplane |
Icons.Filled.Flight |
| Departed | airplane.departure |
Icons.Filled.FlightTakeoff |
| Arrived | airplane.arrival |
Icons.Filled.FlightLand |
| Seat | chair.lounge.fill |
Icons.Filled.EventSeat |
| Bag tracker | suitcase.fill |
Icons.Filled.Luggage |
| Add to Wallet | wallet.pass |
Icons.Filled.AccountBalanceWallet |
| Boarding pass | qrcode |
Icons.Filled.QrCode2 |
| Upgrade list | arrow.up.circle.fill |
Icons.Filled.Upgrade |
| Gate change | exclamationmark.triangle.fill |
Icons.Filled.Warning |
| Premier tier | star.circle.fill |
Icons.Filled.Stars |
| Back | chevron.left |
Icons.AutoMirrored.Filled.ArrowBack |
| Share | square.and.arrow.up |
Icons.Filled.IosShare |
7. Minimum SDK & Accessibility Notes
- minSdk 24 (
Popup, modern motion comfortable at 24).compileSdk 34+, Compose BOM2024.09+, Kotlin2.0+. - Edge-to-edge: call
enableEdgeToEdge(); the navy flight-detail header draws under the status bar with light-content system icons; the boarding-pass barcode must stay above the navigation-bar inset. - Font scaling:
sphonors the user’s font scale — keep it on route IATA, screen title, section, body, meta. Pin layout-sensitive text (barcode, seat-map cells, 10sp cell labels, 10sp tab labels, status-pill text) viaCompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)). Data values scale one step then clamp to preserve the 3-cell grid. - TalkBack: label the pass “Boarding pass, {name}, gate {gate}, seat {seat}, boards {time}, {premierGroup}”; each seat “Seat {id}, {Economy Plus / standard / taken / selected}, {row}”; the status chip announces the full status word; the bag tracker announces the current step + carousel.
- Touch targets: Material guidance is 48.dp — primary button ≥ 48.dp; seat cells ≥ 32.dp tap (expand small cabins); list rows 52.dp full-row tappable.
- Contrast:
#E9EEF6on#0A1424and#0C1F38on#FFFFFFpass WCAG AA; the functional status pairs are validated against their translucent fills — re-check any custom amber pair. - Brightness: raise
WindowManager.LayoutParams.screenBrightness = 1fwhile the full-screen pass is visible (gate scanning); restore the user’s value on dismiss. - Reduce motion: when
Settings.Global.ANIMATOR_DURATION_SCALE == 0, disable the aircraft glide and seat-pop spring; keep the status color change as an instant swap and the selected-seat state (they convey state). - Dark mode: invert via the
Dark*palette —#0A1424, NOT true black, so the United navy survives; the barcode flips to a light bar pattern so it stays machine-scannable. Do not enable Material YoudynamicColorScheme()— United’s two-blue identity must hold regardless of wallpaper.