The Loyaly mark everywhere a person sees the product

Brand assets in brand/ (the 512px mark, sizes for each surface, a
multi-size .ico). Windows executables carry it as a compiled-in
resource (rsrc_windows_amd64.syso from go-winres) so Explorer, the
taskbar and the installer show it; installer/build.ps1 therefore uses a
plain go build rather than wails build, which would add a second copy
and fail the link. The tray icon is the mark with a state dot over its
corner - a plain coloured circle read as a generic status light among
other icons - rendered from the embedded PNG at 32px so it survives
150% scaling. The desktop app's login, setup and sidebar marks, the
head-office web app's mark and favicon, and the engine dashboard's
favicon are the same file.

Also found while packaging: no wheel so far shipped static/, so the
engine's own dashboard at :8010 on a Windows source install would have
failed with a missing file. package-data now includes it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-19 12:54:19 +05:30
parent effa4f3d62
commit 3a3930aa27
47 changed files with 302 additions and 64 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -155,6 +155,15 @@ def create_app(engine: Engine) -> FastAPI:
def dashboard() -> str:
return (_STATIC / "dashboard.html").read_text(encoding="utf-8")
@app.get("/static/favicon.png")
def favicon() -> Response:
# The one static asset besides the page itself. Served explicitly
# rather than mounting the directory: nothing else in there is meant
# to be reachable, and a mount would make that a matter of what lands
# in the folder.
return Response((_STATIC / "favicon.png").read_bytes(), media_type="image/png",
headers={"cache-control": "public, max-age=86400"})
@app.get("/api/health")
def health() -> dict:
from .paths import describe

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Behavision</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; margin: 0; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
brand/loyaly-icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
brand/loyaly-icon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

BIN
brand/loyaly-icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
brand/loyaly-icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
brand/loyaly-icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
brand/loyaly-icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
brand/loyaly-icon-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
brand/loyaly-mark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
brand/loyaly.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
desktop/build/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -0,0 +1,68 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.OutputFilename}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{safeBundleID .Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,63 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.OutputFilename}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{safeBundleID .Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -0,0 +1,15 @@
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Behavision</title>
<script type="module" crossorigin src="./assets/index-Dxm9O68-.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Bml5ogS2.css">
<script type="module" crossorigin src="./assets/index-MXXBc3qm.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-W0yLGths.css">
</head>
<body>
<div id="root"></div>

View File

@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'
import { api, isDesktop, message } from './bridge.js'
import { usePolled } from './hooks.js'
import * as Icon from './ui/icons.jsx'
import logo from './assets/loyaly-mark.png'
import Login from './views/Login.jsx'
import Setup from './views/Setup.jsx'
import Live from './views/Live.jsx'
@@ -49,7 +50,7 @@ export default function App() {
// error nobody will read.
return (
<div className="login"><div className="box">
<span className="mark"><Icon.Shield size={20} /></span>
<span className="mark"><img src={logo} alt="" /></span>
<h1>Behavision</h1>
<p className="lead">
This is the Behavision window running outside the app, so it has no
@@ -80,7 +81,7 @@ export default function App() {
<div className="shell">
<aside className="side">
<div className="brand">
<span className="mark"><Icon.Shield size={17} /></span>
<span className="mark"><img src={logo} alt="" /></span>
<div className="id">
<h1>Behavision</h1>
<p>{session.site_name || session.user?.client_name || 'This shop'}</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -105,11 +105,8 @@ input, textarea { user-select: text; }
display: flex; flex-direction: column; min-height: 0;
}
.side .brand { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-5) var(--sp-5) var(--sp-4); }
.side .brand .mark {
width: 30px; height: 30px; border-radius: 9px; flex: none; display: grid; place-items: center;
background: linear-gradient(160deg, var(--accent-2), var(--s2));
border: 1px solid #1B4C5A; color: var(--accent);
}
.side .brand .mark { width: 30px; height: 30px; flex: none; display: grid; place-items: center; }
.side .brand .mark img, .login .mark img { width: 100%; height: 100%; object-fit: contain; display: block; }
.side .brand .id { min-width: 0; }
.side .brand h1 { font-family: var(--font-display); font-size: 15px; font-weight: 600; letter-spacing: -.012em; line-height: 1.2; }
.side .brand p { font-size: 11.5px; color: var(--ink-3); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -411,11 +408,7 @@ tbody tr[role="button"]:hover, tbody tr.clickable:hover { background: var(--s2);
width: 100%; max-width: 396px; background: var(--s1); border: 1px solid var(--line);
border-radius: var(--r-lg); padding: var(--sp-7); box-shadow: var(--shadow-lg);
}
.login .mark {
width: 38px; height: 38px; border-radius: 11px; margin-bottom: var(--sp-4);
display: grid; place-items: center; color: var(--accent);
background: linear-gradient(160deg, var(--accent-2), var(--s2)); border: 1px solid #1B4C5A;
}
.login .mark { width: 44px; height: 44px; margin-bottom: var(--sp-4); display: grid; place-items: center; }
.login h1 { font-family: var(--font-display); font-size: 21px; font-weight: 600; letter-spacing: -.022em; }
.login .lead { margin: 6px 0 var(--sp-5); }
.login .btn { width: 100%; justify-content: center; margin-top: var(--sp-1); }

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
import { api, message } from '../bridge.js'
import * as Icon from '../ui/icons.jsx'
import logo from '../assets/loyaly-mark.png'
// The gate. Nothing else in the app is reachable until this succeeds, because
// the broker credentials and the customer database both live behind it.
@@ -25,7 +26,7 @@ export default function Login({ onDone }) {
return (
<div className="login">
<div className="box">
<span className="mark"><Icon.Shield size={20} /></span>
<span className="mark"><img src={logo} alt="" /></span>
<h1>Behavision</h1>
<p className="lead">Sign in to connect this PC to your store.</p>
<form onSubmit={submit}>

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
import { api, message } from '../bridge.js'
import * as Icon from '../ui/icons.jsx'
import logo from '../assets/loyaly-mark.png'
// Linking this PC to a shop — the first thing that happens on a new install,
// and until now the one thing the app could not do.
@@ -42,7 +43,7 @@ export default function Setup({ onDone, onCancel }) {
return (
<div className="login">
<div className="box">
<span className="mark"><Icon.Link size={20} /></span>
<span className="mark"><img src={logo} alt="" /></span>
<h1>{onCancel ? 'Link to head office' : 'Set up this PC'}</h1>
<p className="lead">
Type the installation code for this shop. You only do this once.

View File

@@ -2,28 +2,32 @@ package main
import (
"bytes"
_ "embed"
"encoding/binary"
"image"
"image/color"
"image/png"
"runtime"
"sync"
agentpaths "github.com/loyaly/behavision-agent/pkg/paths"
)
// iconFor renders the tray icon at run time rather than embedding four PNGs.
// iconFor renders the tray icon at run time: the Loyaly mark with a state
// dot in the corner. Green, amber, red or grey is the only thing a taskbar
// conveys at this size, and the mark is what makes it OURS among a row of
// other icons - a plain coloured circle read as a generic status light.
//
// A 16x16 filled circle is all the taskbar shows at this size, and generating
// it means the four states cannot drift apart visually or have one file go
// missing from a build.
// The mark is embedded once at 128px and scaled down here, so the four
// states cannot drift apart and no file can go missing from a build.
//
// The encoding is per-platform and is NOT cosmetic. systray writes these bytes
// to a temp file and, on Windows, hands the path to LoadImageW with
// IMAGE_ICON|LR_LOADFROMFILE — which decodes .ico and nothing else. A PNG
// IMAGE_ICON|LR_LOADFROMFILE - which decodes .ico and nothing else. A PNG
// there returns 0, systray logs "unable to set icon", and the product ships
// with no tray icon at all: the one control surface a shop manager has.
func iconFor(state string) []byte {
img := circle(colorFor(state))
img := trayImage(colorFor(state))
if runtime.GOOS == "windows" {
return encodeICO(img)
}
@@ -46,30 +50,93 @@ func colorFor(state string) color.RGBA {
}
}
const iconSize = 16
// 32px rather than 16: Windows shows 16 at 100% scaling and 24 at 150%, and
// scaling a 32 down looks right at both, where a 16 scaled up looks like 2005.
const iconSize = 32
func circle(c color.RGBA) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, iconSize, iconSize))
const r = 6.5
cx, cy := float64(iconSize)/2-0.5, float64(iconSize)/2-0.5
//go:embed tray-logo.png
var trayLogoPNG []byte
var (
trayLogoOnce sync.Once
trayLogo *image.RGBA
)
// logo is the embedded mark, decoded once and box-filtered down to iconSize.
// A box filter rather than nearest-neighbour: 128->32 is an exact 4x4 average
// and nearest would drop three pixels in four, which shreds the thin outline.
func logo() *image.RGBA {
trayLogoOnce.Do(func() {
src, err := png.Decode(bytes.NewReader(trayLogoPNG))
if err != nil {
trayLogo = image.NewRGBA(image.Rect(0, 0, iconSize, iconSize))
return
}
b := src.Bounds()
f := b.Dx() / iconSize
out := image.NewRGBA(image.Rect(0, 0, iconSize, iconSize))
for y := 0; y < iconSize; y++ {
for x := 0; x < iconSize; x++ {
var r, g, bl, a uint64
for dy := 0; dy < f; dy++ {
for dx := 0; dx < f; dx++ {
// Premultiplied so transparent pixels do not drag the
// colour of the edge towards black.
pr, pg, pb, pa := src.At(b.Min.X+x*f+dx, b.Min.Y+y*f+dy).RGBA()
r += uint64(pr)
g += uint64(pg)
bl += uint64(pb)
a += uint64(pa)
}
}
n := uint64(f * f)
out.SetRGBA(x, y, color.RGBA{
R: uint8(r / n >> 8), G: uint8(g / n >> 8), B: uint8(bl / n >> 8), A: uint8(a / n >> 8),
})
}
}
trayLogo = out
})
return trayLogo
}
// trayImage is the mark with a state dot over its bottom-right corner, ringed
// so it reads against both the yellow of the mark and a dark taskbar.
func trayImage(c color.RGBA) *image.RGBA {
base := logo()
img := image.NewRGBA(base.Bounds())
copy(img.Pix, base.Pix)
const r = 6.0
cx, cy := float64(iconSize)-r-0.5, float64(iconSize)-r-0.5
ring := color.RGBA{R: 0x11, G: 0x14, B: 0x18, A: 0xFF}
for y := 0; y < iconSize; y++ {
for x := 0; x < iconSize; x++ {
dx, dy := float64(x)-cx, float64(y)-cy
d := dx*dx + dy*dy
switch {
case d <= (r-1)*(r-1):
case d <= (r-1.5)*(r-1.5):
img.SetRGBA(x, y, c)
case d <= r*r:
img.SetRGBA(x, y, ring)
case d <= (r+1)*(r+1):
// One-pixel feathered edge; a hard-aliased circle looks broken
// next to every other icon in the tray.
a := uint8(float64(c.A) * (r*r - d) / (r*r - (r-1)*(r-1)))
img.SetRGBA(x, y, color.RGBA{R: c.R, G: c.G, B: c.B, A: a})
a := uint8(255 * ((r+1)*(r+1) - d) / ((r+1)*(r+1) - r*r))
bg := img.RGBAAt(x, y)
img.SetRGBA(x, y, blend(bg, ring, a))
}
}
}
return img
}
func blend(under, over color.RGBA, a uint8) color.RGBA {
fa := float64(a) / 255
mix := func(u, o uint8) uint8 { return uint8(float64(u)*(1-fa) + float64(o)*fa) }
ua := float64(under.A)/255*(1-fa) + fa
return color.RGBA{R: mix(under.R, over.R), G: mix(under.G, over.G), B: mix(under.B, over.B), A: uint8(ua * 255)}
}
// encodeICO writes a single-image .ico holding an uncompressed 32-bit DIB.
//
// Vista and later also accept a PNG stored inside the .ico container, which
@@ -94,8 +161,8 @@ func encodeICO(img *image.RGBA) []byte {
// ICONDIRENTRY. 256 is encoded as 0 in these byte fields; at 16px it is moot.
b.WriteByte(byte(w))
b.WriteByte(byte(h))
b.WriteByte(0) // palette size: none
b.WriteByte(0) // reserved
b.WriteByte(0) // palette size: none
b.WriteByte(0) // reserved
binary.Write(&b, binary.LittleEndian, uint16(1)) // colour planes
binary.Write(&b, binary.LittleEndian, uint16(32)) // bits per pixel
binary.Write(&b, binary.LittleEndian, uint32(dib)) // bytes in resource

View File

@@ -13,7 +13,7 @@ import (
// nothing — and nothing on a Mac could notice. These tests are the substitute
// for the Windows box we do not have.
func TestEncodeICOIsAValidIconFile(t *testing.T) {
b := encodeICO(circle(colorFor("ok")))
b := encodeICO(trayImage(colorFor("ok")))
if len(b) < 22 {
t.Fatalf("far too short: %d bytes", len(b))
}
@@ -48,12 +48,13 @@ func TestEncodeICOIsAValidIconFile(t *testing.T) {
func TestEncodeICOPixelsAreBGRABottomUp(t *testing.T) {
want := colorFor("error") // red: distinguishable from B and G if swapped
img := circle(want)
img := trayImage(want)
b := encodeICO(img)
// Centre of the circle, which is solid fill. Bottom-up means image row
// iconSize/2 lands at DIB row iconSize/2-1 counting from the start.
row := iconSize - 1 - iconSize/2
i := 22 + 40 + (row*iconSize+iconSize/2)*4
// Centre of the state dot, which is solid fill. Bottom-up means image row
// y lands at DIB row iconSize-1-y counting from the start.
x, y := iconSize-6-1, iconSize-6-1
row := iconSize - 1 - y
i := 22 + 40 + (row*iconSize+x)*4
got := b[i : i+4]
if !bytes.Equal(got, []byte{want.B, want.G, want.R, 0xFF}) {
t.Errorf("centre pixel = % x, want % x (BGRA)",

Binary file not shown.

BIN
desktop/tray-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -31,6 +31,8 @@
#define MyAppExeName "Behavision.exe"
[Setup]
; The Loyaly mark, on the installer and in Add/Remove Programs.
SetupIconFile=..\brand\loyaly.ico
AppId={{7C4B9E2A-3F51-4C86-9D0A-B1E7A2F65D11}
AppName={#MyAppName}
AppVersion={#MyAppVersion}

View File

@@ -66,15 +66,13 @@ npm run build
Pop-Location
Push-Location (Join-Path $root "desktop")
# Wails v2 talks to WebView2 through pure-Go bindings, so no cgo and no
# toolchain beyond Go itself. Verified by cross-compiling the same package from
# a Mac with CGO_ENABLED=0.
# toolchain beyond Go itself. A plain go build is used on purpose: the icon
# and the manifest are compiled in from rsrc_windows_amd64.syso (go-winres,
# from brand/loyaly-icon-512.png), and `wails build` would add a second copy
# of both and fail the link with duplicate resources.
$env:CGO_ENABLED = "0"
if (Get-Command wails -ErrorAction SilentlyContinue) {
wails build -platform windows/amd64 -clean -ldflags "-X main.version=$Version"
} else {
Write-Warning "wails CLI not found - falling back to a plain go build (no icon, no manifest)."
go build -ldflags "-H windowsgui -X main.version=$Version" -o (Join-Path $root "desktop\build\bin\Behavision.exe") .
}
go build -ldflags "-H windowsgui -X main.version=$Version" -o (Join-Path $root "desktop\build\bin\Behavision.exe") .
if ($LASTEXITCODE -ne 0) { throw "desktop build failed" }
Pop-Location
Step "Headless agent"

View File

@@ -26,5 +26,8 @@ dev = ["pytest>=8.0"]
[tool.setuptools.packages.find]
include = ["behavision*"]
[tool.setuptools.package-data]
behavision = ["static/*"]
[tool.pytest.ini_options]
testpaths = ["tests"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
server/internal/web/dist/favicon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -4,9 +4,10 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark" />
<link rel="icon" type="image/png" href="/favicon.png" />
<title>Behavision</title>
<script type="module" crossorigin src="/assets/index-xQ5C-n-U.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bgt5SnW3.css">
<script type="module" crossorigin src="/assets/index-DR5k3RfF.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-YUqJozyx.css">
</head>
<body>
<div id="root"></div>

BIN
server/internal/web/dist/loyaly-mark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark" />
<link rel="icon" type="image/png" href="/favicon.png" />
<title>Behavision</title>
</head>
<body>

BIN
web/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
web/public/loyaly-mark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -61,12 +61,10 @@ h3 { font-size: 13px; font-weight: 600; text-transform: uppercase;
.brand strong { display: block; font-size: 15px; font-weight: 620; letter-spacing: -.01em; }
.brand .org { display: block; font-size: 12px; color: var(--muted); }
.mark {
width: 18px; height: 18px; border-radius: 50%;
border: 2.5px solid var(--accent);
box-shadow: inset 0 0 0 3px var(--ground);
flex: none;
width: 22px; height: 22px; flex: none;
background: url(/loyaly-mark.png) center / contain no-repeat;
}
.mark.big { width: 30px; height: 30px; border-width: 3px; margin-bottom: 14px; }
.mark.big { width: 40px; height: 40px; margin-bottom: 14px; }
.tabs { display: flex; gap: 2px; margin-right: auto; }
.tabs button {