Reference

Theme guide

The supplied visual design reference.

View source

Design system recovered from the app's StyleSheet.create calls, app.config, and bundled font resources. Everything here is observed, not inferred from a design file — the app has no theme module beyond a two-entry Colors export (white: '#FFFFFF', black: '#000000'), so the real system lives inline in ~450 named style objects.

Aimed at building a display that matches the app. Values are raw React Native units, which map 1:1 to CSS px.


1. Foundations

The app is dark-only. app.config sets userInterfaceStyle: "dark", splash background #000000, and Android status bar #000000. There is no light theme and no useColorScheme branch anywhere in the app code — don't build one.

The core move is a pure black page with raised dark-grey cards. Not a layered grey-on-grey system: black background, one card colour, done.


2. Colour

Surfaces

Token Value Use
--bg #000000 Page background, splash, status bar
--surface #1D1D1D Every card, panel, tab bar, header block
--surface-raised #2A2A2A Nested panels inside a card (custom-tuning section), selected tiles
--surface-sunken #141414 Disabled tile background
--control #444444 Text inputs, value chips, slider value pills
--control-alt #333333 Modal body, slider end-label chips
--control-muted #666666 Reset buttons
--control-track #555555 Battery bar track, speedometer arc track
--icon-circle #3C3C3C Round icon buttons (disconnect)

#1D1D1D does the overwhelming majority of the work — 12 of 23 background declarations in app code. If in doubt, a surface is #1D1D1D.

Accent

Token Value Use
--accent #69D2DF The brand colour: logo, slider fill + thumb, battery fill, active tick marks, section dividers, selected borders, badge circles
--accent-deep #2BB7CA Device-list connect buttons (the one darker variant)
--accent-soft #7DD3E3 A single lighter border

A pale cyan. Used sparingly and almost always as fill or edge, never as a large surface — the biggest accent area in the app is a 35 px battery bar.

Text

Token Value Use
--text #FFFFFF Default. A wrapper <Text> component injects Colors.white before any passed style, so white is the inherited default everywhere
--text-secondary #BBBBBB Feature labels, feature values, tile labels
--text-muted #AAAAAA Tab labels (inactive), disclosure arrows
--text-dim #808080 Sub-labels beside option text
--text-on-accent #000000 Text sitting on the cyan battery fill
--text-on-accent-alt #1D1D1D Volts text on the battery fill

Note the inversion on the battery bar: black and #1D1D1D text on cyan. That's the only place dark-on-light appears.

Semantic

Token Value Use
--danger #FF4444 Overheating tab label + underline
--danger-strong #AC0E0E Sport / Agro mode badge
--danger-alt #CC3232 Disabled-state text
--warning #FFC300 "Previous value" hints under changed settings
--gauge #FFD700 Speedometer arc fill (gold, not the accent)
--success #198E13 Eco / Float mode badge
--info #0E77AC Cruise / Standard mode badge
--neutral-mode #777777 Custom / unknown mode badge
--overheat-overlay rgba(255,0,0,0.7) Full-width overheat banner

Mode badges are a fixed four-colour set reused for both ride modes and wheelie modes: red #AC0E0E, blue #0E77AC, green #198E13, grey #777777.

Scrims

rgba(0,0,0,0.5) for modal overlays (most common), with 0.4 and 0.7 variants also present.


3. Typography

Fonts

The app ships exactly one custom font. Everything else is the platform system face.

Family Source Where
Sonsie One @expo-google-fonts/sonsie-one, SonsieOne_400Regular, loaded via useFonts({ 'Sonsie One': … }) Two places only: a centred label with paddingRight: 2, and a 24 px white centred heading
System sans RN default (Roboto on Android, SF Pro on iOS) Everything else — no fontFamily is set on any other text style

Sonsie One is a display face with heavy slab-ish serifs and high contrast. On the web, load it from Google Fonts:

@import url('https://fonts.googleapis.com/css2?family=Sonsie+One&display=swap');

Do not use Inter or JetBrains Mono. Both appear in res/font/ (inter_regular/medium/semibold/bold, jetbrains_mono_light/medium/regular) and it's tempting to treat them as the app's typefaces — they are not. They ship with expo-dev-menu / expo-dev-launcher (confirmed: 1071 dev-menu symbol hits in classes5.dex) and are never referenced from app code. roboto_medium_numbers.ttf is likewise an AndroidX/Play-Services resource.

The string 'Roadgeek 2005 Series E' appears 62 times, but only inside embedded SVG markup — it's the FHWA highway-signage face used to draw speed-limit sign graphics, not a UI font. It isn't bundled, so those SVGs fall back to whatever the renderer has.

Scale

Observed sizes, by frequency in app stylesheets:

px Count Typical role
16 18 Body, tab labels, option text, slider labels
22 12 Section labels, toggle labels, stat labels/values, save-button text
18 11 Feature labels/values, stat units, battery text
14 8 Small print, "previous value" hints, button text
20 4 Mode button titles, features title
24 3 Section titles
30 3 Page titles, disclosure arrows
11–12 3 Slider end labels (uppercase)
28, 36, 50, 60 1 each Angle value (36), logo (60), sub-logo (50)

Plus two scale-derived sizes in the speedometer: round(60 × scale) for the speed number and round(25 × scale) for the unit.

A workable ladder: 11 / 14 / 16 / 18 / 20 / 22 / 24 / 30 / 36 / 60.

Weights

Weight Count Use
400 75 Default body
bold (700) 26 Values, titles, save buttons, logo
500 13 Labels, tab text, feature text
600 7 Section titles, slider labels, preset labels

Only four weights. The jump straight from 500/600 to bold with nothing in between is characteristic — there's no 800/900 anywhere.

Other text properties

  • lineHeight is set on only four styles: 30 (disclosure arrows, matching their 30 px size), 22 (20 px mode titles), 20, 18. Everything else uses the platform default.
  • letterSpacing: 0.5 on save-button text (4 uses); 0.35 once.
  • textTransform: 'uppercase' on slider end labels only.
  • fontStyle: 'italic' for the connect-screen subtitle and "feature status" text — used to mark secondary/placeholder states.
  • includeFontPadding: false + textAlignVertical: 'center' on the disconnect icon glyph, for optical centring in a circular button.

4. Shape and spacing

Radii

px Use
2 Battery icon nub, tab underline caps
5 Inputs, scan button, small chips
8 Slider value pills, option rows
10 Modal content, device buttons, battery bar
12 The default card radius — mode buttons, features container, custom-tuning panel
14, 16, 18 Occasional; 18 = half of the 36 px circular buttons
20 Tab pill interior, bottom corners of the top section
25 Tab bar container
999 Fully-round mode icons

12 is the house radius for cards; 999 for anything circular.

Spacing

The app uses a loose 4 px grid: gaps of 2, 4, 5, 8, 10, 12, 14, 16, 20, 24, 30, 32.

Recurring patterns: - Screen horizontal padding: 20 - Card interior padding: 16 or paddingHorizontal: 20 - Card-to-card gap: 12 (dataGridRow, modeContainer) - Vertical rhythm between sections: 14 (dataGrid gap) or 16 - Negative horizontal margins of −20 / −10 to let cards bleed past the screen padding (marginHorizontal: 4294967276 in the decompiled output is −20 rendered unsigned)

Elevation

Barely used. Three shadows total, all on the slider thumb and similar small controls: shadowOpacity 0.3, shadowRadius 6, elevation 6, with shadowColor set to the accent #69D2DF so the thumb has a faint cyan glow. Cards have no shadow — separation comes purely from the #1D1D1D-on-black contrast.


5. Component recipes

Copied values, lightly translated to CSS.

Card

background: #1D1D1D;
border-radius: 12px;
padding: 16px 20px;
min-height: 125px;
justify-content: center;

Top section (the header block the dashboard hangs off)

background: #1D1D1D;
padding: 0 20px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
margin: 0 -20px;   /* bleeds past the 20px screen padding */
z-index: 1;

Tab bar

/* container */
display: flex; background: #1D1D1D;
border-radius: 25px; padding: 4px; margin: 14px 0;

/* tab */
flex: 1; padding: 10px 0; text-align: center; border-radius: 20px;

/* label */            color: #AAA; font-size: 16px; font-weight: 500;
/* label, active */    color: #FFF; font-weight: bold;
/* label, overheat */  color: #FF4444;

/* underline */
position: absolute; bottom: -3px; height: 3px;
background: #FFF; border-radius: 2px 2px 0 0;
/* overheat underline */ background: #FF4444;

Slider

/* track */        height: 10px; background: #555; border-radius: 5px;
/* active fill */  height: 10px; background: #69D2DF;
                   border-radius: 5px 0 0 5px; top: 11px;
/* thumb */        width: 24px; height: 24px; border-radius: 12px;
                   background: #69D2DF; border: 2px solid #FFF; top: 4px;
                   box-shadow: 0 0 6px rgba(105,210,223,.3);
/* tick, active */ width: 3px; height: 12px; background: #69D2DF;
/* value pill */   background: #444; padding: 4px 12px;
                   border-radius: 8px; min-width: 36px;
/* value text */   font-size: 16px; font-weight: bold; color: #FFF;
/* label */        font-size: 16px; font-weight: 600; color: #FFF;
/* end labels */   background: #333; padding: 3px 6px; border-radius: 5px;
                   font-size: 11px; font-weight: 500; color: #BBB;
                   text-transform: uppercase;

Mode button

flex: 1; display: flex; flex-direction: column; align-items: center;
background: #1D1D1D; border-radius: 12px;
padding: 8px 4px 10px; gap: 5px; min-height: 125px;

/* icon disc */  min/max 60–80px square; aspect-ratio: 1;
                 border-radius: 999px; background: <mode colour>;
/* icon glyph */ font-size: 36px; font-weight: bold; padding-bottom: 4px;
/* title */      font-size: 20px; font-weight: bold; line-height: 22px;
/* arrow */      font-size: 30px; line-height: 30px; color: #AAA;
/* overlay */ position: fixed; inset: 0;
              background: rgba(0,0,0,.5);
              display: flex; align-items: center; justify-content: center;
/* content */ background: #333; border-radius: 10px; padding: 20px;
              width: 80%; min-width: 280px; max-width: 500px;

Selected / disabled states

.selected { border-color: #69D2DF; background: #2A2A2A; }
.disabled { opacity: .5; border-color: #222; background: #141414; }

Save bar

padding: 20px; border-top: 1px solid #444;
/* text */ color: #FFF; font-size: 22px; font-weight: bold; letter-spacing: .5px;

Round icon button

width: 36px; height: 36px; border-radius: 18px;
background: #3C3C3C;        /* or #666 with a 4px #1D1D1D border for resets */
display: flex; align-items: center; justify-content: center;

6. The speedometer

The signature element, worth reproducing exactly. It's an SVG arc.

viewBox         260 × 180
arc path        M 27 168 A 110 110 0 1 1 233 168
radius          110
sweep           1.2333333333333334 × π rad  ≈ 222°
arc length      1.2333333333333334 × π × 110  ≈ 426.2
stroke-width    14
stroke-linecap  butt
fill            none
track colour    #555555
value colour    #FFD700   (gold — deliberately not the cyan accent)

Progress is drawn with stroke-dasharray:

const filled = arcLength * speedPercentage;
strokeDasharray = `${filled} ${arcLength - filled}`;

Everything scales off a nominal 300 px width:

const scale = size / 300;
speedFontSize = Math.round(60 * scale);
unitFontSize  = Math.round(25 * scale);
unitMarginTop = Math.round(-8 * scale);   // pulls the unit up under the number
actualHeight  = Math.round(180 * scale);

The negative unit margin is what gives the tight number/unit lockup.

Battery bar (sits under the speedometer)

position: relative; height: 35px; width: 100%;
background: #555; border-radius: 10px; overflow: hidden;
/* fill */  position: absolute; inset: 0 auto 0 0; height: 100%;
            background: #69D2DF; width: <pct>;
/* text */  color: #000; font-size: 18px; font-weight: bold;
/* volts */ color: #1D1D1D; font-size: 18px; font-weight: 400;

Stat readouts

/* label */ font-size: 22px; font-weight: bold;
/* row */   display: flex; align-items: baseline; gap: 4px; margin-top: 2px;
/* value */ font-size: 22px; font-weight: 400;
/* unit */  font-size: 18px; font-weight: 400;

Baseline alignment with a 4 px gap and a smaller unit is the house pattern for every number-plus-unit pairing.

Overheat banner

Appears when a temperature crosses OVERHEAT_THRESHOLD = 75.

position: absolute; bottom: -10px; left: 0; right: 0; z-index: 100;
background: rgba(255,0,0,.7); padding: 12px 16px;
/* text */ font-size: 16px; font-weight: bold; text-align: center;

7. Icons

The app uses Ionicons (via @expo/vector-icons). Every other family — MaterialCommunityIcons, FontAwesome 5/6, Feather, AntDesign, Entypo, Octicons, SimpleLineIcons, Fontisto, Foundation, Zocial, EvilIcons — ships in the APK but is only registered, never used from app code. If you're matching the look, pull Ionicons.

Mode icons are not icons at all: they're single letters (S, C, E for Sport/Cruise/Eco; 1, 2, 3 for Float/Standard/Agro) rendered at 36 px bold inside the coloured disc.


8. Drop-in CSS variables

@import url('https://fonts.googleapis.com/css2?family=Sonsie+One&display=swap');

:root {
  color-scheme: dark;

  /* surfaces */
  --bg:               #000000;
  --surface:          #1D1D1D;
  --surface-raised:   #2A2A2A;
  --surface-sunken:   #141414;
  --control:          #444444;
  --control-alt:      #333333;
  --control-muted:    #666666;
  --control-track:    #555555;
  --icon-circle:      #3C3C3C;

  /* accent */
  --accent:           #69D2DF;
  --accent-deep:      #2BB7CA;
  --accent-soft:      #7DD3E3;

  /* text */
  --text:             #FFFFFF;
  --text-secondary:   #BBBBBB;
  --text-muted:       #AAAAAA;
  --text-dim:         #808080;
  --text-on-accent:   #000000;

  /* semantic */
  --danger:           #FF4444;
  --danger-strong:    #AC0E0E;
  --danger-alt:       #CC3232;
  --warning:          #FFC300;
  --gauge:            #FFD700;
  --success:          #198E13;
  --info:             #0E77AC;
  --neutral-mode:     #777777;
  --overheat:         rgba(255, 0, 0, .7);
  --scrim:            rgba(0, 0, 0, .5);

  /* type */
  --font-display: 'Sonsie One', serif;
  --font-ui: system-ui, -apple-system, 'Roboto', 'Helvetica Neue', sans-serif;
  --fs-xs: 11px;  --fs-sm: 14px;  --fs-base: 16px; --fs-md: 18px;
  --fs-lg: 20px;  --fs-xl: 22px;  --fs-2xl: 24px;  --fs-3xl: 30px;
  --fs-4xl: 36px; --fs-logo: 60px;
  --fw-regular: 400; --fw-medium: 500; --fw-semibold: 600; --fw-bold: 700;
  --ls-wide: .5px;

  /* shape */
  --r-xs: 2px; --r-sm: 5px; --r-md: 8px; --r-lg: 10px;
  --r-card: 12px; --r-xl: 20px; --r-pill: 25px; --r-full: 999px;

  /* spacing */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 24px; --sp-8: 32px;

  --glow-accent: 0 0 6px rgba(105, 210, 223, .3);
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--fs-base);
  padding: 0 var(--sp-5);
}

9. Getting the feel right

Five things that carry most of the app's character:

  1. Pure black page, one card grey. Resist a layered grey hierarchy — #000 and #1D1D1D with nothing in between is the look.
  2. Cards separate by contrast, not shadow. No elevation on surfaces at all.
  3. Cyan is an edge colour. Fills, thumbs, 2 px dividers, selected borders. The gauge is gold; the accent never fills a large area except the battery bar.
  4. Numbers are big and unit-paired. Value and unit on a shared baseline, 4 px apart, unit one step smaller and the same weight.
  5. One display font, used twice. Sonsie One for the wordmark and one heading; everything else is system sans. Resist spreading it around.

10. Caveats

  • Colours were harvested from the decompiled bundle, scoped to the app's own module range to exclude library defaults and inline SVG style blocks (the bike illustration alone contributes ~100 #000 declarations that are SVG defaults, not UI colour).
  • Names like --surface-raised are mine; the app has no token names.
  • The app is an expo-updates build, so a device may be running a newer JS bundle than the one in this APK.

Search the guides and implementation notes. Press Esc to close.