Update branding and fix video content display

- Update favicon and icons with new 2026 branding
- Fix video content display - skip hero replacement for video posts
- Add video_url to asset resolution in client.js
- Update LiveKit broadcast with egress recording
- Fix auth context and topbar styling
- Update PostCard and map styling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
SocioWire 2026-01-07 20:11:49 +00:00
parent 3e16629a32
commit 29e80a7f0e
24 changed files with 136 additions and 30 deletions

View File

@ -9,8 +9,9 @@
<link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" /> <link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" href="/favicon.ico" /> <link rel="icon" type="image/png" href="/logo2026.png" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/logo2026.png" />
<link rel="shortcut icon" type="image/png" href="/logo2026.png" />
<!-- PWA Manifest --> <!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
@ -18,16 +19,17 @@
<!-- Meta pour partage (logo + titre + description) --> <!-- Meta pour partage (logo + titre + description) -->
<meta property="og:title" content="SocioWire.com - Wired to life" /> <meta property="og:title" content="SocioWire.com - Wired to life" />
<meta property="og:description" content="Local news, events, market deals & friends on the map." /> <meta property="og:description" content="Local news, events, market deals & friends on the map." />
<meta property="og:image" content="https://sociowire.com/icons/og-image.png" /> <meta property="og:image" content="https://sociowire.com/logo2026.png" />
<meta property="og:url" content="https://sociowire.com" /> <meta property="og:url" content="https://sociowire.com" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta name="facebook-domain-verification" content="o4iph92mh9fgqx58sn1b77ia08h8cp" /> <meta name="facebook-domain-verification" content="o4iph92mh9fgqx58sn1b77ia08h8cp" />
<meta name="msvalidate.01" content="FF2D1A70EEA0A84AAF58B82A8E33F686" />
<!-- Twitter Card --> <!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="SocioWire.com - Wired to life" /> <meta name="twitter:title" content="SocioWire.com - Wired to life" />
<meta name="twitter:description" content="Local news, events, market deals & friends on the map." /> <meta name="twitter:description" content="Local news, events, market deals & friends on the map." />
<meta name="twitter:image" content="https://www.sociowire.com/icons/og-image.png" /> <meta name="twitter:image" content="https://sociowire.com/logo2026.png" />
<meta property="og:image:width" content="1200" /> <meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" /> <meta property="og:image:height" content="630" />
@ -46,6 +48,16 @@
} }
} }
</script> </script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "SocioWire",
"url": "https://sociowire.com",
"logo": "https://sociowire.com/logo2026.png",
"sameAs": []
}
</script>
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
@ -70,8 +82,10 @@
</script> </script>
<!-- Titre de la page --> <!-- Titre de la page -->
<title>SocioWire.com - Wired to life</title> <title>SocioWire.com - Wired to life</title>
<style>.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}</style>
</head> </head>
<body> <body>
<h1 class="sr-only">SocioWire - Local News, Events, Deals and Social Posts on a Live Map</h1>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.jsx"></script>
</body> </body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 946 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 170 KiB

BIN
public/logo2026.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -24,4 +24,4 @@
"background_color": "#050816", "background_color": "#050816",
"description": "SocioWire - Local news, events, market deals & friends on map", "description": "SocioWire - Local news, events, market deals & friends on map",
"orientation": "portrait" "orientation": "portrait"
} }

View File

@ -241,7 +241,7 @@ export default function App() {
<footer className="sw-footer"> <footer className="sw-footer">
<div className="sw-footer-inner"> <div className="sw-footer-inner">
<div className="sw-footer-brand">SOCIOWIRE</div> <div className="sw-footer-brand">SOCIOWIRE</div>
<div className="sw-footer-brand">© Sociowire v0.0001</div> <div className="sw-footer-brand">© Sociowire v0.0002</div>
<nav className="sw-footer-nav" aria-label="Site"> <nav className="sw-footer-nav" aria-label="Site">
<a href="/">Home</a> <a href="/">Home</a>
<a href="/#map">Live Map</a> <a href="/#map">Live Map</a>

View File

@ -107,7 +107,7 @@ async function resolveAssetRefsInPosts(posts) {
if (!Array.isArray(posts) || posts.length === 0) return posts; if (!Array.isArray(posts) || posts.length === 0) return posts;
const ids = []; const ids = [];
for (const p of posts) { for (const p of posts) {
[p?.image_small, p?.image_medium, p?.image_large, p?.image, p?.media_url].forEach((v) => { [p?.image_small, p?.image_medium, p?.image_large, p?.image, p?.media_url, p?.video_url].forEach((v) => {
if (isAssetRef(v)) ids.push(v.replace("asset://", "")); if (isAssetRef(v)) ids.push(v.replace("asset://", ""));
}); });
if (Array.isArray(p?.media_urls)) { if (Array.isArray(p?.media_urls)) {
@ -127,7 +127,7 @@ async function resolveAssetRefsInPosts(posts) {
clone.id = clone.post_id; clone.id = clone.post_id;
} }
} }
["image_small", "image_medium", "image_large", "image", "media_url"].forEach((k) => { ["image_small", "image_medium", "image_large", "image", "media_url", "video_url"].forEach((k) => {
const v = clone[k]; const v = clone[k];
if (isAssetRef(v)) { if (isAssetRef(v)) {
const id = v.replace("asset://", ""); const id = v.replace("asset://", "");

View File

@ -28,6 +28,15 @@ function isCrossOriginSSO() {
} }
} }
// Check if we're on the isolated apps subdomain (no SSO sync with main site)
function isAppsSubdomain() {
try {
return window.location.hostname === "apps.sociowire.com";
} catch {
return false;
}
}
let kcInstance = null; let kcInstance = null;
function getKeycloak() { function getKeycloak() {
if (!kcInstance) { if (!kcInstance) {
@ -437,6 +446,9 @@ export function AuthProvider({ children }) {
setUser(w.username ? { username: w.username } : null); setUser(w.username ? { username: w.username } : null);
setStatus("auth"); setStatus("auth");
// Clear logout flag so session can restore on reload
try { sessionStorage.removeItem("sociowire:logout"); } catch {}
// update verified state from backend // update verified state from backend
await refreshProfile(t.access_token); await refreshProfile(t.access_token);
@ -447,7 +459,8 @@ export function AuthProvider({ children }) {
if (refreshTimer.current) clearInterval(refreshTimer.current); if (refreshTimer.current) clearInterval(refreshTimer.current);
refreshTimer.current = null; refreshTimer.current = null;
const redirectUri = window.location.origin + window.location.pathname; const redirectUri = window.location.origin + window.location.pathname;
const doSsoLogout = keycloakEnabled(); // Don't do SSO logout for apps subdomain - keep it isolated
const doSsoLogout = keycloakEnabled() && !isAppsSubdomain();
try { try {
sessionStorage.setItem("sociowire:logout", String(Date.now())); sessionStorage.setItem("sociowire:logout", String(Date.now()));
} catch {} } catch {}
@ -553,11 +566,13 @@ export function AuthProvider({ children }) {
} }
setUser(w.username ? { username: w.username } : null); setUser(w.username ? { username: w.username } : null);
try { sessionStorage.removeItem("sociowire:logout"); } catch {}
setStatus("auth"); setStatus("auth");
await refreshProfile(t.access_token); await refreshProfile(t.access_token);
} catch (err) { } catch (err) {
console.error("[Auth] Post-SSO verification failed:", err); console.error("[Auth] Post-SSO verification failed:", err);
// Still set as authenticated since we have tokens // Still set as authenticated since we have tokens
try { sessionStorage.removeItem("sociowire:logout"); } catch {}
setStatus("auth"); setStatus("auth");
} }
@ -579,6 +594,12 @@ export function AuthProvider({ children }) {
return false; return false;
} }
// Skip silent SSO for apps subdomain - keep it isolated from main site
if (isAppsSubdomain()) {
console.log("[Auth] Skipping silent SSO check (apps subdomain - isolated)");
return false;
}
let kc = null; let kc = null;
try { try {
if (sessionStorage.getItem("sociowire:logout")) { if (sessionStorage.getItem("sociowire:logout")) {

View File

@ -406,7 +406,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
<header className="topbar"> <header className="topbar">
<div className="brand-stack"> <div className="brand-stack">
<div className="brand-toprow"> <div className="brand-toprow">
<div className="logo-circle">SW</div> <div className="logo-circle"><img src="/logo2026.png" alt="SocioWire" /></div>
<div className="title-block"> <div className="title-block">
<div className="main-title">SOCIOWIRE.com</div> <div className="main-title">SOCIOWIRE.com</div>
<div className="sub-title">Wired to life</div> <div className="sub-title">Wired to life</div>

View File

@ -4,6 +4,7 @@ import { useAuth } from "../../auth/AuthContext";
import "./Live.css"; import "./Live.css";
const API_BASE = (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, ""); const API_BASE = (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, "");
const LIVEKIT_API = "https://live.us1.sociowire.com";
/** /**
* LiveKit-based broadcast component * LiveKit-based broadcast component
@ -18,6 +19,19 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
const [localStream, setLocalStream] = useState(null); const [localStream, setLocalStream] = useState(null);
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
const [livePostId, setLivePostId] = useState(null); const [livePostId, setLivePostId] = useState(null);
const [liveCoords] = useState(() => {
if (coords && typeof coords === "object") {
if (Array.isArray(coords) && coords.length >= 2) {
return { lon: coords[0], lat: coords[1] };
}
const lat = coords.lat ?? coords.latitude;
const lon = coords.lon ?? coords.lng ?? coords.longitude;
if (Number.isFinite(lat) && Number.isFinite(lon)) {
return { lat, lon };
}
}
return { lat: null, lon: null };
});
const videoRef = useRef(null); const videoRef = useRef(null);
@ -90,7 +104,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
onStreamCreated({ onStreamCreated({
roomName, roomName,
title: title.trim(), title: title.trim(),
coords, coords: liveCoords,
}); });
} }
@ -106,8 +120,8 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
snippet: "Live now", snippet: "Live now",
category: "EVENT", category: "EVENT",
sub_category: "Live", sub_category: "Live",
lat: coords?.lat, lat: liveCoords?.lat,
lon: coords?.lon, lon: liveCoords?.lon,
author: username, author: username,
source: "sociowire", source: "sociowire",
visibility: "public", visibility: "public",
@ -128,13 +142,31 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
console.error("[LiveKit] Failed to create live post:", err); console.error("[LiveKit] Failed to create live post:", err);
alert("Live started, but we couldn't create the live post."); alert("Live started, but we couldn't create the live post.");
} }
}, [title, localStream, startBroadcast, roomName, coords, onStreamCreated, authToken, username]); }, [title, localStream, startBroadcast, roomName, liveCoords, onStreamCreated, authToken, username]);
const handleEndStream = useCallback(async (save) => { const handleEndStream = useCallback(async (save) => {
await stopBroadcast(); await stopBroadcast();
if (localStream) { if (localStream) {
localStream.getTracks().forEach((t) => t.stop()); localStream.getTracks().forEach((t) => t.stop());
} }
// Stop egress recording and get video key
let videoKey = "";
try {
const egressRes = await fetch(`${LIVEKIT_API}/api/livekit/egress/stop`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ room: roomName, save: !!save }),
});
if (egressRes.ok) {
const egressData = await egressRes.json();
videoKey = egressData.key || "";
console.log("[LiveKit] Egress stopped, video key:", videoKey);
}
} catch (err) {
console.warn("[LiveKit] Failed to stop egress:", err);
}
if (livePostId) { if (livePostId) {
try { try {
const res = await fetch(`${API_BASE}/api/live/finalize`, { const res = await fetch(`${API_BASE}/api/live/finalize`, {
@ -146,7 +178,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
body: JSON.stringify({ body: JSON.stringify({
post_id: livePostId, post_id: livePostId,
save: !!save, save: !!save,
video_url: "", video_key: videoKey,
}), }),
}); });
if (res.ok && save) { if (res.ok && save) {
@ -159,7 +191,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
if (!res.ok) { if (!res.ok) {
const errText = await res.text(); const errText = await res.text();
console.error("[LiveKit] Finalize failed:", res.status, errText); console.error("[LiveKit] Finalize failed:", res.status, errText);
alert("We couldn't save the live post. Please verify your email and try again."); alert("We couldn't save the live post. Please try again.");
} }
} catch (err) { } catch (err) {
console.error("[LiveKit] Failed to finalize live post:", err); console.error("[LiveKit] Failed to finalize live post:", err);
@ -167,7 +199,7 @@ export default function LiveKitBroadcast({ coords, onClose, onStreamCreated }) {
} }
} }
onClose?.(); onClose?.();
}, [stopBroadcast, localStream, onClose, livePostId, authToken, username]); }, [stopBroadcast, localStream, onClose, livePostId, authToken, roomName]);
const handleBackdropClick = (e) => { const handleBackdropClick = (e) => {
if (e.target === e.currentTarget && !isLive) { if (e.target === e.currentTarget && !isLive) {

View File

@ -148,6 +148,18 @@ export function useLiveKit({ roomName, userID, username, token: authToken }) {
roomRef.current = room; roomRef.current = room;
setIsLive(true); setIsLive(true);
console.log("[LiveKit] Broadcasting started"); console.log("[LiveKit] Broadcasting started");
// Start egress recording
try {
await fetch(`${LIVEKIT_API}/api/livekit/egress/start`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ room: roomName }),
});
console.log("[LiveKit] Egress recording started");
} catch (err) {
console.warn("[LiveKit] Failed to start egress:", err);
}
} catch (err) { } catch (err) {
console.error("[LiveKit] Failed to start broadcast:", err); console.error("[LiveKit] Failed to start broadcast:", err);
setError(err.message || "Failed to start broadcast"); setError(err.message || "Failed to start broadcast");

View File

@ -235,9 +235,10 @@ function applyPostDetails(modalWrap, post) {
const summaryEl = modalWrap.querySelector(".sw-modal-summary"); const summaryEl = modalWrap.querySelector(".sw-modal-summary");
if (summaryEl) summaryEl.textContent = summary; if (summaryEl) summaryEl.textContent = summary;
// Skip hero replacement for camera posts - they have video content // Skip hero replacement for camera and video posts - they have video content
const isCamera = post?.content_type === 'camera'; const isCamera = post?.content_type === 'camera';
if (!isCamera) { const isVideo = post?.content_type === 'video';
if (!isCamera && !isVideo) {
const heroGallery = collectGalleryImages(post); const heroGallery = collectGalleryImages(post);
const heroImageSrc = heroGallery[0] || heroImageForPost(post); const heroImageSrc = heroGallery[0] || heroImageForPost(post);
const heroFullSrc = heroGallery[0] || fullImageForPost(post); const heroFullSrc = heroGallery[0] || fullImageForPost(post);
@ -1015,11 +1016,15 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
title: postData.title?.substring(0, 30) title: postData.title?.substring(0, 30)
}); });
// Camera posts now use the regular modal with video content // Camera and video posts use the regular modal with video content
const isCamera = postData.content_type === 'camera'; const isCamera = postData.content_type === 'camera';
const isVideo = postData.content_type === 'video';
if (isCamera) { if (isCamera) {
console.log('[openCenteredOverlay] Opening camera as regular post with video'); console.log('[openCenteredOverlay] Opening camera as regular post with video');
} }
if (isVideo) {
console.log('[openCenteredOverlay] Opening saved video post:', postData.video_url);
}
const data = adaptPostToTemplateData(postData, { size: "full" }); const data = adaptPostToTemplateData(postData, { size: "full" });
const cat = normCatLabel(postData); const cat = normCatLabel(postData);
@ -1191,7 +1196,7 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
<button class="sw-truth-rail-btn sw-truth-rail-btn-down" type="button">FALSE</button> <button class="sw-truth-rail-btn sw-truth-rail-btn-down" type="button">FALSE</button>
</div> </div>
` : ''} ` : ''}
<div class="sw-modal-hero" ${isCamera ? 'style="flex:1;max-width:100%;"' : ''}> <div class="sw-modal-hero" ${(isCamera || isVideo) ? 'style="flex:1;max-width:100%;"' : ''}>
${isCamera ? ` ${isCamera ? `
<div class="sw-camera-hero" style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:12px;overflow:hidden;"> <div class="sw-camera-hero" style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:12px;overflow:hidden;">
<video <video
@ -1210,6 +1215,21 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
LIVE LIVE
</div> </div>
</div> </div>
` : isVideo ? `
<div class="sw-video-hero" style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:12px;overflow:hidden;">
<video
controls
playsinline
style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;"
src="${escapeHtml(postData.video_url || '')}"
>
<source src="${escapeHtml(postData.video_url || '')}" type="video/mp4">
</video>
<div class="sw-video-badge" style="position:absolute;top:12px;left:12px;display:flex;align-items:center;gap:6px;padding:4px 10px;background:rgba(59,130,246,0.9);border-radius:4px;font-size:11px;font-weight:700;color:#fff;text-transform:uppercase;">
<i class="fa-solid fa-video"></i>
RECORDED
</div>
</div>
` : img ` : img
? `<img src="${escapeHtml(img)}" data-fullsrc="${escapeHtml(fullImageForPost(postData))}" alt="" loading="lazy" />` ? `<img src="${escapeHtml(img)}" data-fullsrc="${escapeHtml(fullImageForPost(postData))}" alt="" loading="lazy" />`
: `<div class="sw-modal-hero-fallback"><div class="sw-modal-hero-fallback-inner"><i class="${categoryIconClass(postData)}"></i><span>${escapeHtml(normCatLabel(postData))}</span></div></div>` : `<div class="sw-modal-hero-fallback"><div class="sw-modal-hero-fallback-inner"><i class="${categoryIconClass(postData)}"></i><span>${escapeHtml(normCatLabel(postData))}</span></div></div>`

View File

@ -295,7 +295,13 @@ export default function PostCard({ post, selected, onSelect }) {
<article <article
className={"post-card sw-wall-card" + (selected ? " selected" : "")} className={"post-card sw-wall-card" + (selected ? " selected" : "")}
onClick={() => { onClick={() => {
trackEvent("post_open", { post_id: postId, context: "wall" }); trackEvent("post_view", {
post_id: postId,
post_title: title?.slice(0, 100),
post_category: cat,
post_author: author,
content_type: "post",
});
onSelect && onSelect(post); onSelect && onSelect(post);
}} }}
> >
@ -397,7 +403,7 @@ export default function PostCard({ post, selected, onSelect }) {
requestAuth("Please sign in or create a free account to like this post."); requestAuth("Please sign in or create a free account to like this post.");
return; return;
} }
trackEvent("post_like_click", { post_id: postId, context: "wall" }); trackEvent("post_like", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat });
const res = await togglePostLike(postId, !liked); const res = await togglePostLike(postId, !liked);
if (res?.ok) { if (res?.ok) {
setLiked(!!res.liked); setLiked(!!res.liked);
@ -422,7 +428,7 @@ export default function PostCard({ post, selected, onSelect }) {
requestAuth("Please sign in or create a free account to comment."); requestAuth("Please sign in or create a free account to comment.");
return; return;
} }
trackEvent("post_comment_open", { post_id: postId, context: "wall" }); trackEvent("post_comment", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat });
if (onSelect) onSelect(post); if (onSelect) onSelect(post);
}} }}
> >
@ -434,7 +440,7 @@ export default function PostCard({ post, selected, onSelect }) {
onClick={async (e) => { onClick={async (e) => {
e.stopPropagation(); e.stopPropagation();
const shareUrl = `${window.location.origin}/p/${postId}`; const shareUrl = `${window.location.origin}/p/${postId}`;
trackEvent("post_share_click", { post_id: postId, context: "wall" }); trackEvent("post_share", { post_id: postId, post_title: title?.slice(0, 100), post_category: cat });
try { try {
if (navigator.share) { if (navigator.share) {
await navigator.share({ title: title || "SocioWire", url: shareUrl }); await navigator.share({ title: title || "SocioWire", url: shareUrl });

View File

@ -209,6 +209,7 @@
gap:.4rem; gap:.4rem;
} }
/* Crosshair pour création */ /* Crosshair pour création */
.map-crosshair{ .map-crosshair{
left:50%; left:50%;

View File

@ -13,11 +13,11 @@
} }
.logo-circle{ .logo-circle{
width:40px; height:40px; border-radius:50%; flex-shrink:0; width:52px; height:52px; flex-shrink:0;
background-image:url("/icons/logo-master.png"); margin: -6px 0;
background-size:cover; background-position:center; background-repeat:no-repeat; }
box-shadow: 0 0 10px rgba(37,99,235,.8), 0 0 18px rgba(56,189,248,.6); .logo-circle img{
color:transparent; width:100%; height:100%; object-fit:contain;
} }
.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; } .title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }