⌂ Kelas Sekejap Knowledge BaseBrandDesign SystemDocsLogo
Docs · Roly’s rules — expressions and voice

Mascot guide

Mascot

Drop this block into CLAUDE.md in ks-design-system (and mirror it into the app repos). It's what stops you re-explaining the mascot in every Claude Code session.


Mascot

KS has one mascot: Roly the Pangolin (Sunda pangolin). Pip the kancil and Tia the tapir are retired.

⚠️ Roly was PROMOTED, not created — read before you grep

Roly was already the pangolin in the retired trio. He keeps his name, species and personality; what changed is his role (was: pronunciation lessons + SPM Paper 3 Part 3 → now: sole mascot, all states, all parts) and his art (redrawn for v7).

String Status Action
Pip / PIP Unambiguously legacy (kancil) Delete on sight
Tia / TIA Unambiguously legacy (tapir) Delete on sight — use word-boundary match, tia hides inside initial, partial, credentials
Roly / ROLY Ambiguous Old refs = trio-era art and role logic. New refs go through <Mascot>. Never bulk-delete.

The only sanctioned way to render Roly is <Mascot>. A Roly reference that touches an asset path directly is unmigrated legacy code — flag it, don't guess.

Roly's voice (inherited from the character bible)

Gentle, careful, patient, quietly encouraging. The patient one — encourages without pressure, notices small efforts and names them, never rushes, never scolds. Speaks slowly with intentional pauses, and says "we" rather than "you".

Tone guard: this is NOT the old Pip's voice. Pip (kancil) was fast, teasing, trickster-coded — "three seconds, go", "bet you can't" — a register descended from Sang Kancil folklore that does not transfer to Roly. If copy sounds impatient or challenge-y, it is wrong for Roly. When migrating old Pip lines, rewrite the register, don't just swap the name.

(Quiet brand asset: the Sunda pangolin is critically endangered. Roly being beloved is also, gently, a conservation message.)

One art set — the 3D renders, in-product and out

Decision (16 Jul): the 3D rendered art is used in-product as well as in marketing. The team chose the rendered look over a separate flat-SVG product tier, so there's one mascot art set now:

Style Where Assets
3D rendered: soft gradients, painted scale texture, props + particles baked in In-app and external (social, decks, campaigns, app store) assets/mascot/marketing/*.png

The flat neo-brutalist SVG product tier is no longer requiredassets/mascot/product/ stays empty and <Mascot> serves the renders. Because the art is pre-rendered, props and particles come baked in; the particles prop is a no-op while the renders are in use (see "Three layers").

How to render it

Never import a mascot asset directly. Always use the component:

import Mascot, { mascotFor, celebrationFor } from '@/components/Mascot';

<Mascot expression="jubilant" size="lg" particles />
<Mascot expression={mascotFor('lesson.complete')} size="md" />
<Mascot expression={celebrationFor(3)} size="xl" particles />

Props: expression (required), size (sm 48 | md 96 | lg 160 | xl 240), flip, particles, meaningful.

The mascot is decorative by default (aria-hidden). Pass meaningful only when it carries information the copy doesn't.

Three layers — getting these wrong is the most common mascot bug

Layer Examples Where it lives
Props mic, calendar, gold card, trophy, book, pom-pom, bottle, backpack Baked into the art. Part of the pose.
Particles sparkle, confetti, music notes, hearts, motion lines Separate animated layer. Declared per-expression in the manifest; pass particles to turn on. Respects prefers-reduced-motion.
Live UI audio waveform, wifi-off icon, streak counter App-rendered from real data. Never drawn.

The live-UI rule is the one people break. mascot-speaking.svg is the pangolin holding a mic — no speech bubble, no waveform. The app renders the user's real waveform beside it. Bake the waveform in and you ship a recording screen that looks like it's listening whether or not the mic is actually open. Same for offline: the mascot supplies the worry, the Lucide wifi-off icon supplies the fact.

The celebration ladder

Three tiers. Escalation only means something if the top is rare. Do not promote a win to a louder tier because it feels nice.

Tier Expression Fires on
1 celebrating (fists up) A single correct answer or exercise
2 jubilant (confetti jump) Lesson complete, streak milestone
3 triumphant (trophy) Level up, achievement unlocked, course complete, SPM mock passed

If a user sees triumphant weekly, the ladder is broken.

The sixteen expressions

Expressions are state-bound. Pick by product state, not by vibe.

Expression Use when Never use for
greeting Onboarding welcome, first home visit, class-code success
thinking AI processing/evaluating — after the user stops speaking The mic-hot state. That's speaking.
tip Hints, coaching, Say Better suggestions, first-run tooltips
studying Lesson in progress, SPM practice Adult situational (Speak Phrases) flows — reads too academic
correcting Speech correction, mistake review — only alongside encouraging copy Failure/error states. The furrowed brow reads as disapproval. Use encouraging.
speaking Mic is hot — user is talking right now Anything else. This is the recording state.
cheering "Keep going" mid-effort — mid-lesson, mid-progress The mic-hot state (speaking) or pre-action CTAs (encouraging)
encouraging Before action — CTA, retry after failure, motivation
celebrating Tier 1 — a single correct answer Lesson completion. That's jubilant.
jubilant Tier 2 — lesson complete, streak milestone Routine correct answers. Confetti inflation kills confetti.
triumphant Tier 3 — level up, achievement, course complete, SPM mock passed Anything routine
premium The ask — paywall, upsell, trial offer, locked feature Post-conversion. That's grateful.
grateful After they say yes — premium thank-you, referral, win-back, feedback Routine success. Spend this sparingly.
resting Empty states, breaks, paused streak Offline. That's offline.
offline Connection lost, sync failed, generic error
streakBroken Streak broken or lost, streak reminder

Three rules that stop people picking the wrong one:

Rules

  1. State-bound. A new expression may only be added if it maps to a real product state. No mood-only additions — that's how expression libraries rot.
  2. One per screen. Never show two mascot expressions on the same screen.
  3. Keep product calm. The mascot appears at decision and emotional moments — empty, complete, error-recovery, onboarding. It does not sit persistently in navigation or on every screen. This is how we hold the "loud brand / calm product" line.
  4. The mascot is a content layer. It does not carry the 2px outline or offset shadow itself. Wrap it in a v7 card or blob if a screen needs those.
  5. The 3D renders bake in props and particles, so the particles prop is a no-op with them. (If a flat SVG tier is ever added later, particles and live UI go back to being separate layers there.) Caveat: the speaking/offline renders bake in a waveform/wifi glyph — don't pair those two with real mic/connection UI.
  6. We don't scold learners for speaking. correcting never handles failure — that's encouraging. And streakBroken is sad with the user, never at them; always pair it with a recovery CTA so they leave with somewhere to go.
  7. Ladder discipline. Don't inflate celebrations.
  8. No substitution. If a state has no mascot, leave the mascot out and flag it. Don't reach for a nearby expression.

Asset naming

Manifest key is camelCase; the file is mascot- + the key in kebab-case. So streakBrokenmascot-streak-broken.svg. No version suffixes, no _final. Full tree in MASCOT-ASSET-STRUCTURE.md.

Source of truth

mascot.manifest.json — expressions, product-state mappings, particle types, alt text, and file paths. The <Mascot> component derives its types from it, so the two cannot drift. Edit the manifest, not the component.