The brand foundation, in one document. Identity, voice, visual system — every locked decision, captured plainly.
Founders start companies to build something specific. The running of the business eats their time on it. RTSN exists so they can keep building.
The founder + their team, building toward what they started the company for.
The grind that eats the time meant for the building.
Keep building. We'll handle the rest.
The grind has no universal shape — vendor calls, customer surprises, admin that is important but feels forced. It varies in form per founder. The felt experience is the same: being swept away from the work that motivates you. The brand stays on the founder's terms; RTSN is the supporting role.
The full statement of this premise lives in the manifesto, next.
Three acts. ~155 words. Read it slowly.
You started this company to build something specific.
An idea you wanted to make real. A team you wanted to gather. A way of working you knew was possible. Something you could stand behind.
Then the running of the business started taking your time.
Vendor calls. Customer surprises. Admin that is important but feels forced. The work you actually started this for kept getting pushed to the weekend, and then off the calendar entirely.
You started making compromises. The version of the company you imagined kept getting smaller than the one you're stuck running.
We built RTSN to make sure that doesn't happen.
We run the brand and operations alongside your team. AI handles the consistent execution. We work with the team you have, not around it.
The point isn't to take your business off your hands. The point is to keep you in the part of the business you started this for. The building. The deciding. The work that motivates you.
Three pieces of customer-facing copy carry the brand. They lock together — the H1 introduces, the subhead explains, the tagline closes every surface.
You keep building what you started the company for. We run brand and operations alongside your team. AI handles the consistent execution, so your team can focus on what matters.
The horizon, and the daily compass. The brand has one centre-of-gravity phrase that operates at multiple scales — Vision expands it, Mission compresses to it, Tagline IS it.
Builders — anyone who makes things, not just founders. The Vision scales beyond the immediate buyer to the wider tribe.
Identical to the tagline. The brand has one centre-of-gravity phrase that operates at multiple scales — Vision expands it, Mission compresses to it, Tagline IS it.
Calm-capable, reader-first, matter-of-fact. The customer is the grammatical subject when possible; RTSN is supporting. Confident — but doesn't perform it. The brand disappears into the proposition.
Reference register: Stripe, Linear, Mercury, Pilot, Notion. Service-tier brands that talk like they have nothing to prove.
If a draft hits any of these, rewrite:
The voice in actual use:
| Surface | Voice |
|---|---|
| 404 | "This page doesn't exist. Use the navigation above, or head back to the homepage." |
| Loading | "One moment." |
| Empty state | "No engagements yet. When one begins, it shows up here." |
| Confirmation | "Thanks, we'll be in touch within two business days." |
| Unsubscribe | "Unsubscribed. Take care and keep building." |
| Payment failure | "The payment didn't go through. Your work continues. We'll retry, and reach out if it doesn't clear within 24 hours." |
| Onboarding | "We've got it from here. The system starts running tonight. We'll tell you what changed by Friday." |
| CTA | "Start a conversation" or "See how we work" |
Before shipping copy, ask:
Three squares ascending. A geometric wordmark beside it. Together — the brand mark. Constructed against cap-height, calibrated for optical balance, documented per tier-1 convention.
Three 12-unit squares, offset by 12 units diagonally, climbing from bottom-left to top-right on a 48-unit grid. Mathematical. No decoration. The mark for the brand.
Set in DM Sans Bold (700). Uppercase, letter-spacing: 0.02em. The wordmark uses the body typeface at bold weight — geometric grotesque DNA matches the cascade's square geometry. Distinct from the editorial display serif (Newsreader) used for taglines elsewhere.
Variant C — Symbol-led. Cascade at 1.1× cap-height, centred on the cap-band (excess 0.05cap above cap-line and 0.05cap below baseline — symmetric). Wordmark to the right, ⅓cap ink-to-ink gap. This is the default lockup — the mark that goes on every surface unless a reason to switch.
Four calibrated variants share the construction principles. C is the default; A, B, D for context-specific use.
The cascade is centred on the cap-band — the same vertical band as the uppercase wordmark letters. Excess height (cascade is 1.1cap, cap-band is 1cap) distributes equally above the cap-line and below the baseline. Both gap brackets read 0.05U. Two views: construction-only on top, clean lockup below.
The lockup uses the modern CSS cap unit (with em fallback) — symbol height scales as a multiple of the wordmark's cap-height at any font-size. Change one number; everything scales.
The lockup inherits colour from its container via currentColor. Five backgrounds, one lockup spec.
Reserve a minimum clear-space of 6 viewBox-units (half a square-width) on every side of the symbol. No other graphic element, type, or chrome may enter this zone.
The mark works because it's mathematical. Stretching breaks the cascade. Rotation breaks the ascent reading. Off-palette colour breaks the system. Ornament breaks the discipline.
Six tokens. Three chrome (Canvas, Ink, Muted) for typography and structure. Three accents (Signal, Verdant, Sapphire) for semantic colour — one warm hero balanced by two cool supporting voices. LCH-calibrated as a sibling family.
Hue arc: 14° → 174° → 205°. Warm earth, cool botanical, deep water. A coherent material sequence — not a colour wheel. The brand defaults to chrome + Signal; Verdant and Sapphire join when content relevance calls.
Each accent has a content register. Colour does meaning-work — it tells the reader what kind of content they're in.
Same composition, three accents. Each accent carries the same content into a different semantic register.
Canvas does 60% of any layout. Ink + Muted do 30% (type + structure). Accents combined do no more than 10%. This is the chrome-calms / content-carries discipline in numbers — the brand should feel calm at a glance, and the colour should only earn the eye when meaning calls.
| Pairing | Ratio | Status |
|---|---|---|
| Ink on Canvas | 17.46:1 | AAA (any size) |
| Muted on Canvas | 5.05:1 | AA (normal text) |
| Signal on Canvas | 4.21:1 | AA-large (decorative / large only) |
| Verdant on Canvas | 4.53:1 | AA (any size) |
| Sapphire on Canvas | 6.22:1 | AA (any size) |
Signal as body text fails AA — never used at < 18pt without bolding. Verdant and Sapphire pass at all sizes.
DM Sans for body and the wordmark; Newsreader for taglines, headlines, manifesto; DM Mono for system data. Free Google Fonts, editorial-functional register — same neighbourhood as the Klim Söhne + Tiempos standard (The Guardian, Stripe Press, Anthropic) without the licensing.
A small Singapore studio. Founders hire us to take the brand and operations work off them. AI handles the consistent execution; humans handle the judgment.
Eyebrow (mono) → H1 (sans) → lede (sans, muted) → tagline (display serif, Signal-coloured) → H3 (sans, semibold) → body (sans). Six type roles in one composition.
Layered radial mesh gradients — warm Signal ellipses joined by cool Verdant and Sapphire layers, blurred, slow. On identity surfaces. Static restraint on functional surfaces. Content carries colour; chrome stays neutral.
Preview pages that demonstrate the system applied to real brand touchpoints. Each surface chooses a different register from the system.
Hero with atmospheric mesh + bento section. Calm, generous, type-led. Signal accents only.
Three acts on light. Layered mesh per act. Scroll-driven text reveals. Closing tagline at near-max scale.
Metrics + bento. Stripe / Mercury discipline. Information-dense without performance.
Nine categories of common misuse, paired side-by-side with the on-brand version. When in doubt whether something is on-brand, this is the reference. The red disc marks what to avoid; the green disc marks what to do instead.
brand/v5-art-direction-photography.md. Photography earns its place by being real.brand/v5-art-direction-motion.md. The surface should feel alive on second look, not active on first.brand/v5-art-direction-iconography.md. Same geometric DNA as the cascade.brand/v5-art-direction-illustration.md. Structural, not decorative.If a piece of work would have been more at home on a generic SaaS launch page than in The New Yorker, in a Pentagram case study, or on Aesop's website — it's wrong-brand. Rewrite, redesign, or reject.
A two-character system. One mascot for marketing, one for product. Both built from the cascade. Both pixel-native. Ten states for the character, one face for the icon-form.
The mascot system has two halves. Mini Mason (24×24 pixels) is the marketing character — small chibi figure with the cascade as a hat, ten emotional/operational states, used in campaigns, social posts, illustration, and any moment where the brand needs a someone. Cascadia Plus (16×16 pixels) is the icon-form mascot — the cascade with two stacked dot eyes, used in favicons, signatures, product UI presence, and any place where the brand needs a marker too small for a full character.
Both are pixel-art native. Both use only the locked six-token palette (plus computed tonal highlights/shadows). Both follow strict animation discipline — pixel-correct frame-step transitions, never smooth interpolation. Neither has a backstory or a public name. They are presence, not personality on a plate.
Ten states. Each has a colour, a trigger context (where it appears), and a forbidden context (where it must never appear). Static renders shown — animations live in assets/mascot/mascot.css.
Tier 3 ships 13 sprite-atlas animations for Mini Mason — the game-ready motion vocabulary that turns the marketing character into an interactive presence for the website, web component, and any game-engine surface. Atlas at assets/mascot/sprite/mason-motion.png (1001×26, 40 frames, 26 tags). Atlas JSON sidecar at mason-motion.json in Aseprite format with RTSN-enriched pivot + anchor extensions for Phaser 3 createFromAseprite().
| Animation | Frames | Cadence | Direction | Trigger / use |
|---|---|---|---|---|
| walk-r / walk-l | 3 (0-2) | 8fps · 500ms | ping-pong | Continuous walking loop |
| jump-r / jump-l | 4 (3-6) | 8fps · 500ms | forward one-shot | Triggered by jump-event |
| land-r / land-l | 2 (7-8) | varied · 250ms | forward one-shot | Follows jump on ground-contact |
| run-r / run-l | 3 (9-11) | 10fps · 400ms | ping-pong | Faster locomotion; wider stride than walk |
| celebrate-r / celebrate-l | 4 (12-15) | 6fps · 1002ms | ping-pong | Joy moment; arms raise + bounce |
| look-around-r / look-around-l | 3 (16-18) | 4fps · 1000ms | ping-pong | Eyes shift L → C → R; head + body unchanged |
| nod-yes-r / nod-yes-l | 3 (19-21) | 4fps · 1000ms | ping-pong | Head bobs vertically; affirmation |
| shake-no-r / shake-no-l | 3 (22-24) | 4fps · 1000ms | ping-pong | Head shifts horizontally; negation |
| curious-tilt-r / curious-tilt-l | 2 (25-26) | 5fps · 400ms | ping-pong | Head tilts right (thoughtful) |
| eureka-r / eureka-l | 2 (27-28) | 300+700ms asymmetric | ping-pong | SIGNAL sparkle at top-left + eyes widen vertically; the "AHA!" moment held 2.3× longer than the build for emphasis |
| dance-r / dance-l | 4 (29-32) | 5fps · 800ms | forward loop | Restrained Sage-register bounce + arm swing |
| surprise-r / surprise-l | 5 (33-37) asymmetric | 400+150+200ms · 750ms | one-shot (R FORWARD, L REVERSE) | Peak shock → mid-rebound → canonical recovery; 2px body-shift recoil + 1×3 alert eyes + canonical mouth (Sage restraint preserves the wise-advisor register) |
| point-r / point-l | 2 (38-39) asymmetric · static | held while pointing | static | Arm extended horizontally; directional indicator |
Mason-symmetry pattern (11 of 13 animations). Mason has no facing-direction features — no nose, symmetric eyes, no held props in motion poses. Horizontal mirroring across the body centerline is a visual no-op: mirrored pixel positions are identical to originals (only the semantic labels "left arm" vs "right arm" swap). The atlas exploits this by giving R/L tag pairs the same frames; explicit -l tags exist so game engines never reach for sprite-flip (which would invert the hat). The hat-preservation discipline is enforced by providing the L tag, not by changing pixels. ~27% smaller atlas vs the naive author-each-direction estimate.
Shared-recovery trick (surprise). Surprise-r and surprise-l share the canonical recovery frame via REVERSE-direction tag. Using Aseprite's 1-indexed convention (F34, F35, F36, F37, F38 — corresponding to the table's 0-indexed range 33-37 above): F34 wide-r → F35 mid-r → F36 canonical (surprise-r tag, FORWARD); F38 wide-l → F37 mid-l → F36 canonical (surprise-l tag, REVERSE). Both tags overlap on F36 but never play each other's wide frame. Saves 1 frame per pair. (Note: the table above uses JSON 0-indexed ranges 33-37; this paragraph uses Aseprite 1-indexed labels F34-F38 — same frames, different counting convention by source format.)
Authoring pipeline. Atlases authored via Aseprite (secure-from-source compile, WebSocket disabled) + Lua scripts + PowerShell wrapper at assets/mascot/aseprite/scripts/. Six hard-won lessons-learned rules documented in scripts/README.md (Aseprite headless quirks, tag auto-extension defensive snapshot+restore pattern, PowerShell ASCII discipline, etc.). For future characters or expansion animations, fork the create-mason-*.lua scripts as templates and follow the existing 6-batch authoring cadence (PRs #271-285 are the reference implementation).
Sixteen-pixel mascot for favicon, signature, and product-UI presence. The cascade with two 1×2 stacked dot eyes on the top block (Curious face). No mouth. Tier 1/2 CSS-driven blink: open eyes 5060ms, closed eyes 220ms, matching the v6 CSS asymmetric timing. (The Tier 3 atlas-driven blink uses a tighter 1000/150ms ping-pong cycle — see the Tier 3 vocabulary table below; use Tier 3 timings when consuming the cascadia-motion.png sprite atlas, Tier 1/2 timings when using the SVG + CSS pattern.)
Tier 3 ships a 4-expression vocabulary + blink + cascade-pulse loading state + bare-body for Cascadia — turning the icon-form mascot into an interactive state-indicator for the website, web component, and animated favicon. Atlas at assets/mascot/sprite/cascadia-motion.png (171×18, 10 frames, 7 tags). All expressions use vertical eye-position to convey state (raised → canonical → taller → lowered); the cascade body never changes shape so the brand mark stays intact.
| Tag | Frames | Duration | Trigger / use |
|---|---|---|---|
| default | 1 static | — | At-rest state; the canonical Cascadia presence |
| blink | 2 (default + eyeless) ping-pong | 1000ms open + 150ms closed | Subtle liveness signal; runs on idle |
| happy | 1 static | — | Positive interaction acknowledgement; successful action |
| surprise | 1 static | — | Unexpected event; reveal moment. Includes 1px SIGNAL spark in empty top-left space |
| busy | 1 static | — | Focused/working state; eyes looking-down concentration |
| cascade-pulse | 4 frames forward loop | 150ms/frame · 600ms cycle | Loading state. SIGNAL color highlights each cascade block in sequence (bottom → middle → top → reset). Eyes remain CANVAS-carved during pulse — Cascadia stays "alive" during the loading animation. The cascade brand mark visually "powers up" as the loading wave sweeps |
| bare-body | 1 static | — | Cascade blocks only, no eyes. For SVG-overlay pipelines that composite eyes as a separate layer (preserves the Tier 1 + Tier 2 multi-layer pattern) |
Interactive-state language for the web component. The web component (deferred — ships after Claude Design + website build kicks off) consumes this atlas to render Cascadia state-by-state in the page-corner widget + favicon + tab-title indicator:
default (with occasional blink for liveness)cascade-pulse looping until readybusy statichappy hold (300-500ms), then return to defaultsurprise hold (400ms), then return to defaultshape-rendering="crispEdges" and CSS image-rendering: pixelated for pixel-perfect output at any display scale.All seven tonal hex values verified by linear sRGB blend formula (audit-confirmed):
Pixel-art motion is frame-stepped, never smoothly interpolated. Tier 1 SVG+CSS animations use steps(N) timing functions (rule set at assets/mascot/mascot.css). Tier 3 motion atlases are driven by sprite frames advanced at fixed per-frame durations (no easing, no interpolation between frames). Smooth easing is reserved for non-pixel motion only (gradient opacity, mesh-halo backgrounds).
FPS allowances by animation category (revised in Tier 3 spec; supersedes the blanket 2fps rule):
| Still-state animations (breathing, smoke, mesh-halo, eye blink, thought particles) | 2 fps max | Calm-capable Sage register; faster reads as anxious |
| Joy moments (look-around, surprise, dance, eureka, curious-tilt, nod-yes, shake-no) | 4-6 fps | Discrete joy expression; composed but visible |
| Celebrate (motion-cycle joy) | 6 fps | Bridge between joy and locomotion; restrained energy |
| Locomotion (walk, run, jump, land) | 8-10 fps | Standard pixel-art convention; needed to read as movement |
| Loading state (cascade-pulse on Cascadia) | ~6.7 fps (150ms/frame) | Cascade highlight sweep at activity-state cadence |
No animation exceeds 10 fps in any category. Smooth 24fps interpolation remains explicitly banned. The bounce/elastic/jump-cut prohibition holds across all categories.
Cascade-hat directional rule. The cascade ascends up-right by definition. Mini Mason's hat preserves this direction. Engine-flipping for left-facing motion reverses the cascade to up-left — which breaks the brand mark. The rule, refined by the Tier 3 bilateral-symmetry discovery:
-l tags pointing at the same frames so game engines never reach for sprite-flip (which would invert the hat to up-left). Use the tag, not the flip.shape-rendering="crispEdges", image-rendering: pixelated) on every containersteps(N) timing only; smooth interpolation produces sub-pixel anti-aliasing that violates pixel-art disciplineAll mascot files at assets/mascot/. Folder structure:
svg/ — 12 source SVGs (10 Mason states + Cascadia Plus default + Cascadia blink frame)png/512/, png/192/, png/180/ — pixel-perfect PNG exports at canonical sizessprite/ — atlas inventory:
mason-motion.png + mason-motion.json — 1001×26, 40 frames, 26 tags (13 animations × R/L variants per Mason-symmetry pattern)cascadia-motion.png + cascadia-motion.json — 171×18, 10 frames, 7 tags (4 expressions + blink + cascade-pulse + bare-body)mason.png (651×76, 54 frames in 3 rows — body + eyes + baked)cascadia.png (35×52, 5 frames in 3 rows)mason-together.png (32×24 standalone, exempt from atlas)aseprite/ — Tier 3 authoring pipeline: cascadia-motion.aseprite + mason-motion.aseprite source files plus scripts/ directory (Lua authoring scripts, lib-rtsn-brand.lua palette, build-atlas.ps1 wrapper, repair-tag-ranges helper, six lessons-learned rules in README.md). Aseprite binary at D:\Tools\aseprite-source\build\bin\aseprite.exe (compiled secure-from-source with WebSocket disabled).favicon/ — 6-file favicon kit (SVG + ICO + Apple Touch + Android 192/512 + manifest)mascot.css — 13 Tier 1 animation rules (SVG + CSS keyframes for the original 10 Mason states + Cascadia blink). Tier 3 motion is driven by sprite atlases, not CSS.mascot-sprite.svg — 11 <symbol> elements for CDN-served sprite pattern (Tier 1 still-states only; Tier 3 motion frames are PNG, not SVG)README.md — usage guide for inline / sprite / Phaser / favicon integrationPer-state frame counts. Tier 2 atlas (eyes always open in state frames; eye overlay handles blink): at-rest 1, breathing 4, working 1, building 2, thinking 4, coffee-break 6, compounding 4, asleep 2, greeting 2. State animation cycles range 1.2-4s; eye overlay blink is asymmetric 5060/220ms. Tier 3 motion atlas (mason-motion): see the Tier 3 motion vocabulary section above for the 13-animation breakdown.
Visual viewers. Tier 2: preview/sprite-sheet-viewer.html — 3-layer atlas overview with body+eyes-overlay compositing. Tier 3: per-character Downloads previews at C:/Users/kenny/Downloads/rtsn-mascot-motion/preview.html (Mason, 13 animations) and C:/Users/kenny/Downloads/rtsn-cascadia-motion/preview.html (Cascadia, 4 expressions + cascade-pulse). Build pipeline: Tier 2 via scripts/build-mascot-sprite-sheets.js (idempotent, regenerates Tier 2 outputs from svg/); Tier 3 via assets/mascot/aseprite/scripts/build-atlas.ps1 (Aseprite headless + Lua, with palette enforcement + binary-alpha verification). Verification: scripts/verify-sprite-sheets.js (31/31 checks passing — confirms dimensions, frame bounds, anchor pivots, source-SVG hash freshness across the consolidated sprite sheets).
Tier 3 SHIPPED (2026-05-17). The Tier 3 game-ready expansion documented at research/mascot-game-ready-framework.md shipped over Phase 3.0-3.4: Mason motion atlas (40 frames / 26 tags / 13 animations) at PRs #271-285, Cascadia motion atlas (10 frames / 7 tags / 4 expressions + cascade-pulse) at PR #286. Full frame-by-frame authoring spec at research/mascot-tier3-frame-spec.md. Deliverable spec at research/mascot-deliverable-framework.md.
The mascot system is not a side asset — Mason and Cascadia carry meaningful weight in every customer-facing artefact the studio ships. The patterns below codify where each character appears, what state per surface, and what's off-limits. Apply them when producing decks, brand-book pages, content artefacts, proposals, product UI, and signatures — anywhere the brand speaks.
Discipline: mascots appear where the brand needs warmth or presence — never where the brand needs distance (sales, legal, crisis). The Do's + Don'ts above are the floor; the table above is the holistic playbook. When in doubt, ask: is this surface a moment for the brand to be a someone, or a moment for the brand to be a service? Mascot if the former, type-only if the latter.
Sizing across surfaces: Mini Mason renders at 48-96px for editorial moments (deck title slides, blog headers, social posts), 24-48px for inline body embeds, 16-24px for footer/sidebar presence. Cascadia Plus renders at 16px for favicon-class use (email signatures, document corners), 24-48px for nav/header presence, 96px+ for hero moments. Below 16px, both characters lose pixel clarity — use the wordmark or lockup instead.
The website is the studio's primary brand artefact, and the mascot system is its most distinctive interactive layer. Apply the patterns below across every page of the website + the design-system documentation page (/the-system). The interactive web component spec below documents the underlying mechanism.
Cascadia lives in the primary nav corner across every page. State is driven by site events:
default · with occasional blink every 5-8 seconds for livenesscascade-pulse · 4-frame 600ms loop until new page paintsbusy · static · held for duration of fetchhappy · 300-500ms hold · returns to defaultsurprise · ~400ms hold · returns to defaulthappy on enter · returns to default on leavesurprise on click · returns to default after 400msMason appears at meaningful section breaks, in margin commentary, in case studies (state-coloured to match), and in the methodology flow as the user's reading companion. Each appearance is purposeful — not gratuitous. Page-by-page expectations:
The website consumes both atlases (mason-motion.png + cascadia-motion.png) via a small interactive web component. The component handles atlas loading, frame advancement, state transitions, and pointer-event reactions. Three implementation options exist; pick based on the surface's needs.
| Approach | When to use | Cost |
|---|---|---|
| Vanilla canvas (~150 LOC) | Default. Marketing website. Single Cascadia in nav + occasional Mini Mason embeds. No collision detection or physics needed. | Zero dependencies; fastest first paint; minimum bundle weight. |
Phaser 3 (createFromAseprite()) | If the site needs game-engine features (interactive scenes, gamified onboarding, in-product mascot interactions). Loads Phaser 3 (~600 KB). | Higher bundle weight; more capabilities; battle-tested atlas consumption. |
| Pure SVG + CSS keyframes | For Cascadia-only contexts where atlas is overkill (favicon animation, single-state badges, brand-book inline demos like the one below). | Zero JavaScript; works in CSS-disabled contexts via static fallback; limited to predetermined state transitions. |
Both atlases ship as PNG + Aseprite-format JSON sidecar. The sidecar carries per-frame durations + per-tag direction (FORWARD / REVERSE / PING_PONG) + RTSN-enriched pivot (per-frame anchor) + anchor (sprite-level bottom-centre alignment). Load pattern:
// Load atlas once at page init
const atlas = await fetch('/assets/mascot/sprite/cascadia-motion.json').then(r => r.json());
const img = new Image();
img.src = '/assets/mascot/sprite/cascadia-motion.png';
await img.decode();
// State machine: event → tag transition
const cascadia = new MascotRenderer({ atlas, img, anchor: atlas.meta.anchor });
cascadia.mount(document.querySelector('#nav-cascadia'));
cascadia.play('default');
// Wire site events to state changes
window.addEventListener('navigation:start', () => cascadia.play('cascade-pulse'));
window.addEventListener('navigation:complete', () => cascadia.play('default'));
window.addEventListener('form:submitting', () => cascadia.play('busy'));
window.addEventListener('form:success', () => cascadia.playOnce('happy', 400).then(() => cascadia.play('default')));
window.addEventListener('form:error', () => cascadia.playOnce('surprise', 400).then(() => cascadia.play('default')));
// Direct pointer reactions
nav.addEventListener('mouseenter', () => cascadia.playOnce('happy', 300));
nav.addEventListener('click', () => cascadia.playOnce('surprise', 400));
Vanilla canvas implementation: a single requestAnimationFrame loop reads the current tag's frame durations from the JSON sidecar, advances the active frame index when accumulated time exceeds the current frame's duration, and draws the source-rect from the atlas PNG to the canvas. Honours tag direction:
The eureka tag is the only animation with asymmetric per-frame durations (300ms build + 700ms held AHA per the source JSON). All other tags use uniform durations within a tag. The renderer must read frame.duration per-frame rather than assuming a fixed FPS per tag.
happy; leave handler returns to defaultsurprise for 400ms one-shot, then defaultbusy while form has focus; returns to default on blurbusy while modal is open (signals attention focused elsewhere)@media (prefers-reduced-motion: reduce) globally. When set: animations disabled; mascots render as static default state only. Hover affordances remain (cursor change) but no state transitions trigger.transform: translateZ(0) on the canvas element for GPU acceleration. No top / left animation; transform: translate() only.requestAnimationFrame loop when the mascot is offscreen (use IntersectionObserver). Resume on re-entry.image-rendering: pixelated CSS; do not upscale via canvas drawImage (loses crispness).Below is the cascade-pulse loading animation running live in this brand book — implemented via pure SVG + CSS keyframes (no JS dependency) as a proof-of-pattern for the SVG + CSS option above. The cascade blocks cycle SIGNAL through bottom → middle → top → reset at 150ms per phase (600ms total cycle). The eyes carved from the top block remain visible throughout the pulse, preserving Cascadia's "alive" presence during the loading state.
Pattern proven:
prefers-reduced-motion — animations replaced with static default state when reduced motion is requestedcascadia-motion.pngProduction-grade implementation (vanilla canvas atlas renderer, ~150 LOC):
class MascotRenderer {
constructor({ atlas, img, anchor }) {
this.atlas = atlas; // parsed Aseprite JSON
this.img = img; // loaded HTMLImageElement
this.anchor = anchor; // { x: 0.5, y: 13/16 } for Cascadia
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.ctx.imageSmoothingEnabled = false; // pixel-perfect
this.currentTag = null;
this.frameIdx = 0;
this.direction = 1; // 1 for FORWARD, -1 for REVERSE/PING_PONG-back
this.elapsed = 0;
this.lastTime = 0;
this.rafId = null;
}
mount(host) {
host.appendChild(this.canvas);
this.canvas.style.cssText = 'image-rendering:pixelated; transform:translateZ(0);';
this._observe(host); // IntersectionObserver to pause when offscreen
}
play(tagName) {
const tag = this.atlas.meta.frameTags.find(t => t.name === tagName);
if (!tag) throw new Error(`Unknown tag: ${tagName}`);
this.currentTag = tag;
this.frameIdx = tag.direction === 'reverse' ? tag.to : tag.from;
this.direction = tag.direction === 'reverse' ? -1 : 1;
this.elapsed = 0;
if (!this.rafId) this._startLoop();
}
playOnce(tagName, ms) {
return new Promise(resolve => {
this.play(tagName);
setTimeout(() => resolve(), ms);
});
}
_startLoop() {
this.lastTime = performance.now();
const tick = (now) => {
const delta = now - this.lastTime;
this.lastTime = now;
this.elapsed += delta;
const frame = this.atlas.frames[`frame-${this.frameIdx}`];
if (this.elapsed >= frame.duration) {
this.elapsed = 0;
this._advanceFrame();
}
this._draw();
this.rafId = requestAnimationFrame(tick);
};
this.rafId = requestAnimationFrame(tick);
}
_advanceFrame() {
const { from, to, direction } = this.currentTag;
this.frameIdx += this.direction;
if (direction === 'pingpong') {
if (this.frameIdx > to) { this.frameIdx = to - 1; this.direction = -1; }
else if (this.frameIdx < from) { this.frameIdx = from + 1; this.direction = 1; }
} else if (this.frameIdx > to) { this.frameIdx = from; }
else if (this.frameIdx < from) { this.frameIdx = to; }
}
_draw() {
const f = this.atlas.frames[`frame-${this.frameIdx}`].frame;
this.canvas.width = f.w;
this.canvas.height = f.h;
this.ctx.clearRect(0, 0, f.w, f.h);
this.ctx.drawImage(this.img, f.x, f.y, f.w, f.h, 0, 0, f.w, f.h);
}
_observe(host) {
const io = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting && this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; }
else if (e.isIntersecting && !this.rafId) { this._startLoop(); }
});
});
io.observe(host);
}
}
Above pattern works for both Cascadia (16×16) and Mason (24×24) — the renderer is dimension-agnostic. Drop into any page surface; wire to events. For framework integrations, the renderer can be wrapped as a React hook (useMascot(tagName, options)), Vue composable (useMascot()), Svelte action, or Web Component (<rtsn-mascot character="cascadia" state="default">) — pick the wrapper that matches the host stack.
The two characters are coordinated, not isolated. Cascadia handles persistent state in the nav corner; Mini Mason handles editorial moments inline with content. When both are present on the same page, they obey relational rules:
default during editorial moments unless a site-state event interrupts.happy (nav corner) while Mason plays celebrate at the form completion location. Two-place celebration reads as "the system noticed your action AND celebrates with you." 300-500ms hold then both return to defaults.busy (focused on solving) and Mason stays at at-rest (does not react theatrically). Recovery copy is factual; mascots support the calm-capable register.For the design-system documentation page (/the-system), all 13 Mason animations and all 7 Cascadia states should be demonstrated in isolation with their trigger contexts annotated — this serves both as developer reference and as a brand artefact showing the system's craft.
When everything locked, and what informed it.
The v5 brand substrate locked in this book is the canonical reference for all new work. The rtsnstudios.com website has been rebuilt on the v5 brand and cut over — the live site renders v5. The internal token pipeline (the codegen palette source, the design engine, and the brand validators) is being migrated from v4 to v5; until that migration lands, some engine-rendered templates may still produce v4-styled output. New customer-facing artefacts should be reviewed against this brand book (v5).
Voice calibrated against twelve contemporary B2B brand homepages: Stripe, Mercury, Linear, Pilot, Notion, Vanta, Loom, Webflow, Vercel, Anthropic, Figma, Ramp. Visual orbit informed by Stripe, Mercury, Immersive Garden, David Whyte. Palette neighbourhood: Aesop, COMO, Anthropic. Logo construction methodology: Pentagram (Mastercard 2016, Slack 2019), Wolff Olins, Collins, DesignStudio (Airbnb Bélo). Typography: free Google Fonts (DM Sans + Newsreader + DM Mono) carrying the same editorial-functional register as the Klim Söhne + Tiempos standard (The Guardian, Stripe Press, Anthropic) — the licensed Klim pair was considered but the free pair carries the register sufficiently and simplifies downstream licensing.
Three research memos informed the locked decisions:
research/visual-references-analysis.md — Stripe / Mercury / Immersive Garden / David Whyte teardownresearch/color-theory-refinement.md — LCH-calibrated accent family + Anthropic / Aesop neighbourhoodresearch/logo-construction-methodologies.md — Pentagram-style construction with cap-height as unit + Mastercard precedentThis brand book is the working reference for engagement rtsn-studios-v5. Singapore-based. v5 replaced shelved earlier work; the previous engagement at engagements/rtsn-studios-v4/ closed. The book lives in git on branch claude/v5-brand-foundation, based on origin/main.
Single self-contained HTML file. Renders in any browser. Refresh whenever the locked artefacts change.
For PDF export, asset-library packaging, and client handoff: see brand/v5-export-guide.md. Three PDF-export tiers documented (browser print / print-CSS / headless Chrome via puppeteer), with a generation-script template for assembling the full handoff package (Brand Book PDF + Asset Library SVGs + PNG exports + brand spec docs + walkthrough). Logo assets live at assets/logo/ — symbol + lockup in 12 SVG variants covering currentColor + 5 backgrounds, all production-ready.