Skip to content

Design system ​

The app is calm and rounded, like the plush: one blue accent for actions, white or near-white surfaces, and only Poulpy and the Home header tint with his mood. Values live in design/tokens.json and are generated into each platform (see design/README.md); this page says how to use them. The live swatches are on the site's Tokens page.

Colour roles ​

Use roles, never raw palette steps, in screens. Each role has a light and a dark value.

RoleUse it for
primary / onPrimaryThe main action on a screen (Start session, Save), selected chips, links, progress
primaryContainer / onPrimaryContainerTonal buttons, selected rows, Poulpy's speech on the blue side
grumpy / onGrumpyThe one action offered while Poulpy is on the ember side (Catch up, Flip him back)
grumpyContainer / onGrumpyContainerThe Home header and cards while Poulpy is on the ember side
background, surface, surfaceVariantScreen, cards and sheets, inset fields and steppers
onSurfaceVariant, outlineSecondary text and captions; card borders and dividers
successA confirmed save or a finished phase, with a check icon
error / onErrorErrors only: crimson, always with an icon and words
heat1, heat2, heat3How much a workout works each muscle on the body map, from a little to most. Orange, on the body map only: never for Poulpy's moods
painA worked area that hurts, outlined on the body map. Purple, so it reads apart from the heat and is never mistaken for an error

Rules:

  • poulpy.blue, poulpy.ember and poulpy.ink are Poulpy's fabric and never change between light and dark.
  • The plush blue and ember are too light for white text: buttons use primary and grumpy.
  • Ember means "a session slipped", never "something broke". An error is crimson, so a cross Poulpy is never read as a failure.
  • Colours that must be told apart also differ in lightness, not hue alone.

Mood shades ​

Six shades, from mood.*, only on Poulpy himself, the Home header tint and mood-coloured charts:

SideShades, calmest to strongest
Blue (on track)recovering, happy, proud
Ember (a session slipped)annoyed, grumpy, fuming

Which shade shows when is in Brand.

Type ​

Baloo 2 for display and titles, Nunito for everything else. Platforms map each role to a system text style so Dynamic Type and font scaling keep working (.font(.poulpy(.headline)) on iOS, MaterialTheme.typography on Android).

RoleFontSize / lineUse
displayBaloo 2 80040 / 44Poulpy's line on Home, end-of-session headline
titleBaloo 2 70028 / 32Screen titles, workout name on its page
headlineBaloo 2 70021 / 25Section heads, the current exercise in the player
bodyNunito 50016 / 24Everything you read
labelNunito 800, tracked 0.08 em, upper case12.5 / 16Eyebrows, tab labels, "Next up"
timerNunito 800, tabular figures64 / 64Timers and rep counts, which must not jitter

Words never break. Translations run longer than English (« Bibliothèque », « Échauffement »), so text is laid out for any length:

  • Text in a space of fixed width (tab labels, chips, stat tiles, stepper values) stays on one line and shrinks to fit, never below 9 sp / pt; past that it ends in an ellipsis. FitText on Android, .lineLimit(1).minimumScaleFactor(…) on iOS, whose tab bar already does it.
  • Buttons wrap their label between words rather than shrink it.
  • Every other text wraps between words only, never inside one, and never hyphenates.

Space, radius, motion ​

  • Spacing steps: 4, 8, 12, 16, 24, 32, 48 (xs to xxxl). Pick a step; never a value between them.
  • Radius: 10 for chips and small fields, 16 for cards, 24 for sheets, pill for buttons.
  • Motion: the flip (700 ms with overshoot) is the one signature move, used only when Poulpy changes side. Mood shades fade in 300 ms. Everything else is 150 to 250 ms and plain. With reduced motion on, the loader slows to one turn in 10 s.

Components ​

ComponentShapeNotes
Primary button56 high, pill, primaryOne per screen
Tonal button56 high, pill, primaryContainerSecondary actions
Outline button56 high, pill, 2 px outlineCancel, less important choices
Grumpy button56 high, pill, grumpyOnly the catch-up action while Poulpy is ember
Chip36 high, pill; on = primary, off = 1.5 px outlineFilters (muscle, equipment, format), with aria-pressed or selected state; PoulpyChip on both platforms
Choosing one valueA select field that opens a sheet for a longer list, or a native menu for a short onePills and chips are for filters and for view switches; a form asks for a value with a select, never a row of pills
Search field48 high, pill, 1.5 px outline, primary while it holds text, a round clear buttonPillSearchField on both platforms
StepperInset surfaceVariant box, label, minus and plus round buttons of 44Sets, reps, load, rest
Pattern badge44 round, primaryContainer, one icon per movement pattern in onPrimaryContainerStands in for exercise pictures until real images exist
Filter rowSearch field, a row of chips, then the count line with one action at its rightListFilters on both platforms, shared by the exercise and workout lists
Poulpy card24 radius, primaryContainer, Poulpy happy on the blue side, a title, a line and one primary buttonPoulpyCard: no results, an empty list
Item menuA long press (iOS context menu) or ⋮; actions with icons, a divider, then the destructive oneDestructive items and their confirmation are error with an icon, never ember; the confirmation says what goes and what stays
Plan sheet24 radius sheet, a month from Monday; past days dimmed, today outlined, planned days primaryPlanSheet on both platforms; tick several days, save once
Error bannererror tint, icon plus textNever ember
LoaderPoulpy from above, a blue outline of eight lobes, one turn every 2.4 sPoulpyLoader on both platforms; replaces spinners

Base fields ​

The fields every form is built from. Reference mockups: profile-edit, the three profile-*-sheet pages, body-log-sheet, body-log-height-sheet. CSS classes are in design/mockups/shared/mockup.css. Build every new form from these, and move old screens to them as they are touched.

FieldShapeBehaviour
Text field52 high, 10 radius, 1.5 px outline, label above, primary outline on focusFree text: a name. A note under it in onSurfaceVariant
Select fieldSame box, the chosen value (or a muted "Not set") and a chevronTapping opens a sheet; nothing else is shown on the screen. Never a row of pills
Option sheet24 radius sheet, a title, one row per choiceSingle choice: a radio and the sheet closes on tap. Several choices: checkboxes and a Done button. A choice that needs text (Custom pronouns) shows its text field inside the sheet
Number fieldText field with a unit at its right (bpm, kg, cm)Selects all its text on focus, opens the numeric keyboard, checks the range on leaving. Optional round − and + buttons on both sides (the number-entry pattern) for small nudges. Imperial height is two number fields, ft and in. Every numeric value in the app uses it, including the builder's sets, reps, rest between sets, rest and weight (builder-sets-edit-step, builder-circuit-edit-step); the − and + step by the same amount as before, and the typed value keeps the same limits
Step sliderA bar of dots, •--•--•--•--•, the current label above it, the two ends named belowA fixed scale of five or fewer named steps, inside an option sheet
Line chartGrid lines behind the line, the unit at the top of the axis, a value at each grid line, the date range under itNo grid colour that implies good or bad; a weight going up is never red
Empty chart cardPoulpyCard shape with Poulpy on the blue side, a line and the one action that adds the first entryShown instead of the chart when nothing is logged
Body mapA front and a back figure, drawn in the build the person chose in the profile, as a muscle chart with transparent skin, side by side, each area a shape that fills with primary when picked; left and right are separate areasEvery area is a checkbox with a name ("Left knee"), so a screen reader and the text summary under the figure say the same. Left is the person's left: the right half of the front figure and the left half of the back one. Used to say where it hurts, and to show what an exercise works
Body map heat map (draft, #244)The body map read-only for a whole workout: areas it doesn't work are outlines only, worked areas fill with heat1, heat2 or heat3 by sets per muscle group compared with the most-worked one (below 40 %, from 40 %, from 75 %); a worked area that hurts gets a 3 px pain outline on a background halo, drawn above its neighbours; a Less / More legend, with Hurts when something hurtsThe figure is one image whose label is the text summary ("shoulders and triceps most, then chest, back and biceps, a little core"), and the heat steps differ in lightness, so nothing depends on colour alone. On the workout page, with the format and equipment badges at its bottom right
Details sheet (draft, #244)24 radius sheet over the body map with no scrim; a handle, the workout name at its top, its content, then the action barThree stops: dragged down it shows only the name and actions under a full-screen map; half up by default; full screen its content scrolls. The back and menu buttons float above it at every stop
Plan summary (draft, #244)primaryContainer row: a calendar icon, "3 sessions planned" over "Next on Fri 30 Oct", and a Change days buttonReplaces one pill per planned day on the workout page

Screens ​

Mockups are plain HTML pages in design/mockups/ (ADR 0016): drafts in drafts/, validated screens in agreed/, each split into phone/, tablet/ and web/. The docs site shows them on the Mockups pages, one page per screen, in light or dark. Build a screen from its agreed mockup, and get a mockup agreed, through a PR that moves it out of drafts/, before building a screen that has none. The player draws the same screens on both phones (player-ready, player-set, player-circuit-round, player-repeat-in-sets, player-warm-up, player-rest, player-recover): pause and end on the bar over the step count, Poulpy straining on an exercise and glancing aside inside the rest ring, a rep target as one big number with its unit under it, a line saying what comes next, and −15 s and +15 s under the ring to lengthen or shorten that one rest. The screen sits on the background role, not surface, so the cards on it stand out. The finished screen names the workout and its date, then shows three stats picked from what the exercises logged: Time always, then sets and weight lifted for sets and weights, distance and pace for runs and walks, and never heart rate; a stat with nothing logged, such as the weight of a bodyweight session, is left out (player-finished and player-finished-run on the Mockups pages). CI runs node design/check-mockups.mjs on every mockup: no literal colours, no px value where a spacing, radius or headline token has that size, no missing link, no script or style from outside the repo (the brand fonts apart), and the title, screen and section meta tags in place.

Accessibility ​

  • Touch targets at least 44 by 44.
  • Text contrast at least 4.5:1, including small grey labels; 3:1 only for text 24 px and up.
  • Every icon-only button has a label; decorative drawings of Poulpy are hidden from screen readers, and a pose that carries meaning has a description.
  • Nothing depends on colour alone: mood changes also change Poulpy's face and words, errors carry an icon.