1 ยท The concept
PRISM is a fictional bank whose entire pitch is transparency as a physical property โ you can literally see through your money. The visual language had to embody that: every surface is glass, every number is legible, and the fee page is one line long.
The aesthetic school is glassmorphism, the frosted-translucency style Apple pushed mainstream with macOS Big Sur and visionOS. Done lazily it's a blur and a white border. Done properly it's a stack: a colorful, slowly moving world behind the glass (so the blur has something to chew on), a tinted layer for contrast, a 1px light catch on the top edge, and a specular highlight that responds to you. This page tries to do it properly.
The palette is deliberately narrow: deep navy, one violet, one cyan, one magenta, a whisper of gold on the card chip. The iridescence comes from gradients between those few values โ not from adding more colors.
2 ยท The techniques
The backdrop-filter glass recipe
Real glass is not just blur. The recipe here layers four things: a solid tint (so text always has AA contrast regardless of what drifts behind), a subtle white-to-transparent gradient (the sheet's own sheen), the blur+saturate backdrop filter (saturate is the secret โ it makes the world behind the glass glow instead of going grey), and an inset 1px highlight on the top edge where light would catch.
/* the whole glass system in one class */
.glass {
background:
linear-gradient(155deg, rgba(255,255,255,.10), rgba(255,255,255,.03) 55%),
rgba(13,17,46,.55); /* โ solid tint = readable text */
backdrop-filter: blur(22px) saturate(160%);
border: 1px solid rgba(255,255,255,.14);
box-shadow:
0 24px 60px rgba(2,4,18,.5), /* depth below */
inset 0 1px 0 rgba(255,255,255,.16); /* light catch on top edge */
}
The tilt + glare math
The hero card maps pointer position to rotation: the cursor's offset from the card center, normalized to โ0.5โฆ0.5, becomes degrees of rotateX/rotateY. The raw values are never applied directly โ a requestAnimationFrame loop lerps current toward target each frame, which is what makes the card feel like it has mass. The glare is a radial gradient repositioned to the same normalized coordinates, blended with mix-blend-mode: screen so it reads as light, not paint.
// pointer โ target rotation + glare position
const px = (e.clientX - rect.left) / rect.width - .5;
const py = (e.clientY - rect.top) / rect.height - .5;
targetRX = py * -13; targetRY = px * 17;
glareX = (px + .5) * 100; glareY = (py + .5) * 100;
// each frame: chase the target (the lerp is the luxury)
current += (target - current) * 0.09;
card.style.transform =
`rotateX(${rx}deg) rotateY(${ry}deg)`;
glare.style.background =
`radial-gradient(circle at ${gx}% ${gy}%,
rgba(255,255,255,.34), transparent 60%)`;
The loop self-suspends: when every value is within 0.05 of its target it stops requesting frames, so an idle page costs nothing. On touch devices (no fine pointer) the card falls back to a slow CSS keyframe float; with prefers-reduced-motion it simply holds still. The chip, number, and logo sit at different translateZ depths inside a preserve-3d card, so the tilt has real parallax.
The gradient mesh background
The "animated mesh" is four huge circles โ up to 110vmax wide โ whose backgrounds are radial gradients fading to transparent. No filter: blur() at all: the gradient falloff is the blur, which costs the GPU nothing. Each blob drifts on its own 38โ60 second transform keyframe, alternating direction, so the composition never repeats visibly.
.blob-1 {
width: 110vmax; height: 110vmax; border-radius: 50%;
background: radial-gradient(circle,
rgba(76,56,214,.55) 0%, transparent 68%); /* pre-blurred by falloff */
animation: drift1 44s ease-in-out infinite alternate;
}
@keyframes drift1 {
to { transform: translate(9vmax, 7vmax) scale(1.12); }
}
The count-up pattern
Every statistic on the page โ the balance, the APY projection, the trust numbers โ animates from zero when it scrolls into view. One IntersectionObserver watches the reveal sections; when one enters, it finds any [data-count] descendants and runs an eased rAF counter with toLocaleString formatting. The elements use font-variant-numeric: tabular-nums so digits don't jitter sideways while counting.
<b data-count="24806.52" data-decimals="2" data-prefix="$">$0.00</b>
const p = Math.min((now - t0) / duration, 1);
const eased = 1 - Math.pow(1 - p, 3); // easeOutCubic
el.textContent = prefix +
(target * eased).toLocaleString('en-US',
{ minimumFractionDigits: d, maximumFractionDigits: d });
The savings-facet calculator reuses the same idea in reverse: sliders update a target value, and the displayed number perpetually lerps toward it โ so dragging the slider makes the projection chase your input rather than snap.
Smaller tricks worth noticing
The sparkline draws itself using pathLength="100" + stroke-dasharray, so the dash math never depends on the path's real length. The feature-card glare is pure CSS custom properties (--mx/--my) set on pointermove and consumed by a ::after radial gradient. Film grain is an inline SVG feTurbulence data URI at 5% opacity โ it stops the big gradients from banding.
3 ยท How it was made
This site was designed and written by Claude (Fable 5) โ vanilla HTML, CSS, and JavaScript, by hand, in single files. No frameworks, no build step, no component library, no stock assets: every gradient, card, chip, and icon is code.
It's one of 25 deliberately different sites in the Fable Showcase, an experiment in how far one model can push web design across wildly different aesthetics. See the rest at fable-25-dhb.pages.dev.
4 ยท Steal this
- Put a solid tint under every glass panel. The classic glassmorphism failure is white text over blur over who-knows-what. A ~55% opaque dark layer inside the background stack guarantees contrast no matter what floats behind.
- Add
saturate(160%)to your backdrop-filter. Blur alone turns the background grey and dead. Saturation makes the glass look like it's amplifying the light behind it. - Never apply pointer values directly โ lerp them.
current += (target โ current) ร 0.09is one line and it's the entire difference between "CSS hover demo" and "premium object with mass." - Fake blur with gradient falloff. A radial-gradient-to-transparent circle looks identical to a blurred blob and costs a fraction of
filter: blur(90px)on a huge element. - Use
tabular-numson anything that counts. Animated numbers with proportional digits wobble horizontally;font-variant-numeric: tabular-numsmakes them tick like an instrument panel.