The flip
CorePoulpy's side follows the mood rules above. Coming back is always one session away, and the flip animation plus a haptic tap is the reward for it.
Inspired by the plush itself.
Poulpy is a reversible plush: blue and smiling on one side, grumpy on the other. In the app his grumpy side turns ember, from burnt orange to red, and both sides shift shade with how he feels.
Tap Poulpy to flip him.
A faithful front view of the plush: tall soft dome, five visible arms, and the ember lining peeking out underneath, which hints at the other side. The logo and the in-app mascot are the same drawing.
The grumpy side is a warm ember: burnt orange (#D9622B) for everyday grumpiness, deepening into red when Poulpy gets really cross.
Poulpy's colour follows his emotion, not just his side. The blue side stays close to the plush: a touch richer when proud, lighter and fresher while recovering. Missed sessions heat the ember up toward red. Tap a shade.
With a red-leaning mascot, error messages move to a crimson (#C8264A) and always come with an icon and text, so a cross Poulpy is never mistaken for an error. The plush ember (#D9622B) is too light for white text, so buttons use the deeper ember 600.
A sticker sheet for the big moments: end of session screens, records, rest days, notifications. The apps draw these same poses, generated from this page's drawing code by design/build-poulpy.mjs.
Yeah. I did that.
Hnnngh… one more!
Gold for the octopus!
Look how big I am now.
Worth it. Probably.
Just… give me a sec.
Rest is training too.
Easy today. I'm right here.
Oh… you're back!
Hmm, which one…
Wheee…
Hmph. We had a date.
EIGHT tentacles and none of them trained!
Poulpy never lifts his tentacles: every pose keeps the plush's shape and tells the mood with his face, his shade, props and little effects. More ideas for later: stretching, a tiny treadmill, sipping from a water bottle, dancing when the eighth tentacle lights.
Both faces are free (SIL Open Font License), so they can ship inside the Android and iOS apps and load from Google Fonts on the web.
A single design/tokens.json is the source of truth, generated into a Compose theme, SwiftUI colour assets and CSS variables. The mood shades are tokens too, so every platform paints Poulpy the same way.
{
"color": {
"poulpy": { "blue": "#3AA6E0", "ember": "#D9622B", "ink": "#141A26" },
"lagoon": { "50": "#EAF6FD", "100": "#CDEBFA", "300": "#7FCBF0", "400": "#3AA6E0",
"600": "#1574B0", "800": "#0C4A73", "900": "#082F4A" },
"ember": { "50": "#FFF1E8", "100": "#FFDCC7", "300": "#F4A473", "400": "#E8873F",
"500": "#D9622B", "600": "#B4471C", "800": "#7A2C12" }
},
"mood": {
"proud": "#2C9CE0", "happy": "#3AA6E0", "recovering": "#5DB4E6",
"annoyed": "#E8873F", "grumpy": "#D9622B", "fuming": "#C0352B"
},
"role": {
"light": { "primary": "{color.lagoon.600}", "onPrimary": "#FFFFFF",
"grumpy": "{color.ember.600}", "error": "#C8264A",
"background": "#F4F9FC", "surface": "#FFFFFF" },
"dark": { "primary": "#5BBEF0", "onPrimary": "#06202F",
"grumpy": "#FF9A66", "error": "#FF6B85",
"background": "#0A1520", "surface": "#112230" }
},
"font": { "display": "Baloo 2", "body": "Nunito" },
"space": { "xs": 4, "sm": 8, "md": 12, "lg": 16, "xl": 24, "2xl": 32, "3xl": 48 },
"radius": { "sm": 10, "md": 16, "lg": 24, "pill": 999 },
"motion": { "flip": { "duration": 700, "easing": [0.5, -0.35, 0.35, 1.4] } }
}
// app-android/.../theme/PoulpyTokens.kt (generated)
object PoulpyColors {
val Blue = Color(0xFF3AA6E0)
val Ember = Color(0xFFD9622B)
val Ink = Color(0xFF141A26)
val Lagoon600 = Color(0xFF1574B0)
val Ember600 = Color(0xFFB4471C)
}
enum class Mood(val tone: Color) {
Proud(Color(0xFF2C9CE0)), Happy(Color(0xFF3AA6E0)), Recovering(Color(0xFF5DB4E6)),
Annoyed(Color(0xFFE8873F)), Grumpy(Color(0xFFD9622B)), Fuming(Color(0xFFC0352B)),
}
val PoulpyLight = lightColorScheme(
primary = PoulpyColors.Lagoon600, onPrimary = Color.White,
tertiary = PoulpyColors.Ember600,
background = Color(0xFFF4F9FC), surface = Color.White,
error = Color(0xFFC8264A),
)
// app-ios/CoachPoulpy/Theme/PoulpyTokens.swift (generated)
// Colours live in Assets.xcassets with Any/Dark appearances, generated from tokens.json.
extension Color {
static let poulpyBlue = Color("PoulpyBlue") // fixed, same in both appearances
static let poulpyEmber = Color("PoulpyEmber")
static let brandPrimary = Color("Primary") // #1574B0 light, #5BBEF0 dark
static let brandGrumpy = Color("Grumpy") // #B4471C light, #FF9A66 dark
}
enum Mood: String, CaseIterable {
case proud, happy, recovering, annoyed, grumpy, fuming
var tone: Color { Color("Mood/\(rawValue)") }
}
extension Font {
static let poulpyDisplay = Font.custom("Baloo2-ExtraBold", size: 40, relativeTo: .largeTitle)
static let poulpyBody = Font.custom("Nunito-Medium", size: 16, relativeTo: .body)
}
/* web/src/styles/tokens.css (generated) */
:root {
--cp-poulpy-blue: #3AA6E0;
--cp-poulpy-ember: #D9622B;
--cp-mood-proud: #2C9CE0; --cp-mood-happy: #3AA6E0; --cp-mood-recovering: #5DB4E6;
--cp-mood-annoyed: #E8873F; --cp-mood-grumpy: #D9622B; --cp-mood-fuming: #C0352B;
--cp-primary: #1574B0;
--cp-grumpy: #B4471C;
--cp-error: #C8264A;
--cp-bg: #F4F9FC;
--cp-font-display: "Baloo 2", "Arial Rounded MT Bold", sans-serif;
--cp-font-body: "Nunito", system-ui, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root { --cp-primary: #5BBEF0; --cp-grumpy: #FF9A66; --cp-error: #FF6B85; --cp-bg: #0A1520; }
}
Soft like stitched fabric. No sharp corners anywhere.
One signature move: the flip, 700 ms with a small overshoot, like turning the plush inside out. Mood shade changes fade over 300 ms. Everything else stays quick (150 to 250 ms) and plain.
Poulpy sits in the middle of the tentacle ring. Only Poulpy and the header tint change with his mood; the rest of the screen stays calm so the app never feels like it's shouting at you.
Happy side. On track with the week, streak alive.
Grumpy side. A planned session was missed; the fix is offered right away and kept short.
Which side shows says whether you're keeping your plan. The shade and the face on that side react to what just happened.
| What happens | Mood | Pose | Rule |
|---|
Core ideas come first; the others can join later.
Poulpy's side follows the mood rules above. Coming back is always one session away, and the flip animation plus a haptic tap is the reward for it.
Inspired by the plush itself.
The ring has eight tentacles. Each finished session lights one, and a lit tentacle never goes dark: there is no weekly reset. The eighth gives the Champion pose and a "full octopus", and the next session starts a new ring.
Octopuses have eight tentacles, and Poulpy never grows more.
Poulpy's colour moves along the spectrum with each emotion: a touch richer after a record, lighter while recovering, hotter ember the longer a session waits.
Octopuses change colour with their mood.
Achievements are shells Poulpy collects in his den: first 5 km on the treadmill, 10 sessions, a squat record, a full rehab block finished.
Octopuses decorate their dens with shells and stones.
Unlock seasonal skins for Poulpy after milestones (a coral reef edition, a deep-sea night edition). Purely cosmetic.
Octopuses can camouflage in a fraction of a second.
A home-screen widget and an optional morning nudge that says which side the real plush on your desk should show today.
Only works because the real Poulpy exists.