⌂ 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 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:

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. Left bare on a background he reads as a sticker dropped on the screen rather than part of it.
  5. Never on a warm surface — no Flame, no Spark. Roly's art is ~36% warm red-orange (his dominant pixel bucket is rgb(255,64,32); Flame is rgb(230,76,9)), so a warm block camouflages him instead of presenting him. Measured separation from his dominant colour: Flame 36 · Spark 102 · Sunflower 143 · Pulse 171 · Ink 223 · Moss 242 · Haze 258 · Paper 294 · Blue 313. Prefer Haze, Paper, Blue, Moss or Ink; treat Sunflower as marginal and check it. If a screen needs a Flame block, keep Roly off it and put him on the neutral part of the screen. Card and ColorBlock publish their tone through SurfaceToneContext, and <Mascot> dev-warns when it lands on a forbidden one.
  6. 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.
  7. 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.
  8. Ladder discipline. Don't inflate celebrations.
  9. 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.