coderef

Overcast

Overcast is the audiophile's podcast app, designed by Marco Arment as a single-developer labor of love, and its iOS interface reflects that ethos: opinionated, restrained, fast, and built around two killer audio features.
DownloadRaw
Overcast

Build with the Overcast look.

Palette, type and components from this DESIGN.md, applied to a sample hero.

Palette

At a glance

Theme
light
Typefaces
SF Pro Text, SF Pro Display, SF Mono
Sections
8
Length
4,047 words
Platforms
Android, Expo, SwiftUI, DESIGN.md

Add it to your project

Then prompt your agent

Design System · Overcast

Light and dark previews, tokens and a ready-to-use package, generated from this DESIGN.md. No AI involved: the same file always produces the same output.

Generating design system…
DownloadRaw

Overcast (iOS) — Jetpack Compose Implementation Guide

Companion to DESIGN.md — the framework-neutral spec. This file ports Overcast’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables (the orange-ring play button, Now Playing player, Smart Speed / Voice Boost rows, playlist rows), core navigation, motion, and haptics.

Why a Compose guide for an iOS-only app? Overcast is iOS-exclusive, but the DESIGN.md tokens are platform-neutral and its identity ports cleanly: the warm paper-cream canvas, the single Overcast Orange accent, the outlined orange-ring play button, and Smart Speed / Voice Boost as first-class toggles. This file keeps that identity while making everything idiomatic Android — NavigationBar instead of a UITabBar, Slider for the scrubber, dp/sp instead of pt.

Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, Coil 2.6+ for cover images. Overcast is light-first (paper-cream); a full dark scheme is provided.

1. Color Tokens

// ui/theme/OCColors.kt
import androidx.compose.ui.graphics.Color

object OCColors {
    // Brand (single accent)
    val Orange        = Color(0xFFFC7E0F)
    val OrangePressed = Color(0xFFE06A00)
    val LinkBlue      = Color(0xFF2D7FF9)

    // Canvas & Surfaces (Light — paper)
    val Paper        = Color(0xFFFBFAF6) // warm off-white — NOT pure white
    val Cream        = Color(0xFFF7F5EF)
    val PressedLight = Color(0xFFEDEBE3)
    val DividerLight = Color(0xFFE4E1D8)

    // Canvas & Surfaces (Dark)
    val Canvas    = Color(0xFF121212)
    val Surface1  = Color(0xFF1C1C1E)
    val Surface2  = Color(0xFF2A2A2C)
    val DividerDk = Color(0xFF303032)

    // Text
    val TextPrimaryLt   = Color(0xFF1A1A1A)
    val TextSecondaryLt = Color(0xFF6E6E6E)
    val TextPrimaryDk   = Color(0xFFF2F2F2)
    val TextSecondaryDk = Color(0xFF9A9A9A)
    val TextTertiary    = Color(0xFF9A9A9A)

    // Semantic
    val Success = Color(0xFF34C759)
    val Error   = Color(0xFFFF3B30)
    val Warn    = Color(0xFFFF9F0A)

    // Playlist icon tiles
    val PlAll      = Color(0xFFFC7E0F)
    val PlProgress = Color(0xFF2D7FF9)
    val PlStarred  = Color(0xFF34C759)
    val PlDownload = Color(0xFF5856D6)
}

Wire it into both schemes. Overcast is light-first (paper-cream); the dark scheme uses a true #121212.

// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

private val OCLight = lightColorScheme(
    primary        = OCColors.Orange,
    onPrimary      = Color(0xFFFFFFFF),
    background      = OCColors.Paper,
    onBackground    = OCColors.TextPrimaryLt,
    surface         = OCColors.Cream,
    onSurface       = OCColors.TextPrimaryLt,
    surfaceVariant  = OCColors.PressedLight,
    outline         = OCColors.DividerLight,
    error           = OCColors.Error,
)

private val OCDark = darkColorScheme(
    primary        = OCColors.Orange,
    onPrimary      = Color(0xFFFFFFFF),
    background      = OCColors.Canvas,
    onBackground    = OCColors.TextPrimaryDk,
    surface         = OCColors.Surface1,
    onSurface       = OCColors.TextPrimaryDk,
    surfaceVariant  = OCColors.Surface2,
    outline         = OCColors.DividerDk,
    error           = OCColors.Error,
)

@Composable
fun OCTheme(
    dark: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) = MaterialTheme(
    colorScheme = if (dark) OCDark else OCLight,
    typography  = OCTypography,
    content     = content,
)

2. Typography (M3)

Overcast uses the platform system font (Roboto on Android stands in for SF Pro). Body 400, titles 700/800; timecodes render in a fixed-width box for tabular stability.

// ui/theme/OCType.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

object OCText {
    val ScreenTitle = TextStyle(fontWeight = FontWeight.ExtraBold, fontSize = 32.sp, lineHeight = 38.sp, letterSpacing = (-0.4).sp)
    val NowPlaying  = TextStyle(fontWeight = FontWeight.Bold,      fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = (-0.3).sp)
    val Section     = TextStyle(fontWeight = FontWeight.Bold,      fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = (-0.2).sp)
    val Episode     = TextStyle(fontWeight = FontWeight.Bold,      fontSize = 18.sp, lineHeight = 23.sp)
    val RowTitle    = TextStyle(fontWeight = FontWeight.SemiBold,  fontSize = 15.sp, lineHeight = 20.sp)
    val Body        = TextStyle(fontWeight = FontWeight.Normal,    fontSize = 16.sp, lineHeight = 24.sp)
    val ShowName    = TextStyle(fontWeight = FontWeight.Medium,    fontSize = 14.sp, lineHeight = 20.sp)
    val Meta        = TextStyle(fontWeight = FontWeight.Normal,    fontSize = 14.sp, lineHeight = 20.sp)
    val PlDesc      = TextStyle(fontWeight = FontWeight.Normal,    fontSize = 12.sp, lineHeight = 16.sp)
    val Button      = TextStyle(fontWeight = FontWeight.SemiBold,  fontSize = 16.sp, lineHeight = 16.sp)
    val Toggle      = TextStyle(fontWeight = FontWeight.SemiBold,  fontSize = 15.sp, lineHeight = 20.sp)
    val Chip        = TextStyle(fontWeight = FontWeight.SemiBold,  fontSize = 10.sp, lineHeight = 12.sp, letterSpacing = 0.1.sp)
    val Tab         = TextStyle(fontWeight = FontWeight.SemiBold,  fontSize = 10.sp, lineHeight = 12.sp, letterSpacing = 0.1.sp)
    val Count       = TextStyle(fontWeight = FontWeight.SemiBold,  fontSize = 14.sp, lineHeight = 16.sp)
    val SkipLabel   = TextStyle(fontWeight = FontWeight.Bold,      fontSize = 9.sp,  lineHeight = 10.sp)
    val StatCaption = TextStyle(fontWeight = FontWeight.SemiBold,  fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.4.sp)
    val Timecode    = TextStyle(fontWeight = FontWeight.Medium,    fontSize = 12.sp, lineHeight = 14.sp)
}

val OCTypography = Typography(
    headlineLarge  = OCText.ScreenTitle,
    headlineMedium = OCText.NowPlaying,
    titleMedium    = OCText.Episode,
    bodyMedium     = OCText.Body,
    labelSmall     = OCText.Tab,
)

For tabular timecodes, render durations/timecodes inside a fixed-width Box (or use the tnum font feature on API 26+) so digits never shift while scrubbing.

3. Signature Components

Orange-Ring Play Button (the signature control)

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun OCPlayRing(playing: Boolean, onToggle: () -> Unit, diameter: androidx.compose.ui.unit.Dp = 66.dp) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    val ring = if (pressed) OCColors.OrangePressed else OCColors.Orange

    Box(
        Modifier
            .size(diameter)
            .border(2.5.dp, ring, CircleShape) // outlined ring — never a filled disc
            .clickable(interaction, indication = null) { onToggle() },
        contentAlignment = Alignment.Center,
    ) {
        Icon(
            if (playing) Icons.Filled.Pause else Icons.Filled.PlayArrow,
            contentDescription = if (playing) "Pause" else "Play",
            tint = OCColors.Orange,
            modifier = Modifier.size(diameter * 0.36f),
        )
    }
}

Skip Button (chevron + interval label)

import androidx.compose.material.icons.filled.FastRewind
import androidx.compose.material.icons.filled.FastForward
import androidx.compose.material3.Text

@Composable
fun OCSkipButton(forward: Boolean, seconds: Int, primary: Color, secondary: Color, onClick: () -> Unit) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(2.dp),
        modifier = Modifier.clickable { onClick() },
    ) {
        Icon(
            if (forward) Icons.Filled.FastForward else Icons.Filled.FastRewind,
            contentDescription = if (forward) "Skip forward $seconds seconds" else "Skip back $seconds seconds",
            tint = primary, modifier = Modifier.size(27.dp),
        )
        Text("$seconds", style = OCText.SkipLabel, color = secondary)
    }
}

Now Playing Player

import androidx.compose.foundation.background
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush

@Composable
fun OCNowPlaying(episodeTitle: String, showName: String, dark: Boolean) {
    var progress by remember { mutableFloatStateOf(0.38f) }
    var playing by remember { mutableStateOf(true) }
    val canvas = if (dark) OCColors.Canvas else OCColors.Paper
    val primary = if (dark) OCColors.TextPrimaryDk else OCColors.TextPrimaryLt
    val secondary = if (dark) OCColors.TextSecondaryDk else OCColors.TextSecondaryLt
    val track = if (dark) OCColors.Surface2 else OCColors.DividerLight

    Column(Modifier.fillMaxSize().background(canvas)) {
        Row(
            Modifier.fillMaxWidth().padding(horizontal = 18.dp, vertical = 8.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween,
        ) {
            Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "Collapse", tint = OCColors.Orange)
            Text("Now Playing", style = OCText.RowTitle.copy(fontSize = 13.sp), color = primary)
            Icon(Icons.AutoMirrored.Filled.List, "Queue", tint = primary)
        }

        Column(
            Modifier.padding(horizontal = 34.dp, vertical = 6.dp)
                .clip(RoundedCornerShape(10.dp)),
        ) {
            Box(
                Modifier.fillMaxWidth().aspectRatio(1f)
                    .background(Brush.linearGradient(listOf(Color(0xFF2B3A55), Color(0xFF121A2E))))
            )
            Box(Modifier.fillMaxWidth().height(5.dp).background(OCColors.Orange))
        }

        Column(Modifier.padding(horizontal = 34.dp)) {
            Text(episodeTitle, style = OCText.NowPlaying, color = primary)
            Text(showName, style = OCText.ShowName, color = secondary, modifier = Modifier.padding(top = 4.dp))
        }

        Column(Modifier.padding(horizontal = 30.dp, vertical = 4.dp)) {
            Slider(
                value = progress, onValueChange = { progress = it },
                colors = SliderDefaults.colors(
                    thumbColor = OCColors.Orange,
                    activeTrackColor = OCColors.Orange,
                    inactiveTrackColor = track,
                ),
            )
            Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween) {
                Text("48:12", style = OCText.Timecode, color = secondary)
                Text("-1:22:40", style = OCText.Timecode, color = secondary)
            }
        }

        Row(
            Modifier.fillMaxWidth().padding(horizontal = 50.dp, vertical = 14.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween,
        ) {
            OCSkipButton(false, 30, primary, secondary) {}
            OCPlayRing(playing, { playing = !playing })
            OCSkipButton(true, 30, primary, secondary) {}
        }

        Row(
            Modifier.fillMaxWidth().padding(horizontal = 26.dp).padding(bottom = 14.dp),
            horizontalArrangement = Arrangement.SpaceAround,
        ) {
            Effect(Icons.Filled.Speed, "1.3×", secondary, active = true)
            Effect(Icons.Filled.GraphicEq, "Smart Speed", secondary, active = true)
            Effect(Icons.Filled.VolumeUp, "Voice Boost", secondary, active = true)
            Effect(Icons.Filled.StarBorder, "Star", secondary, active = false)
        }
    }
}

@Composable
private fun Effect(icon: androidx.compose.ui.graphics.vector.ImageVector, label: String, secondary: Color, active: Boolean) {
    val c = if (active) OCColors.Orange else secondary
    Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(5.dp)) {
        Icon(icon, label, tint = c, modifier = Modifier.size(19.dp))
        Text(label, style = OCText.Chip, color = c)
    }
}

Smart Speed / Voice Boost Toggle Row

import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults

@Composable
fun OCEffectToggleRow(
    title: String, subtitle: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit,
    dark: Boolean,
) {
    val primary = if (dark) OCColors.TextPrimaryDk else OCColors.TextPrimaryLt
    val secondary = if (dark) OCColors.TextSecondaryDk else OCColors.TextSecondaryLt
    val divider = if (dark) OCColors.DividerDk else OCColors.DividerLight
    val track = if (dark) OCColors.Surface2 else OCColors.DividerLight

    Row(
        Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 15.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween,
    ) {
        Column(Modifier.weight(1f)) {
            Text(title, style = OCText.Toggle, color = primary)
            Text(subtitle, style = OCText.PlDesc, color = secondary)
        }
        Switch(
            checked = checked, onCheckedChange = onCheckedChange,
            colors = SwitchDefaults.colors(
                checkedThumbColor = Color.White,
                checkedTrackColor = OCColors.Orange,
                uncheckedThumbColor = Color.White,
                uncheckedTrackColor = track,
            ),
        )
    }
    HorizontalDivider(color = divider)
}

Playlist Row

@Composable
fun OCPlaylistRow(
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    tile: Color, name: String, desc: String, count: Int, dark: Boolean,
) {
    val primary = if (dark) OCColors.TextPrimaryDk else OCColors.TextPrimaryLt
    val secondary = if (dark) OCColors.TextSecondaryDk else OCColors.TextSecondaryLt
    val divider = if (dark) OCColors.DividerDk else OCColors.DividerLight

    Row(
        Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 14.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(14.dp),
    ) {
        Box(
            Modifier.size(38.dp).clip(RoundedCornerShape(9.dp)).background(tile),
            contentAlignment = Alignment.Center,
        ) { Icon(icon, name, tint = Color.White, modifier = Modifier.size(18.dp)) }
        Column(Modifier.weight(1f)) {
            Text(name, style = OCText.RowTitle, color = primary)
            Text(desc, style = OCText.PlDesc, color = secondary, maxLines = 1)
        }
        Text("$count", style = OCText.Count, color = secondary)
    }
    HorizontalDivider(color = divider)
}

Buttons

import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.OutlinedButton

@Composable
fun OCPrimaryButton(title: String, onClick: () -> Unit) {
    Button(
        onClick = onClick,
        shape = RoundedCornerShape(10.dp),
        colors = ButtonDefaults.buttonColors(containerColor = OCColors.Orange, contentColor = Color.White),
        contentPadding = PaddingValues(horizontal = 26.dp, vertical = 14.dp),
    ) { Text(title, style = OCText.Button) }
}

@Composable
fun OCOutlineButton(title: String, onClick: () -> Unit) {
    OutlinedButton(
        onClick = onClick,
        shape = RoundedCornerShape(10.dp),
        border = BorderStroke(1.5.dp, OCColors.Orange),
        colors = ButtonDefaults.outlinedButtonColors(contentColor = OCColors.Orange),
        contentPadding = PaddingValues(horizontal = 22.dp, vertical = 12.dp),
    ) { Text(title, style = OCText.RowTitle) }
}

4. Core Navigation (Bottom Bar)

Overcast has a 4-tab bottom strip. No tint pill — active is just the single orange accent.

import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings

@Composable
fun OCBottomBar(selected: Int, onSelect: (Int) -> Unit, dark: Boolean) {
    NavigationBar(
        containerColor = if (dark) OCColors.Canvas else OCColors.Paper,
        tonalElevation = 0.dp,
    ) {
        val items = listOf(
            "Playlists" to Icons.AutoMirrored.Filled.List,
            "Podcasts"  to Icons.Filled.GridView,
            "Search"    to Icons.Filled.Search,
            "Settings"  to Icons.Filled.Settings,
        )
        items.forEachIndexed { i, (label, icon) ->
            NavigationBarItem(
                selected = selected == i,
                onClick = { onSelect(i) },
                icon = { Icon(icon, label, modifier = Modifier.size(22.dp)) },
                label = { Text(label, style = OCText.Tab) },
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor = OCColors.Orange,
                    selectedTextColor = OCColors.Orange,
                    unselectedIconColor = OCColors.TextTertiary,
                    unselectedTextColor = OCColors.TextTertiary,
                    indicatorColor = Color.Transparent, // no Material pill — Overcast has none
                ),
            )
        }
    }
}

5. Navigation

Use Navigation Compose / Nav3 with four destinations (Playlists, Podcasts, Search, Settings) plus a modal NowPlaying route presented as a bottom sheet that expands from the mini-player. Page transitions slide horizontally tween(300); the mini-player → full-player transition is a slideInVertically from the bottom tween(300) with the artwork as a shared element transitioning into the cover.

6. Motion

Overcast motion is quiet and editorial — 150–300ms ease ranges. Only the player cover gets a soft shadow.

Moment Compose recipe
Play/pause icon swap Crossfade(playing, tween(200)); ring press scale 0.96 via animateFloatAsState
Scrubber drag Slider thumb; grow via interactionSource pressed → thumb radius animate
Toggle flip Switch animates track cross-fade to #FC7E0F implicitly
Mini → full player slideInVertically(tween(300)) + shared-element artwork
Playlist row swipe SwipeToDismissBox 1:1 reveal, 50% commit, spring(dampingRatio = 0.85f)
Theme switch animateColorAsState on canvas/text tween(250) — cross-fade, no hard cut
Page navigation NavHost slide push tween(300)
// Ring press — the canonical Overcast micro-motion
val scale by animateFloatAsState(if (pressed) 0.96f else 1f, tween(120), label = "ringScale")

Haptics: use LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) for the soft impact on play, toggle flip, scrubber boundary, and the recommend action. The Smart Speed time-saved stat updates silently — no haptic.

7. Icons

Overcast uses simple stroked glyphs; the play control is an orange ring (a bordered Box, not an icon). androidx.compose.material:material-icons-extended covers the rest.

Purpose SF Symbol (iOS) Material Icon (Compose)
Playlists (tab) list.bullet Icons.AutoMirrored.Filled.List
Podcasts (tab) square.grid.2x2 Icons.Filled.GridView
Search (tab) magnifyingglass Icons.Filled.Search
Settings (tab) gearshape Icons.Filled.Settings
Play / Pause play.fill / pause.fill Icons.Filled.PlayArrow / Icons.Filled.Pause
Skip back / forward chevron.left.2 / chevron.right.2 Icons.Filled.FastRewind / Icons.Filled.FastForward
Smart Speed waveform.path Icons.Filled.GraphicEq
Voice Boost speaker.wave.2.fill Icons.Filled.VolumeUp
Playback speed speedometer Icons.Filled.Speed
Star star / star.fill Icons.Filled.StarBorder / Icons.Filled.Star
In Progress (playlist) clock.arrow.circlepath Icons.Filled.History
Downloaded arrow.down.circle.fill Icons.Filled.DownloadDone
Collapse player chevron.left Icons.AutoMirrored.Filled.KeyboardArrowLeft
Queue / list list.bullet Icons.AutoMirrored.Filled.List
Recommend hand.thumbsup Icons.Filled.ThumbUp
More ellipsis Icons.Filled.MoreHoriz
Add podcast plus Icons.Filled.Add

8. Minimum SDK & Accessibility Notes

  • minSdk 24 (Compose floor is 21; Slider, modern motion comfortable at 24). compileSdk 34+, Compose BOM 2024.09+, Kotlin 2.0+.
  • Edge-to-edge: call enableEdgeToEdge(); the paper-cream light theme wants dark-content system bars (light-content in dark mode). The mini-player sits directly above the NavigationBar inside the navigation-bar inset.
  • Paper canvas: use #FBFAF6 (surfaces #F7F5EF) for light — never pure white. Do not enable Material You dynamicColorScheme(); the warm paper + single-orange identity must hold regardless of wallpaper.
  • Tabular timecodes: render durations/timecodes in a fixed-width Box (or tnum font feature on API 26+) so digits never shift while scrubbing.
  • Orange ring: the play button is a Box with Modifier.border(2.5.dp, OCColors.Orange, CircleShape) — never a filled circle.
  • Font scaling: sp honors the user’s font scale — keep it on screen titles, episode titles, body, metadata. Pin layout-sensitive text (10sp tab labels, effect chips, timecodes, counts, skip-interval labels) via CompositionLocalProvider(LocalDensity provides Density(density, fontScale = 1f)).
  • TalkBack: label the play ring “Play” / “Pause, {episode}”; the Slider exposes a percentage stateDescription — add custom actions for “Skip forward {n} seconds” / “Skip back {n} seconds”; playlist rows announce “Playlist: {name}, {count} episodes, {description}”.
  • Touch targets: Material guidance is 48.dp. The 66.dp play ring is fine; give 27.dp skip glyphs a 48.dp hit via padding; playlist rows are full-row tappable ≥56.dp; tab items are 48.dp by default.
  • Contrast: #FC7E0F keeps ≥3:1 against both #FBFAF6 and #121212 for the ring/icons; #1A1A1A on #FBFAF6 passes WCAG AAA for body. Never rely on orange alone for active effects — pair Smart Speed / Voice Boost ON with the labeled toggle + time-saved stat.
  • Reduce motion: when Settings.Global.ANIMATOR_DURATION_SCALE == 0, disable the scrubber-thumb grow, the play/pause crossfade, and the theme cross-fade; keep state changes.
  • Dark mode: invert via the dark token set — true #121212, #F2F2F2 text; the single orange accent is unchanged.
All mobile designs →

Brand names describe the visual style only; coderef is not affiliated with or endorsed by any brand listed. Licenses