Concept & inspiration
The brief was "glassmorphism + a floating 3D glass object." Rather than treat glassmorphism as a texture applied on top of a generic layout, the whole information architecture is built around it: the three service pillars and six disciplines are modeled as physical panes of glass stacked in front of a deep violet-to-blue gradient, each one catching soft colored light from blurred "glow" blobs sitting behind it. The mental image driving the layout was a tessellated window — hence the name — where every panel is a separate tessera (tile) refracting the same light source differently depending on its position and tilt.
Typography & color
Type is Manrope, self-hosted via
@fontsource-variable/manrope so the whole variable-font weight axis
(200–800) ships in a single woff2 without a Google Fonts request. Manrope's rounded,
slightly geometric letterforms read as modern and technical without being cold —
display headings sit at 700–800 weight, body copy at 400–500. There is no second
typeface: with this much visual texture already coming from blur, glow and gradient,
a second display face would compete rather than add.
The palette is a deep violet-blue gradient (#151233 →
#241b4d) with two accent hues pulled straight from the brief: cyan
#7dd3fc for interactive/informational accents (eyebrows, links, focus
rings) and violet #a78bfa for the softer decorative glow and gradient
endpoints. Glass panels use a translucent white fill
(rgba(255,255,255,0.07–0.12)) rather than a tinted fill, which keeps the
background gradient legible through every panel instead of muddying it.
The glassmorphism + parallax technique
Each glass tile is a .glass element: a soft gradient fill, a 1px
semi-transparent border, an inner highlight via inset box-shadow, and
backdrop-filter: blur(18px) saturate(140%). Behind each cluster of tiles
sits a .glow-field — two or three absolutely positioned circles with
filter: blur(70px) and a radial gradient in cyan or violet — which is
what actually reads as "colored light hitting glass" rather than a flat blur.
The tilt effect is vanilla JS, no library
(src/scripts/tilt.js). Every element with data-tilt gets a
pointermove listener that computes the cursor's position within the
element as a 0–1 fraction, maps it to a target rotation clamped to
±data-tilt-max degrees, and eases the live rotation toward that target
every frame (rx += (target - rx) * 0.08) so the motion is damped instead
of snapping. The eased values are written to CSS custom properties
(--rx, --ry) which a transform: perspective(900px)
rotateX(var(--ry)) rotateY(var(--rx)) rule consumes — the JS never touches
transform directly, so the CSS stays declarative and easy to reason
about.
The hero object composes two transforms: an outer wrapper carries the cursor tilt
(same mechanism as the tiles, smaller max angle), and an inner wrapper runs two
independent CSS @keyframes — a vertical bob and a small
±3deg sway — so it never stops moving even when the cursor
is idle. All of it is gated behind
prefers-reduced-motion: reduce: the JS listener is simply never attached,
and the CSS animations are killed with animation: none !important, which
leaves every panel at a static, neutral tilt.
A second pass added a scroll-linked complement to the cursor tilt:
src/scripts/parallax.js drifts each .glow-blob a few
pixels vertically as you scroll, alternating direction per blob so the layers read as
independent depths rather than one flat plane. It's a plain scroll
listener that only ever writes a transform, throttled to one update per
frame with requestAnimationFrame. It's skipped entirely below 641px and
under prefers-reduced-motion: reduce — the blobs are the cheapest layer
on the page (no backdrop-filter), but mobile gets zero extra animation
budget spent on it regardless. Each of the three tile grids (Pillars, Disciplines,
Work) also got a thin gradient hairline along the top edge of its cards — violet for
Pillars, cyan for Disciplines, a cyan-to-violet blend for Work — so the three grids
read as related but distinct instead of three copies of the same card.
Two more second-pass additions: every content tile now gets a hover state — a
brighter fill and a wider shadow bloom — that deliberately never touches
transform, so it can't fight with the cursor-tilt rotation already
running on the same element. And body's background gained a fourth,
barely-there layer: a tiled feTurbulence SVG data URI blended in with
background-blend-mode: overlay at roughly 3.5% strength. It's static (no
JS, no extra compositing layer, painted once with the rest of the background), and it
exists purely to break up gradient banding across the large violet-to-blue field and
sell the glass panels as sitting on a textured material rather than a flat color.
Generated imagery
Two images were generated with Higgsfield's gpt_image_2 model, both at
high quality / 2k resolution:
- Hero glass owl (1:1) — prompt: "a sculptural translucent glass
owl figurine, frosted and clear glass mixed, refracting cool cyan and violet studio
light, dramatic rim lighting, isolated on dark background, product photography
style." The raw output already had a clean near-black background, but it was
additionally run through Higgsfield's
image_background_removermodel to produce a true alpha-channel cutout (glass-owl-cutout.png), so it sits directly on the page's gradient with no visible seam. - Background texture (16:9) — prompt: "abstract macro photography
of shattered glass shards and prism facets, deep violet and cyan light refraction,
dark background, crystalline geometric texture, subtle bokeh, high contrast
dramatic lighting." This one is used at low opacity with
mix-blend-mode: screenand a radialmask-imagebehind the hero copy, purely as atmosphere — it's never meant to be looked at directly.
Both are served through Astro's built-in <Image /> component from
astro:assets, which converts them to WebP and generates
appropriately-sized output at build time (the hero owl went from a 5.3MB PNG to a
~55KB WebP).
Honest tradeoffs
1. The "3D glass object" is a flat image, not a 3D model. A real glTF/WebGL owl would let it rotate a full 360° convincingly. Because this is a single generated photograph, the animation is deliberately restrained to a small bob and a few degrees of sway/tilt — enough to sell "floating," not enough to expose that it's a flat plane. This was the pragmatic choice given the image-generation budget for this build, but it's the ceiling of what a photo-based hero object can do; a true rotation would need either a 3D render pipeline or several generated angles cross-faded together.
2. backdrop-filter is not free. Six discipline tiles
plus three pillar tiles plus the manifesto and contact panels means up to a dozen
simultaneously-blurred layers on one page. Desktop compositors handle this fine, but
on mobile GPUs repeated large-radius backdrop blurs are a known jank source. The
mitigation here is a media query at 640px that drops the blur radius from 18px to
10px and reduces the glow-blob blur from 70px to 40px with lower opacity — it doesn't
eliminate the cost, but it meaningfully reduces the compositing work on the devices
least able to afford it.