Skip to content

Logo refinement — V12 (drawn-oblique wordmark)

Supersedes Logo refinement — V11 for the wordmark treatment and the reveal/Journey motion timing. V11's mark geometry (the ring, taper, and dot) is unchanged and still governed by Logo Rationale — this pass only touches the wordmark and the two animated sequences.

Issue: OBJ-2225 · Shipped: PR #2139 (wordmark + assets, 75b0bf57d) and PR #2140 (motion, 32c31d61d), both merged to master 2026-08-07. UI-SPECs: logo-v12-oblique-wordmark.md, logo-v12-motion.md — the full design contracts this record summarizes.

Why

Outfit ships no italic cut. font-style: italic on the wordmark was renderer-synthesised — the slant angle and stroke weight differed per engine, and the exported SVG lockups rendered fully upright anywhere italics weren't faked. The component and its own exported logo files disagreed about what the logo looked like.

The oblique

The wordmark is sheared with an explicit skewX(-8deg) instead of a synthesised italic, pivoted on a defined line rather than the glyph baseline:

css
.logo-wordmark {
  font-family: 'Outfit', sans-serif;
  font-weight: 800;
  transform: skewX(-8deg);
  transform-origin: 0 0.6em;
  display: inline-block;
  font-kerning: normal;
  font-size: 16cqi;
  letter-spacing: clamp(-0.03em, calc(-0.0426em + 0.71px), -0.015em);
  line-height: 1;
  padding-right: 1.5cqi;
}

(ionic_frontend/src/components/ui/ObjectuveLogo.vue:805-820)

The pivot is 0.6em, not the 38%-of-height figure the original design pass proposed. With line-height: 1, Outfit's baseline sits 0.87em below the box top; the optical centre of the glyph's visible span (ascender +0.732em, descender −0.222em) is 0.27em above that baseline — 0.87 − 0.27 = 0.6em. A percentage origin also resolves differently in light and dark mode, because .logo-wordmark-light carries an extra padding-bottom: 0.15em that dark mode doesn't — so 0 38% would have sheared light and dark mode by different amounts. 0.6em is stable in both. This correction was Desi's, caught by measuring the patch rather than transcribing it — see UI-SPEC §2.1 ("the pivot must be 0.6em, not 0 38%").

The mark→word gap is derived, not eyeballed. The mark's 100-unit viewBox carries 8 units of dead space to the right of the ring's outer edge; margin-right: -1.4cqi (8% of the mark's 17.5cqi width) pulls exactly that much, so the wordmark's pivot-line origin lands on the ring's outer edge — .logo-mark-integrated { margin-right: -1.4cqi; } (ObjectuveLogo.vue:803). The same rule, x = 92 in a 100-unit viewBox, is what the exported SVGs use to place their own wordmark. One rule, two surfaces.

Tracking crosses over between −0.015em and −0.03em, anchored in px. The original patch's clamp used cqi for its middle term on an element whose font-size is itself set in cqi1em = 16cqi on .logo-wordmark, so the clamp collapsed to a flat constant that never actually varied. The shipped formula, clamp(-0.03em, calc(-0.0426em + 0.71px), -0.015em), computes:

SurfaceWordmark sizeComputed tracking
App header25.6px−0.0150em (upper bound)
Sign-in48.0px−0.0278em
Marketing hero64.0px−0.0300em (lower bound)

.text-reveal's keyframe carries skewX(-8deg) at both stops so the wordmark doesn't snap upright mid-animation (ObjectuveLogo.vue:865-869).

The horizontal and stacked lockups

Same two rules — an 0.27em pivot and a gap measured from that pivot line — applied to the two other lockups, normalized so all three assets pivot at the same height (they previously varied from 0.273em to 0.353em):

  • Horizontal: wordmark origin sits one ring stroke (14 mark-units) past the ring's outer edge; baseline corrected from 63 → 63.7 so the cap-height centre lands on the mark's centre.
  • Stacked: wordmark centre sits on the mark's centre; vertical gap is 1.5 ring strokes; viewBox cropped to the descenders.

Full derivations are in UI-SPEC §3.3, "Pivot normalisation across the family".

Font embedding — outline to paths, not @import

Decision every future asset export needs to know: the wordmark in each exported lockup SVG is outlined to <path> data, not left as live text with a webfont reference.

The prior assets carried <style>@import url('…Outfit:ital,wght@1,800')</style> in <defs> — and that import requested the italic axis, the exact thing this pass removes, so it couldn't be pasted back unchanged. Re-adding a non-italic @import would only have matched the status quo's own reliability: @import inside an SVG referenced via <img src> is blocked by most browsers anyway, so it only ever worked in inline/<object> contexts. Outlining is what actually delivers "renders identically in tools that don't fake italics" — a standalone SVG no longer depends on Outfit being installed or fetchable, which is the print / App Store / partner-handoff case this whole pass exists for.

Every shipped lockup SVG (ionic_frontend/src/assets/logo/lockup-*.svg) now carries no <text>, font-family, @import, or @font-face — verified via grep -lE '<text|font-family|@import|@font-face' ionic_frontend/src/assets/logo/lockup-*.svg, which returns nothing. The skew is baked into the path data itself, not left as a skewX(-8) wrapper transform.

Trade-off, recorded on purpose: tracking and slant can no longer be re-tuned by editing a text attribute — any future change to the wordmark's angle or spacing means re-outlining, not a one-line SVG edit. That's the intended property, not a regression; it's what makes the exported files immune to the per-engine italic-synthesis problem this whole issue was about.

Dark lockup: two assets, not one

lockup-integrated-dark.svg keeps the gold taper and dot (#fcc419) as the default dark treatment. A new sibling, lockup-integrated-dark-mono.svg, renders the same lockup entirely in white — an opt-in for surfaces that want the quieter treatment. In the component, this is the accent prop:

ts
accent?: 'gold' | 'mono';   // default 'gold'; 'mono' only applies dark + variant="integrated"

accent="gold" (the default) is what ships on SignIn.vue and SignUp.vue — gold taper and dot on the dark auth screens, confirmed directly by Josh after an initial draft of the UI-SPEC had mapped the mono (all-white) treatment to those two screens instead. Under accent="mono", the dot still pops gold on reveal and cross-fades to white over 200ms as the wordmark settles — mono is a resting treatment, not something that should cost the reveal its warmest beat.

Motion tightening

Both sequences are tightened for pacing, not redrawn — geometry, HSL color blending, bloom, and wave radii are unchanged in the Journey sequence. Every total below is measured against fake timers in the shipped Vitest suite, not read off a source constant.

Reveal (ObjectuveLogo.vue)

BeforeAfter (measured)
reveal2.08s (2.26s to animationComplete)1502ms
reveal-breath (sign-in)~2.60s1502ms — same total as reveal

The dot now fires at a fixed 792ms — 88% of the 900ms sweep — while the ring is still closing its final ~12°, instead of waiting for the sweep to finish. (The original patch's DOT_DELAY = -108 as a literal setTimeout delay would have clamped to 0 and fired the dot at the moment the sweep ended, not overlapping it — the scheduling had to be restructured to fire from an elapsed-time check inside the animation frame loop, not from a setTimeout at loop end.) easeOutQuint replaces easeOutCubic on the ring's head so 900ms reads as decisive. The wordmark's reveal travel shortened from 10px to 6px so it slides in under the settling dot rather than after it.

Sign-in's reveal-breath variant used to insert a 520ms pause between the ring closing and the dot popping. That pause is gone — the same 300ms swell now runs underneath the dot and wordmark instead of before them, so sign-in lands on the identical 1502ms total as every other reveal instance.

Journey (ObjectuveLogoJourney.vue)

BeforeAfter (measured)
Total6.48s3.94s (+400ms autoplay delay in its sole consumer, WelcomeIntro.vue → 4.34s from mount)

ObjectuveLogoJourney's only production consumer is WelcomeIntro.vue (desktop-only, first screen of onboarding) — not LegendaryCelebration.vue, which was the assumption in the original issue. The ignition (the dot's collision with the closing ring) moves to the front of the sequence: easeInOutCubic put it at the exact midpoint of the rebuild, the slowest, least eventful frame; easeOutQuart lands it at ~159ms, so the strike is the entrance and everything after is aftermath. Ray count dropped from 16 to 12 dashed spokes so they read as individual strokes rather than a stock sunburst; particle life shortened from 700ms to 520ms so the motes clear before the burst waves do.

Reduced-motion still path

ObjectuveLogoJourney.vue had no prefers-reduced-motion path before this pass — it always played the full sweep, spring scale, burst, and particle splash. It now imports useReducedMotion and routes to playStill(): the completed gold ring fades in over 160ms, holds for 900ms, then cross-fades to rest over 200ms — 1.26s total, opacity only. No sweep, scale, burst, particles, or wave rings. The equivalent guard was added to ObjectuveLogo.vue's three terminating reveal modes (reveal, reveal-breath, origin).

Both components' completion events (animationComplete, complete) are guaranteed to fire exactly once per cycle on every path, reduced-motion included — this is the load-bearing invariant, since SignIn.vue gates the entire sign-in form's visibility on animationComplete.

Deferred — not in this pass

Two follow-ups were identified during this work and deliberately not shipped here; both need a product/brand decision before they're picked up:

  • An upright ("1c", no-slant) wordmark variant for contexts where a skew is unwanted — print, App Store listings, partner handoff. Explored and rejected as the default treatment during this pass, but the outline-conversion decision above makes it cheap to produce as a sibling asset later.
  • Three social-media SVGs still carry bespoke italic wordmarks, all under marketing_landing/images/social/: linkedin-banner.svg, x-header.svg, facebook-cover.svg. No v12 asset was supplied for these — they weren't in scope for this pass and are out of step with everything else that ships the oblique treatment.

Also recorded, not fixed here (needs Josh's sign-off before touching, since either one re-scales every exported PNG, the marketing site, the docs site, and the OG image):

  • The exported integrated lockup draws the wordmark ~17% smaller than the component's own ratio would put it — same mark, two different proportions.
  • ~44% of every exported integrated lockup's viewBox is empty space to the right of the wordmark.

Verification

Every figure above was measured against the merged source, not read off a design-doc estimate:

  • ObjectuveLogo.vue:380-387REVEAL_DURATION = 900, DOT_DELAY = -108, DOT_POP_DURATION = 340, TEXT_DELAY = DOT_DELAY + 90, REVEAL_TOTAL = 1502.
  • ObjectuveLogo.vue:795-820.logo-mark-integrated/.logo-wordmark CSS quoted above.
  • ObjectuveLogo.vue:856-869.text-reveal keyframe.
  • ObjectuveLogoJourney.vue:270-274P1_DURATION = 1000, P2_DURATION = 900, P3_DURATION = 420, P3_PAUSE = 120, P4_DURATION = 1100.
  • ObjectuveLogoJourney.vue:329RAY_ANGLES at 12 entries.
  • ObjectuveLogoJourney.vue:512PARTICLE_LIFE = 520.
  • ObjectuveLogoJourney.vue:543,572playStill(), gated on reducedMotion.value.
  • ionic_frontend/src/assets/logo/lockup-*.svg — grep-verified, no <text>/font-family/@import/ @font-face.
  • ionic_frontend/src/views/SignIn.vue:23, SignUp.vue:23 — no accent prop passed, so both render the component's 'gold' default.

See also


Last updated: 2026-08-07 · Version: v4.4.5

Loading…