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 required — assets/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:
speakingvsthinkingvscheering—speaking= the mic is open.thinking= the mic just closed, AI is evaluating.cheering= generic mid-lesson support, no mic involved.encouragingvscheeringis temporal.encouragingfires before the user acts (CTA, retry).cheeringfires while they're mid-effort.premiumvsgrateful—premiumis the ask,gratefulis the thank-you. Never usegratefulto sell.
Rules
- 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.
- One per screen. Never show two mascot expressions on the same screen.
- 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.
- 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.
- The 3D renders bake in props and particles, so the
particlesprop 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: thespeaking/offlinerenders bake in a waveform/wifi glyph — don't pair those two with real mic/connection UI. - We don't scold learners for speaking.
correctingnever handles failure — that'sencouraging. AndstreakBrokenis sad with the user, never at them; always pair it with a recovery CTA so they leave with somewhere to go. - Ladder discipline. Don't inflate celebrations.
- 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 streakBroken → mascot-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.