base wears the mark of the host that served it, not a generic glyph

base.hanzo.ai showed a rounded-square "B" — a placeholder, not Hanzo's logo —
in the tab and in the app, which renders it from icon.ts in __root and login.

The real marks already exist and are already served per brand at
/brands/<brand>/logo.svg. They are carried here rather than linked cross-origin,
and they draw in currentColor, so they take the surface's own colour instead of
shipping a second palette.

ONE map from host to brand, in index.html, resolved before the bundle loads and
left on window for the app to read. The alternative was that map plus a copy in
the bundle, and two answers to "which brand is this host" is exactly how a page
ends up titled Hanzo over somebody else's logo. Title, favicon and in-app mark
now come from the same answer and cannot disagree.

Checked from the built output across every host the fleet serves:

  base.hanzo.ai / superbase / recipes.base  Hanzo Base + hanzo mark
  lux.id, base.lux.network                  Lux Base   + lux mark
  zoolabs.id, base.zoo.ngo                  Zoo Base   + zoo mark
  localhost                                 Base       + generic glyph

An unknown host keeps the unbranded glyph, and the patterns anchor to the
registrable domain: evil-hanzo.ai.attacker.com and zoolabs.id.attacker.com are
handed no mark of ours at all.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-21 07:32:40 -07:00
co-authored by Hanzo Dev
parent 826bfbfc3f
commit 45fbd3d9cd
16 changed files with 195 additions and 23 deletions
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<style>
.bg { fill: #000; }
.mark { fill: #fff; }
@media (prefers-color-scheme: light) {
.bg { fill: #fff; }
.mark { fill: #000; }
}
</style>
<rect class="bg" width="64" height="64" rx="8"/>
<g transform="translate(8, 8) scale(0.716)">
<path class="mark" d="M22.21 67V44.6369H0V67H22.21Z"/>
<path class="mark" d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"/>
<path class="mark" d="M22.21 0H0V22.3184H22.21V0Z"/>
<path class="mark" d="M66.7198 0H44.5098V22.3184H66.7198V0Z"/>
<path class="mark" d="M66.7198 67V44.6369H44.5098V67H66.7198Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 696 B

+9
View File
@@ -0,0 +1,9 @@
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor"/>
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="currentColor" opacity="0.8"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="currentColor"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor"/>
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="currentColor" opacity="0.8"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 638 B

+12
View File
@@ -0,0 +1,12 @@
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<style>
.bg { fill: #000; }
.mark { fill: #fff; }
@media (prefers-color-scheme: light) {
.bg { fill: #fff; }
.mark { fill: #000; }
}
</style>
<rect class="bg" width="64" height="64" rx="8"/>
<path class="mark" d="M32 52 L12 18 L52 18 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 343 B

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path d="M50 85 L15 25 L85 25 Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 129 B

+12
View File
@@ -0,0 +1,12 @@
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<style>
.bg { fill: #000; }
@media (prefers-color-scheme: light) {
.bg { fill: #fff; }
}
</style>
<rect class="bg" width="64" height="64" rx="8"/>
<circle cx="32" cy="22" r="12" fill="#00A652"/>
<circle cx="21" cy="40" r="12" fill="#ED1C24"/>
<circle cx="43" cy="40" r="12" fill="#2E3192"/>
</svg>

After

Width:  |  Height:  |  Size: 389 B

+13
View File
@@ -0,0 +1,13 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="outerCircleMono">
<circle cx="508" cy="510" r="283"></circle>
</clipPath>
</defs>
<g clip-path="url(#outerCircleMono)">
<circle cx="513" cy="369" r="234" fill="none" stroke="currentColor" stroke-width="33"></circle>
<circle cx="365" cy="595" r="234" fill="none" stroke="currentColor" stroke-width="33"></circle>
<circle cx="643" cy="595" r="234" fill="none" stroke="currentColor" stroke-width="33"></circle>
<circle cx="508" cy="510" r="265" fill="none" stroke="currentColor" stroke-width="36"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 691 B

+24 -11
View File
@@ -2,26 +2,39 @@
<html lang="en" class="dark" style="background:#000;color-scheme:dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link id="favicon" rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Base</title>
<!-- The page carries the brand of the host it was served from. Base is shared
infrastructure: the same binary answers on hanzo.ai, lux.network and
zoo.ngo, so a name compiled into the page is right on one of them and
wrong on the rest. Inline and before the bundle, because the tab is
titled while React is still downloading. -->
<!-- The page carries the mark and the name of the host it was served from.
Base is shared infrastructure: the same binary answers on hanzo.ai,
lux.network and zoo.ngo, so anything compiled in is right on one of them
and wrong on the rest.
THE BRAND IS RESOLVED HERE, ONCE, and left on window for the app to
read. The alternative was this map plus a second copy in the bundle, and
two copies of "which brand is this host" is how a page ends up titled
Hanzo over somebody else's mark.
Inline and ahead of the bundle, because the tab is titled and the icon
swapped while React is still downloading. -->
<script>
(function () {
var h = location.hostname
var brand = /(^|\.)(lux\.(network|finance|id))$/.test(h) ? 'Lux'
: /(^|\.)(zoo\.(ngo|network)|zoolabs\.id)$/.test(h) ? 'Zoo'
: /(^|\.)hanzo\.(ai|id)$/.test(h) ? 'Hanzo'
var slug = /(^|\.)(lux\.(network|finance|id))$/.test(h) ? 'lux'
: /(^|\.)(zoo\.(ngo|network)|zoolabs\.id)$/.test(h) ? 'zoo'
: /(^|\.)hanzo\.(ai|id)$/.test(h) ? 'hanzo'
: ''
if (brand) document.title = brand + ' Base'
// Unknown host keeps the unbranded mark. A lookalike domain must not be
// handed our logo, so the patterns anchor to the registrable domain.
window.__brand = slug
if (!slug) return
document.title = { lux: 'Lux', zoo: 'Zoo', hanzo: 'Hanzo' }[slug] + ' Base'
var link = document.getElementById('favicon')
if (link) link.href = '/brands/' + slug + '/favicon.svg'
})()
</script>
<script type="module" crossorigin src="/assets/index-DPUOfdJ-.js"></script>
<script type="module" crossorigin src="/assets/index-BQLoHopS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-7Zn13fNS.css">
</head>
<body>
+23 -10
View File
@@ -2,23 +2,36 @@
<html lang="en" class="dark" style="background:#000;color-scheme:dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link id="favicon" rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<title>Base</title>
<!-- The page carries the brand of the host it was served from. Base is shared
infrastructure: the same binary answers on hanzo.ai, lux.network and
zoo.ngo, so a name compiled into the page is right on one of them and
wrong on the rest. Inline and before the bundle, because the tab is
titled while React is still downloading. -->
<!-- The page carries the mark and the name of the host it was served from.
Base is shared infrastructure: the same binary answers on hanzo.ai,
lux.network and zoo.ngo, so anything compiled in is right on one of them
and wrong on the rest.
THE BRAND IS RESOLVED HERE, ONCE, and left on window for the app to
read. The alternative was this map plus a second copy in the bundle, and
two copies of "which brand is this host" is how a page ends up titled
Hanzo over somebody else's mark.
Inline and ahead of the bundle, because the tab is titled and the icon
swapped while React is still downloading. -->
<script>
(function () {
var h = location.hostname
var brand = /(^|\.)(lux\.(network|finance|id))$/.test(h) ? 'Lux'
: /(^|\.)(zoo\.(ngo|network)|zoolabs\.id)$/.test(h) ? 'Zoo'
: /(^|\.)hanzo\.(ai|id)$/.test(h) ? 'Hanzo'
var slug = /(^|\.)(lux\.(network|finance|id))$/.test(h) ? 'lux'
: /(^|\.)(zoo\.(ngo|network)|zoolabs\.id)$/.test(h) ? 'zoo'
: /(^|\.)hanzo\.(ai|id)$/.test(h) ? 'hanzo'
: ''
if (brand) document.title = brand + ' Base'
// Unknown host keeps the unbranded mark. A lookalike domain must not be
// handed our logo, so the patterns anchor to the registrable domain.
window.__brand = slug
if (!slug) return
document.title = { lux: 'Lux', zoo: 'Zoo', hanzo: 'Hanzo' }[slug] + ' Base'
var link = document.getElementById('favicon')
if (link) link.href = '/brands/' + slug + '/favicon.svg'
})()
</script>
</head>
+18
View File
@@ -0,0 +1,18 @@
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<style>
.bg { fill: #000; }
.mark { fill: #fff; }
@media (prefers-color-scheme: light) {
.bg { fill: #fff; }
.mark { fill: #000; }
}
</style>
<rect class="bg" width="64" height="64" rx="8"/>
<g transform="translate(8, 8) scale(0.716)">
<path class="mark" d="M22.21 67V44.6369H0V67H22.21Z"/>
<path class="mark" d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"/>
<path class="mark" d="M22.21 0H0V22.3184H22.21V0Z"/>
<path class="mark" d="M66.7198 0H44.5098V22.3184H66.7198V0Z"/>
<path class="mark" d="M66.7198 67V44.6369H44.5098V67H66.7198Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 696 B

+9
View File
@@ -0,0 +1,9 @@
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor"/>
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="currentColor" opacity="0.8"/>
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="currentColor"/>
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor"/>
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor"/>
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="currentColor" opacity="0.8"/>
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 638 B

+12
View File
@@ -0,0 +1,12 @@
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<style>
.bg { fill: #000; }
.mark { fill: #fff; }
@media (prefers-color-scheme: light) {
.bg { fill: #fff; }
.mark { fill: #000; }
}
</style>
<rect class="bg" width="64" height="64" rx="8"/>
<path class="mark" d="M32 52 L12 18 L52 18 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 343 B

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path d="M50 85 L15 25 L85 25 Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 129 B

+12
View File
@@ -0,0 +1,12 @@
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<style>
.bg { fill: #000; }
@media (prefers-color-scheme: light) {
.bg { fill: #fff; }
}
</style>
<rect class="bg" width="64" height="64" rx="8"/>
<circle cx="32" cy="22" r="12" fill="#00A652"/>
<circle cx="21" cy="40" r="12" fill="#ED1C24"/>
<circle cx="43" cy="40" r="12" fill="#2E3192"/>
</svg>

After

Width:  |  Height:  |  Size: 389 B

+13
View File
@@ -0,0 +1,13 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="outerCircleMono">
<circle cx="508" cy="510" r="283"></circle>
</clipPath>
</defs>
<g clip-path="url(#outerCircleMono)">
<circle cx="513" cy="369" r="234" fill="none" stroke="currentColor" stroke-width="33"></circle>
<circle cx="365" cy="595" r="234" fill="none" stroke="currentColor" stroke-width="33"></circle>
<circle cx="643" cy="595" r="234" fill="none" stroke="currentColor" stroke-width="33"></circle>
<circle cx="508" cy="510" r="265" fill="none" stroke="currentColor" stroke-width="36"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 691 B

+13 -1
View File
@@ -6,4 +6,16 @@
// Vite rewrites the references it can see — the ones in index.html and anything
// imported — but a path written as a string in JSX is opaque to it and ships
// verbatim. So the base belongs in one value, here, rather than at each usage.
export const icon = `${import.meta.env.BASE_URL}icon.svg`
// The mark is the SERVING HOST'S, not a compiled-in one, for the same reason the
// title is: one binary answers for Hanzo, Lux and Zoo. index.html resolves which
// brand this host is before the bundle loads and leaves the answer on
// window.__brand — read here rather than re-derived, so there is exactly one map
// from host to brand and no way for the name and the logo to disagree.
//
// An unknown host falls back to the unbranded mark. That is deliberate: lending
// our logo to a lookalike domain is worse than showing a generic glyph.
const slug = typeof window === 'undefined' ? '' : ((window as { __brand?: string }).__brand ?? '')
export const icon = slug
? `${import.meta.env.BASE_URL}brands/${slug}/logo.svg`
: `${import.meta.env.BASE_URL}icon.svg`