Theming.
Color, texture, provenance, footer material, and player chrome make a Profile World feel authored.
The goal is that no two casset pages look the same, but every casset page feels like Casset. Cover art is the default seed, but creators can now override the primary color, add profile textures, and choose provenance badges. The system should feel like atmosphere authoring, not theme configuration.
Color extraction
On first load, useDynamicTheme (see hooks/useDynamicTheme.ts) samples the cover via an offscreen canvas. It throws away near-black and near-gray pixels, then clusters the rest into buckets and picks the most saturated dominant bucket.
That single HSL seed expands into a set of tokens the UI consumes directly:
interface DynamicThemeTokens {
base: string // hsl(…) — the seed
surface: string // translucent pill bg
accent: string // progress bars, highlights
glow: string // box-shadow tint (alpha baked in)
gradientEnd: string // darker endpoint for hero fades
footerStart: string // footer gradient top (translucent)
footerEnd: string // footer gradient bottom (more opaque)
isExtracted: boolean // false means we fell back to neutrals
}The extraction runs once per cover URL, caches the result in memory, and can also be pre-computed and stored on Artist.themeJson. When stored, the server returns tokens with the rest of the casset data, so the first paint on a cold load is already themed — no flash-of-gray.
Creator overrides
In the desktop preview panel, owners can choose a curated primary color, pick a custom color, or reset to the cover-derived color. Custom swatches persist their HSL seed in Artist.themeJson; the embedded fan preview receives the override immediately through a scopedpostMessage event so the owner sees the change before the round trip completes.
Why HSL
Operating in HSL makes the derivations trivial. We nudge lightness up for hover states, desaturate for disabled, bump hue by ±8° for secondary accents. In RGB those would all be bespoke palettes; in HSL they're one-liners.
Per-track accents
When a track is actively playing, its row is tinted with an accent derived from the casset theme. The math:
accent.text = hsl(h, min(s+0.15, 0.75), min(l+0.30, 0.70))
accent.bg = hsla(h, s, l, 0.22)
accent.border = hsla(h, min(s+0.1, 1), min(l+0.12, 0.55), 0.4)The result: a subtle, always-legible highlight that inherits the page palette rather than a hard-coded purple. Unlocked tracks vs hook-only tracks use the same accent with different alphas so the visual hierarchy is obvious without extra copy.
Profile textures
Profile texture is the tactile layer on the casset/profile surface. It is stored as profilePatternId for compatibility with older API names, but the feature now renders as CSS texture overlays.
- Clean (
none) — plain material. - Soft grain (
soft-grain) — fine record-sleeve noise. - Paper fiber (
paper-fiber) — worn zine paper. - Halftone (
halftone) — printed poster dots. - Scanlines (
scanlines) — cassette monitor lines. - Paint fog (
paint-fog) — soft spray texture.
The profile page resolves the selected texture against the current hue and saturation, then composes it over the casset device background. Texture changes are live-previewed in the embedded phone and persisted through PATCH /api/studio/casset/[id]. The implementation source is lib/casset-profile-patterns.ts.
Profile provenance badges
Casset supports two profile-level provenance badges: AI profile and No AI used profile. They are intentionally mutually exclusive. Settings and Studio write them through /api/auth/update-profile, and the preview header displays the selected badge beside verification state.
These badges are not moral rankings. They are identity and provenance signals for an AI-era music culture where creators may be fully human, AI-assisted, hybrid, or intentionally no-AI.
Footer themes
The media footer at the bottom of a casset is the same component everywhere, but its "material language" is picked per-artist. The config is declarative (lib/footer-themes.ts), so switching materials doesn't touch the render logic.
- minimal_luxury — smoked glass, restrained shadows, default on new cassets.
- retro_tape — warm analog cassette-deck hardware with inset grooves and tactile buttons. (Currently gated, see file.)
- futuristic_collectible — glossy sculpted UI with cinematic glow rings around artwork. (Gated.)
Each theme declares everything the footer needs — backdrop blur, shadows, hardware inset strength, waveform treatment — as plain data. Nothing imports React, so the config also flows through server-side validation when an artist saves a selection.
Retro player skins
Skins are the loudest listener-side lever over presentation. They replace the entire mobile player surface with something that looks like a different device while preserving the creator's sound and atmosphere.
Track list + animated visualizer. The baseline look.
Click wheel, monochrome LCD, and a brushed-metal shell.
D-pad navigation, pixel-LCD track list, startup chime.
Landscape tape deck with spinning reels synced to playback.
Skin selection is stored per-user (not per-artist) in casset_player_skin via lib/player-skin.tsx. The chosen skin persists across casset pages, so a listener who loves the Game Boy deck stays in that skin for everyone they listen to.
The holographic profile card
On the /[slug] page, tapping the profile avatar opens a sheet with a rotating, chromatically-iridescent card — a nod to holographic trading cards. The effect is pure CSS:
- Conic gradients stacked with
mix-blend-mode: color-dodgeso the base art shows through the sheen. - Pointer-position-driven
translate3don a light-pass layer so the hologram tracks the viewer's finger / cursor. - A second radial gradient in
mix-blend-mode: overlayadds the soft highlight that makes it feel glossy.
Everything lives in app/preview/HoloProfileCard.tsx + HoloProfileCard.css — isolated so you can lift it wholesale if you want to reuse the effect somewhere else.
Chromatic devices (marketing)
The colored dripping phones on / and /features ride the same accent-from-art idea, but deterministic: each device has a fixed color / colorDark pair pulled from its actual play-button hue. The drip SVG is seeded by index so each phone gets a unique-but-stable stream pattern — no hydration mismatch, no layout shift on first paint.
Designing without the tokens
If you're building a new surface and want it to feel on-brand without fighting the extractor, use the constants from app/docs/_shared.tsx:
export const BG = "#0B0908"
export const INK = "#F7F0E6"
export const INK_SOFT = "rgba(247,240,230,0.72)"
export const INK_MUTE = "rgba(247,240,230,0.48)"
export const INK_FAINT = "rgba(247,240,230,0.14)"
export const ACCENT = "#FF7A4D"This is the editorial palette used by the docs surface: atmospheric black, warm paper text, restrained signal colors, and analog warmth. Paired with AeonikPro for text and Cobra VIP for the casset wordmark, it's enough for 95% of first-draft layouts.