Build Guide

How BLOOM was grown.

This is one of ten different art directions built for the same Nomadic Owls brief, all sharing identical real content. This direction — BLOOM — treats the agency's work as something cultivated rather than manufactured: roots, branches and blooms standing in for a technical practice. This page is a specific, honest account of how it was actually built, for anyone who wants to reproduce or critique the approach.

Concept & inspiration

The brief's premise was that Nomadic Owls' work is "cultivated, not manufactured." Rather than illustrate that with literal plant photography, the site tries to make the agency's own service structure look and behave like a living root system: the three pillars (Full-Stack Development, Digital Marketing, Technical SEO) aren't presented as a feature-comparison grid, they're drawn as branches growing out of one root, splitting further into the six disciplines. Everything else — card shapes, buttons, decorative blobs — follows the same "grown, not drawn with a ruler" logic: irregular per-corner border-radius instead of uniform rounding, continuous slow morphing instead of static shapes, organic easing curves instead of linear or robotic ones.

The morphing shapes

Two separate techniques do the "soft blob" work, both described in src/styles/global.css and src/components/MorphBlob.astro:

  • CSS border-radius keyframes. Most blobs (hero backdrop shapes, the contact-section accent, the belief "buds", the quiet corner accent behind the work section) are plain divs animated with a @keyframes morph block that cycles three border-radius values (--grow-1, --grow-2, --grow-3 — each a different per-corner elliptical radius like 60% 40% 30% 70% / 60% 30% 70% 40%) on a 16–28s loop with a custom cubic-bezier(0.34, 0.06, 0.22, 0.98) easing curve so the transitions feel like slow breathing rather than a mechanical loop.
  • SMIL path-morphing SVG. The three decorative hero blobs use a real <animate attributeName="d"> on an SVG <path>, cycling between three hand-authored blob outlines with calcMode="spline" and explicit keySplines so the interpolation between path shapes is organic instead of linear. This is the literal "animate the SVG path d attribute" technique from the brief, used where a true liquid-morph silhouette (not just a rounded rectangle) mattered most.

Reduced motion: CSS animations are killed with a single prefers-reduced-motion: reduce media query that forces animation: none and freezes every [data-morph]/.blob element on the --grow-2 shape. SMIL doesn't respond to that CSS query at all, so the fix is a four-line inline script in MorphBlob.astro that checks matchMedia('(prefers-reduced-motion: reduce)') on load and removes the <animate> elements outright, leaving the static first path.

The branching diagram

The structural gimmick lives in src/components/BranchDiagram.astro. On screens ≥900px it renders one root node, three pillar nodes and six discipline "leaf" nodes as circles in a 1200×700 SVG viewBox, connected by hand-authored cubic-bezier branch paths (a thicker moss-green "root branch" from the root to each pillar, thinner terracotta "twigs" from each pillar to its two disciplines). The six discipline labels are grouped under pillars by theme rather than left/right order in the source JSON — Concepting and Automation under Full-Stack Development, Affiliate Marketing and Lead Generation under Digital Marketing, Search Engine Marketing and Copywriting under Technical SEO — a presentational grouping choice, not a change to any of the underlying facts.

The text labels are ordinary HTML, absolutely positioned at percentages derived from the same coordinate system as the SVG (so a label at SVG x=600 sits at left: 50%), not baked into the SVG as <text>. That keeps the copy real, selectable, and readable by screen readers in logical DOM order (root → pillar → its two disciplines, repeated) regardless of where CSS visually places it. Branch paths use the pathLength="1" trick with stroke-dasharray/dashoffset so they "grow" into view once the section scrolls into an IntersectionObserver, sidestepping the need to calculate each curve's real length.

Tradeoff: below 900px, the fanned diagram gets too short and cramped for legible labels once the SVG's fixed 1200:700 aspect ratio scales down to a 390–768px-wide container. Rather than force six overlapping text chips into ~230px of height, the component reuses the exact same label markup but switches the CSS to a vertical stacked list (SVG hidden, connecting line drawn with a single gradient ::before) via one @media (max-width: 899px) block. It's a deliberate simplification, not a bug: the mobile version keeps the same grouping and content, it just abandons the fan-out geometry that only reads well with horizontal room to breathe.

Typography & color

Display type is Lora, self-hosted via @fontsource/lora, used almost exclusively in its italic cut for headlines and pull-quotes — the brief calls for warmth, and upright Lora reads closer to a neutral serif than a warm one. Body copy is Quicksand (@fontsource/quicksand), a geometric sans with rounded terminals that echoes the "grown" soft-cornered cards without competing with Lora's italic swashes. Both are imported as static latin-*.css weight files rather than the full variable family, to keep the shipped font payload to only the weights actually used (400/500/600/700, plus italics for Lora).

The palette is the brief's four colors used almost without deviation: terracotta (#c8623b) as the primary accent (eyebrow dashes, underlines, the branch diagram's twig lines and card borders), moss green (#3f5a3e) for headings and the darkest UI surfaces (nav text, footer, the beliefs panel), soft sand (#f2e4d3) as the default card background, and cream (#faf6ee) as the page background. A handful of color-mix() and manually-picked "deep"/"soft" variants (e.g. --terracotta-deep, --moss-soft) extend the four base tokens for hover states and contrast without introducing new hues.

One deviation, made during accessibility QA: plain terracotta on cream is only 3.7:1, which reads fine for large display type but fails WCAG AA's 4.5:1 minimum for normal-size text. Filled buttons (.btn) and the terracotta contact-card both originally used plain --terracotta as a solid fill behind cream text — exactly the failure case. Both now use --terracotta-deep (#a04a2a, 5.55:1 against cream) instead, so every CTA and the contact section's body copy clears AA at their actual text size. Plain terracotta remains the accent color everywhere it's used as a line, border, or large/decorative glyph rather than a solid fill behind body text.

Generated imagery

Two images were generated with Higgsfield's gpt_image_2 model, within a 3-image budget shared across all ten site directions:

  • Hero owl illustration (src/assets/owl.png, 3:4) — prompt: "a warm terracotta and cream illustrated owl in flight, soft organic linework, botanical accents, gentle gradient, minimalist illustration style." Used as the hero's framed portrait, served through Astro's built-in astro:assets pipeline (sharp) at three responsive widths in WebP.
  • Abstract blob texture (src/assets/blob-bg.png, 16:9) — prompt: "soft abstract organic blob shapes in terracotta, moss green and sand, flat illustration, gentle gradients, no text." Used as a low-opacity, multiply-blended background layer behind the "What We Grow" section, also compressed to WebP at build time.

The optional third image (a botanical linework accent) was deliberately skipped in favor of a hand-drawn inline SVG owl glyph in the nav — a vector accent scales crisply at any size, costs nothing from the shared image budget, and matches the palette exactly without a color-matching pass.

Honest tradeoffs

  • Mobile nav is intentionally minimal. Below 640px the header collapses to the wordmark and a single "Say Hello" button rather than a hamburger/drawer menu. For a one-page site where every section is one scroll away and the nav's real job is the final CTA, a full mobile menu felt like unnecessary chrome — but it's a real simplification, not an oversight.
  • The branch diagram is hand-authored, not algorithmically generated. The brief calls it a "simple generative branching SVG diagram" — the paths here are fixed, hand-picked bezier curves rather than a runtime L-system or randomized layout. It reads as organic (asymmetric control points, uneven branch lengths) but it's static per build, not procedurally different on every load. A true generative version was judged higher-risk for guaranteeing zero overlap at three fixed breakpoints than the payoff was worth.