PageHare brand assets and motion
Status: implemented. Binding brand rules live in LOGO.md;
this guide covers use and regeneration. The design system
governs ordinary UI motion. The /styleguide brand section is the runnable
reference, using the same components as the product.
Sources and outputs
The source currentColor marks live in
packages/app/src/lib/brand/. BrandMark.svelte
includes those shapes directly. When a source SVG changes, update the matching
branch in that component. BrandLockup.svelte switches between the full wordmark
and the bare mark.
The wordmark is outlined SVG, so its lettering needs no font request.
| Surface | Asset choice | Output |
|---|---|---|
| Small browser tab | Bare B | static/favicon.svg, favicon-16.png |
| Larger raster favicon | Refined framed A | static/favicon-32.png, favicon-48.png |
| Legacy browser favicon | Matching 16/32/48px entries | static/favicon.ico |
| Installed app | Framed A on the existing green | static/app-icon.svg, pwa-192.png, pwa-512.png |
| Maskable installed app | Framed A inside the mask-safe area | static/pwa-maskable-512.png |
| Apple home screen | Framed A | static/apple-touch-icon.png |
| Social avatar | Bare B | brand/exports/avatar-*.png |
| Social banner | PageHare lockup and product promise, clear of avatar crops | brand/exports/banner-*.png |
| Link preview | Recognizable PageHare lockup | static/og-image.png |
| Email lockup | Refined lockup at existing email sizes | packages/app/src/lib/email/assets/ |
static/ in this table means packages/app/static/. The manifest uses the
dedicated app-icon SVG, so the small-tab SVG can stay bare B. Tenant favicon and
Open Graph rules remain in server/theme.ts; app-brand assets are not substituted
for a tenant's identity.
Use the motion components
BrandMotion.svelte accepts variant (dive, ears, missed, or success)
and size (inline pixels). Motion is always decorative and hidden from assistive
technology: nearby status text explains the state. Use PageholeLoader for an
announced loading status and BrandMark with title for a named static mark.
<script lang="ts">
import BrandMotion from '$lib/brand/BrandMotion.svelte';
import PageholeLoader from '$lib/components/PageholeLoader.svelte';
import { useDelayedLoading } from '$lib/components/useDelayedLoading.svelte.js';
let { loading }: { loading: boolean } = $props();
const load = useDelayedLoading(() => loading);
</script>
{#if load.visible}
<PageholeLoader variant="ears" size={32} label="Loading more pages" />
{/if}
<BrandMotion variant="missed" size={96} />
<h2>No pages yet</h2>Use PageholeLoader for loading: it owns the status semantics and accessible
label, while useDelayedLoading gates visibility. The two loading variants share
the same cycle, so switching to curious ears preserves the existing delay and
minimum-display rules. Timers control visibility only; CSS animates the SVG.
The product uses:
- Quiet dive:
dashboard, editor, and preview splash loading.
- Curious ears:
loading more pages in Discover.
- Wrong burrow:
404 and first-run empty states in Discover and the dashboard. It plays once and rests as E; filtered empty lists remain ordinary text.
- A small hooray:
the confirmation toast after publishing succeeds in the editor or preview. It plays once and rests as B. Other toasts retain their normal status treatment.
Non-404 errors use static B. Reduced-motion users see B immediately for loading or success and E for missed; ripples and accent strokes stay hidden. Static fallbacks are CSS-driven and work without JavaScript. Keep status text and controls usable independently of the illustration.
Regenerate the asset family
Run from the repository root after installing workspace dependencies. Static
generation uses the existing Playwright dependency and a Chromium browser; motion
exports also require ffmpeg on PATH. Use Playwright's installed Chromium, or
set CHROMIUM_PATH to an existing browser executable.
npm run brand:assets
npm run brand:assets:check
npm run brand:motionFor an environment with system Chromium:
CHROMIUM_PATH=/usr/bin/chromium npm run brand:assets
CHROMIUM_PATH=/usr/bin/chromium npm run brand:assets:check
CHROMIUM_PATH=/usr/bin/chromium npm run brand:motionscripts/generate-brand-assets.mjs derives the static images from the canonical
SVGs, the existing palette in tokens.css, and the bundled Literata font for
banner copy. The check rerenders the outputs and compares bytes, and also checks
that inline BrandMark shapes match their SVG sources.
scripts/generate-brand-motion.mjs compiles the actual BrandMotion component,
renders it in Chromium, and exports the animation frames. Outputs in
brand/exports/motion/ use these stems:
-
dive-loop -
ears-loop -
missed-once -
success-once
Each exports GIF, WebP, MP4, and WebM, with a contact sheet for frame review. Missed and success GIF/WebP exports play once. The product renders inline SVG and CSS; it does not load these media exports.
Review a change
-
Open
/styleguide, inspect all four states at 24, 48, and 96px, and use the replay control for the two one-shot gestures. Check each theme in light/dark. -
Enable reduced motion: every example should immediately show the correct resting mark with no moving ears, ripple, or accent strokes.
-
Inspect favicons at actual 16, 24, and 32px sizes, app icons with masking, banner crops, and light/dark browser-tab backgrounds. Follow the cold-read ritual in
LOGO.mdbefore shipping an edited mark. -
Run
npm run brand:assets:check,npm run check, andnpm run lint:tokens -w app; check loading, 404, and publishing behavior with the corresponding UI tests. Reader changes must preserve the zero-JS gate.
Historical brand inputs in docs/design/brand-history.md, docs/design/reference/,
and brand/retired/ remain historical references; regeneration does not rewrite
them.