fix(app): flame mark is the real menubar flame

Replaces the rejected hand-traced silhouette with Bootstrap Icons'
fire path (MIT, embedded with license note) — the flame.fill-style
teardrop with the inner tongue — filled with the menubar BurnFlame's
actual Ember gradient from the Swift theme (#f0a070 > #e8774a >
#c9521d > #8b3e13, bottom to top), not the brighter accent ramp.
Flicker hook moves to the whole mark (single compound path).
Also: .splash-word snapped to var(--fs-kpi).
This commit is contained in:
iamtoruk 2026-07-16 05:11:10 -07:00
parent 76bc80ca67
commit 0f836db72f
2 changed files with 32 additions and 32 deletions

View file

@ -1,46 +1,47 @@
import { useId, useMemo } from 'react'
import { motionClass, motionEnabled } from '../lib/motion'
import { motionEnabled } from '../lib/motion'
// Flame silhouette: Bootstrap Icons `fire` (MIT License, © The Bootstrap Authors,
// https://github.com/twbs/icons/blob/main/icons/fire.svg). A filled teardrop
// flame whose tip curls right, with a small inner negative-space tongue at the
// bottom center -- the closest MIT-licensed match to the menubar's SF Symbol
// `flame.fill`. Path embedded verbatim; only the fill is swapped for the brand
// gradient.
const FLAME_PATH =
'M8 16c3.314 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .25 1.5-1.25 2-1.25 2C11 4 9 .5 6 0c.357 2 .5 4-2 6-1.25 1-2 2.729-2 4.5C2 14 4.686 16 8 16m0-1c-1.657 0-3-1-3-2.75 0-.75.25-2 1.25-3C6.125 10 7 10.5 7 10.5c-.375-1.25.5-3.25 2-3.5-.179 1-.25 2 1 3 .625.5 1 1.364 1 2.25C11 14 9.657 15 8 15'
/**
* Hand-authored brand flame: an orange body tongue over a lighter inner tongue,
* traced to match the menubar mark. Shared between the launch splash (large) and
* the sidebar (small). `live` gives the inner tongue an extremely subtle idle
* flicker in the sidebar; its phase is randomized once per mount so a row of
* flames never metronomes. All motion is gated by motionEnabled().
* Brand flame mark, mirroring the menubar's `BurnFlame`. The vertical gradient
* reproduces that view's Ember-preset stops (glow -> light -> base -> deep, from
* base to tip). Shared between the launch splash (large) and the sidebar (small).
* `live` gives the whole mark an all-but-imperceptible idle flicker in the
* sidebar; its phase is randomized once per mount so a row of flames never
* metronomes. All motion is gated by motionEnabled().
*/
export function FlameMark({ size = 20, live = false }: { size?: number; live?: boolean }) {
const uid = useId()
const body = `fm-body-${uid}`
const core = `fm-core-${uid}`
const grad = `fm-grad-${uid}`
// Random negative delay so the loop starts mid-cycle at a different point each
// mount. Computed once; only takes effect when the flicker class is present.
const flickerStyle = useMemo(() => ({ animationDelay: `-${(Math.random() * 4 + 1).toFixed(2)}s` }), [])
const coreClass = live ? motionClass('fm-core', 'fm-flicker') : 'fm-core'
const flicker = live && motionEnabled()
return (
<svg className="flamemark" width={size} height={size} viewBox="0 0 32 40" fill="none" aria-hidden="true">
<svg className="flamemark" width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true">
<defs>
<linearGradient id={body} x1="16" y1="2" x2="16" y2="39" gradientUnits="userSpaceOnUse">
<stop offset="0" stopColor="#f2701c" />
<stop offset=".5" stopColor="#e8590c" />
<stop offset="1" stopColor="#c2410c" />
</linearGradient>
<linearGradient id={core} x1="16" y1="16" x2="16" y2="39" gradientUnits="userSpaceOnUse">
<stop offset="0" stopColor="#ffd9a8" />
<stop offset=".55" stopColor="#ffb877" />
<stop offset="1" stopColor="#f79a56" />
<linearGradient id={grad} x1="0" y1="1" x2="0" y2="0">
<stop offset="0" stopColor="#f0a070" />
<stop offset=".33" stopColor="#e8774a" />
<stop offset=".67" stopColor="#c9521d" />
<stop offset="1" stopColor="#8b3e13" />
</linearGradient>
</defs>
<path
fill={`url(#${body})`}
d="M17.4 2.8C19.6 7.2 17.2 9.9 15.2 12.7C12.6 16.3 9.7 19.1 9.2 23.4C8.9 25.7 9.6 27.8 11 29.6C9.4 28.9 8.2 27.4 7.7 25.6C6.1 27.2 5.2 29.4 5.4 31.7C5.7 35.9 10.3 39 15.9 39C22 39 26.7 35.4 26.7 29.6C26.7 25.6 24.6 22.4 23.2 20.6C23.9 22.4 23.7 24.3 22.6 25.4C24 21.7 22.4 17.7 19.6 15.3C21.6 12.9 22.5 9.9 21.6 7.1C20.8 9.3 19.4 10.6 18.8 10.9C20 8.2 19.8 5 17.4 2.8Z"
/>
<path
className={coreClass}
style={live && motionEnabled() ? flickerStyle : undefined}
fill={`url(#${core})`}
d="M16.6 17.8C17.8 20.4 16 22.2 15.6 24.6C15.3 26.6 16.4 28.3 18.4 28.7C20.1 29 21.7 28 22 26.3C22.3 24.6 21.4 23.3 20.8 22.6C21.1 24.1 20.3 25 19.4 25.2C20.5 22.5 18.9 20.4 17.6 19.6C18.3 18.5 18 17.2 16.6 17.8Z"
className={flicker ? 'fm-flicker' : undefined}
style={flicker ? flickerStyle : undefined}
fill={`url(#${grad})`}
d={FLAME_PATH}
/>
</svg>
)

View file

@ -832,11 +832,10 @@ td:first-child { font-size: var(--fs-body); font-weight: var(--fw-body); }
added only through motionClass()/motionEnabled(); the media query is a
belt-and-suspenders escape hatch. */
/* Sidebar living flame: an all-but-imperceptible idle flicker on the inner
tongue, pivoting from its base, several-second period, phase randomized
/* Sidebar living flame: an all-but-imperceptible idle flicker on the whole
mark, pivoting from its base, several-second period, phase randomized
per mount via an inline animation-delay. */
.fm-core { transform-box: fill-box; transform-origin: 50% 90%; }
.fm-flicker { animation: fm-flicker 4.5s ease-in-out infinite; }
.fm-flicker { transform-box: fill-box; transform-origin: 50% 90%; animation: fm-flicker 4.5s ease-in-out infinite; }
@keyframes fm-flicker {
0%, 100% { transform: scale(1) skewX(0deg); }
28% { transform: scale(1.015, 0.99) skewX(0.6deg); }
@ -861,7 +860,7 @@ td:first-child { font-size: var(--fs-body); font-weight: var(--fw-body); }
opacity: 0.5;
}
.splash-word {
font-size: 17px; font-weight: var(--fw-strong); letter-spacing: -0.01em; color: var(--ink);
font-size: var(--fs-kpi); font-weight: var(--fw-strong); letter-spacing: -0.01em; color: var(--ink);
}
.splash-lit .splash-mark {