Merge pull request 'JRCC logos (light/dark) + watermark + 9-style picker' (#75) from feature/jrcc-brand-styles into master
CI / skip-ci-check (push) Successful in 29s
CI / docker-ci (push) Successful in 30s
CI / python-lint (push) Successful in 31s
CI / secret-scan (push) Successful in 35s
CI / viewer-unit (push) Successful in 1m35s
CI / admin-unit (push) Successful in 1m52s
CI / e2e (push) Successful in 3m21s

This commit was merged in pull request #75.
This commit is contained in:
2026-08-04 12:07:57 -05:00
16 changed files with 467 additions and 91 deletions
+5
View File
@@ -103,3 +103,8 @@ logs/
e2e/env-defaults.local.json
viewer-frontend/.data/
# Brand logos (explicitly tracked despite *.png)
!viewer-frontend/public/logo.png
!viewer-frontend/public/brand/
!viewer-frontend/public/brand/**/*.png
+1 -1
View File
@@ -28,7 +28,7 @@ Dev URLs: https://punimtagdev.levkin.ca · https://punimtagadmindev.levkin.ca
| Issue | Need |
|-------|------|
| **#44** JRCC watermark | SVG/PNG + placement/opacity |
| **#44** JRCC watermark | ✅ PNG watermark + light/dark header logos (`public/brand/`, `/styles` picker) |
| **#43** Tag without account | Product rules (approval, PII, ToS) |
## Performance notes (#26)
@@ -5,10 +5,10 @@ import { createPortal } from 'react-dom';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ManageUsersContent } from './ManageUsersContent';
import Image from 'next/image';
import Link from 'next/link';
import UserMenu from '@/components/UserMenu';
import { ThemeToggle } from '@/components/ThemeToggle';
import { JrccLogo } from '@/components/JrccLogo';
interface ManageUsersPageClientProps {
onClose?: () => void;
@@ -34,16 +34,9 @@ export function ManageUsersPageClient({ onClose }: ManageUsersPageClientProps) {
<main id="main-content" className="w-full px-4 pt-3 pb-8">
{/* Header */}
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-2 mb-3 border-b">
<div className="mb-2 flex items-center justify-between">
<Link href="/" aria-label="Home">
<Image
src="/logo.png"
alt="PunimTag"
width={300}
height={80}
className="h-11 w-auto cursor-pointer hover:opacity-80 transition-opacity"
priority
/>
<div className="mb-2 flex items-center justify-between gap-3">
<Link href="/" aria-label="JRCC home" className="min-w-0 shrink">
<JrccLogo priority className="cursor-pointer hover:opacity-90 transition-opacity" />
</Link>
<div className="flex items-center gap-1">
<ThemeToggle />
@@ -45,38 +45,49 @@ async function getWatermarkOverlay(baseWidth: number, baseHeight: number) {
let cached: Buffer | undefined = watermarkCache.get(cacheKey);
if (!cached) {
const diagonal = Math.sqrt(bucketWidth * bucketWidth + bucketHeight * bucketHeight);
const watermarkWidth = Math.round(diagonal * 1.05);
const watermarkHeight = Math.round((watermarkWidth * 120) / 360);
const watermarkSvg = `
<svg width="${watermarkWidth}" height="${watermarkHeight}" viewBox="0 0 360 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="wmGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#1a3c7c" stop-opacity="0.45"/>
<stop offset="100%" stop-color="#0a1e3f" stop-opacity="0.45"/>
</linearGradient>
</defs>
<rect width="360" height="120" fill="url(#wmGradient)" rx="20" ry="20" />
<g fill="rgba(255,255,255,0.65)">
<text x="30" y="82" font-family="'Inter','Segoe UI',sans-serif" font-size="72" font-weight="700">J</text>
<text x="144" y="82" font-family="'Inter','Segoe UI',sans-serif" font-size="72" font-weight="700">M</text>
<g transform="translate(80,20) scale(0.7)">
<polygon points="60,0 80,35 120,35 90,57 100,95 60,75 20,95 30,57 0,35 40,35"
fill="none" stroke="rgba(255,255,255,0.7)" stroke-width="12" stroke-linejoin="round" />
</g>
<text x="214" y="50" font-family="'Inter','Segoe UI',sans-serif" font-size="24" font-weight="600">Jewish</text>
<text x="214" y="78" font-family="'Inter','Segoe UI',sans-serif" font-size="24" font-weight="600">and Modern</text>
</g>
</svg>
`;
const watermarkWidth = Math.round(diagonal * 0.55);
const logoPath = path.join(process.cwd(), 'public', 'brand', 'jrcc-watermark.png');
const fallbackLogoPath = path.join(process.cwd(), 'public', 'brand', 'jrcc-logo-light.png');
const logoFile = existsSync(logoPath) ? logoPath : fallbackLogoPath;
let logoBuffer: Buffer;
if (existsSync(logoFile)) {
logoBuffer = await sharp(logoFile)
.resize({
width: watermarkWidth,
fit: 'inside',
withoutEnlargement: false,
})
.ensureAlpha()
.toBuffer();
} else {
// Fallback SVG if brand assets are missing
const watermarkHeight = Math.round((watermarkWidth * 120) / 360);
const watermarkSvg = `
<svg width="${watermarkWidth}" height="${watermarkHeight}" viewBox="0 0 360 120" xmlns="http://www.w3.org/2000/svg">
<text x="20" y="78" font-family="'Inter','Segoe UI',sans-serif" font-size="64" font-weight="700" fill="rgba(255,255,255,0.55)">JRCC</text>
</svg>
`;
logoBuffer = await sharp(Buffer.from(watermarkSvg)).ensureAlpha().toBuffer();
}
const logoMeta = await sharp(logoBuffer).metadata();
const logoW = logoMeta.width || watermarkWidth;
const logoH = logoMeta.height || Math.round(watermarkWidth * 0.33);
const canvas = await sharp({
create: {
width: logoW + 80,
height: logoH + 60,
channels: 4,
background: { r: 10, g: 30, b: 63, alpha: 0.28 },
},
})
.composite([{ input: logoBuffer, gravity: 'centre' }])
.png()
.toBuffer();
const rotationAngle = -Math.atan(bucketHeight / bucketWidth) * (180 / Math.PI);
const newCached = await sharp(Buffer.from(watermarkSvg))
.resize({
width: watermarkWidth,
height: watermarkHeight,
fit: 'inside',
withoutEnlargement: true,
})
const newCached = await sharp(canvas)
.rotate(rotationAngle, { background: { r: 0, g: 0, b: 0, alpha: 0 } })
.resize({
width: bucketWidth,
+2 -2
View File
@@ -10,8 +10,8 @@ const inter = Inter({
});
export const metadata: Metadata = {
title: "PunimTag Photo Viewer",
description: "Browse and search your family photos",
title: "JRCC Photo Gallery",
description: "Jewish Russian Community Centre of Ontario — browse and search community photos",
};
export default function RootLayout({
@@ -0,0 +1,326 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { ThemeToggle } from '@/components/ThemeToggle';
import { JrccLogo } from '@/components/JrccLogo';
import { cn } from '@/lib/utils';
const STORAGE_KEY = 'jrcc-gallery-style-pick';
type StyleId =
| 'flame-forum'
| 'memory-lane'
| 'face-challenge'
| 'reunion-pulse'
| 'story-shelves'
| 'soft-salon'
| 'night-gallery'
| 'digest-desk'
| 'cluster-lab';
type StyleOption = {
id: StyleId;
name: string;
tagline: string;
whyComeBack: string;
palette: string[];
fonts: string;
mood: string;
previewClass: string;
accentClass: string;
sampleTitle: string;
sampleSub: string;
};
const STYLES: StyleOption[] = [
{
id: 'flame-forum',
name: 'Flame & Forum',
tagline: 'Community board energy',
whyComeBack: 'See who tagged faces this week and jump into open unknowns.',
palette: ['#0B1F4B', '#1E40AF', '#F5C518', '#F8FAFC'],
fonts: 'Display: Manrope · Body: Source Sans 3',
mood: 'Confident JRCC blue/gold, notice-board rhythm',
previewClass: 'bg-[#0B1F4B] text-white',
accentClass: 'bg-[#F5C518] text-[#0B1F4B]',
sampleTitle: "Who's new this week",
sampleSub: '12 faces waiting · 4 people tagged',
},
{
id: 'memory-lane',
name: 'Memory Lane',
tagline: 'On this day, years ago',
whyComeBack: 'Daily nostalgia: same date across decades of community photos.',
palette: ['#2A2118', '#C4A484', '#F3EDE4', '#8B4513'],
fonts: 'Display: Fraunces · Body: Literata',
mood: 'Warm archival paper, soft film grain',
previewClass: 'bg-[#F3EDE4] text-[#2A2118]',
accentClass: 'bg-[#8B4513] text-[#F3EDE4]',
sampleTitle: 'On this day · Aug 4',
sampleSub: 'Photos from 1998 · 2007 · 2019',
},
{
id: 'face-challenge',
name: 'Face Challenge',
tagline: 'Five unknowns a day',
whyComeBack: 'A short daily streak: help name faces, climb a light scoreboard.',
palette: ['#111827', '#22D3EE', '#F472B6', '#F9FAFB'],
fonts: 'Display: Space Grotesk · Body: IBM Plex Sans',
mood: 'Playful streak UI, big face crops, one-tap suggest',
previewClass: 'bg-[#111827] text-white',
accentClass: 'bg-[#22D3EE] text-[#111827]',
sampleTitle: 'Daily challenge · 3 / 5',
sampleSub: 'Streak: 6 days · Keep going',
},
{
id: 'reunion-pulse',
name: 'Reunion Pulse',
tagline: 'Event mode progress',
whyComeBack: 'Live “faces still unknown” bar after weddings, picnics, holidays.',
palette: ['#7F1D1D', '#FCA5A5', '#FEF2F2', '#1F2937'],
fonts: 'Display: Sora · Body: DM Sans',
mood: 'Celebration meter, upload dump → clear the queue',
previewClass: 'bg-[#FEF2F2] text-[#7F1D1D]',
accentClass: 'bg-[#7F1D1D] text-white',
sampleTitle: 'Purim 2026 album',
sampleSub: '68% identified · 41 faces left',
},
{
id: 'story-shelves',
name: 'Story Shelves',
tagline: 'People as timelines',
whyComeBack: 'Open a person and scroll their life across events — not crop grids.',
palette: ['#1C1917', '#A8A29E', '#FAFAF9', '#0EA5E9'],
fonts: 'Display: Newsreader · Body: Karla',
mood: 'Editorial shelves, chapter headings per decade',
previewClass: 'bg-[#FAFAF9] text-[#1C1917]',
accentClass: 'bg-[#0EA5E9] text-white',
sampleTitle: 'Anna K. · Timeline',
sampleSub: '1989 → Toronto · 112 photos',
},
{
id: 'soft-salon',
name: 'Soft Salon',
tagline: 'Calm museum gallery',
whyComeBack: 'Quiet browsing with generous space — invite family without noise.',
palette: ['#F7F5F2', '#3F3A36', '#9A8F84', '#D6CFC6'],
fonts: 'Display: Cormorant Garamond · Body: Outfit',
mood: 'Breathing room, soft stone, restrained motion',
previewClass: 'bg-[#F7F5F2] text-[#3F3A36]',
accentClass: 'bg-[#3F3A36] text-[#F7F5F2]',
sampleTitle: 'Collection',
sampleSub: 'Search by people · seasons · places',
},
{
id: 'night-gallery',
name: 'Night Gallery',
tagline: 'Dark-first cinema',
whyComeBack: 'Evening browsing: luminous photos on deep navy, focus on faces.',
palette: ['#020617', '#1E3A8A', '#E2E8F0', '#FBBF24'],
fonts: 'Display: Syne · Body: Figtree',
mood: 'Cinematic dark, gold spark accents',
previewClass: 'bg-[#020617] text-[#E2E8F0]',
accentClass: 'bg-[#FBBF24] text-[#020617]',
sampleTitle: 'Evening gallery',
sampleSub: 'Favorites · recently tagged',
},
{
id: 'digest-desk',
name: 'Digest Desk',
tagline: 'Reasons to open mail',
whyComeBack: 'Weekly digest preview in-app: 3 photos + one “who is this?”',
palette: ['#0F172A', '#334155', '#F1F5F9', '#EA580C'],
fonts: 'Display: Libre Baskerville · Body: Public Sans',
mood: 'Editorial inbox, clip + caption cards',
previewClass: 'bg-[#F1F5F9] text-[#0F172A]',
accentClass: 'bg-[#EA580C] text-white',
sampleTitle: 'This weeks digest',
sampleSub: '3 memories · 1 mystery face',
},
{
id: 'cluster-lab',
name: 'Cluster Lab',
tagline: 'Group lookalikes',
whyComeBack: 'Name one stranger → cascade to a cluster of similar unknowns.',
palette: ['#042F2E', '#14B8A6', '#CCFBF1', '#134E4A'],
fonts: 'Display: Rubik · Body: Nunito Sans',
mood: 'Lab grid of face clusters, teal precision',
previewClass: 'bg-[#042F2E] text-[#CCFBF1]',
accentClass: 'bg-[#14B8A6] text-[#042F2E]',
sampleTitle: 'Cluster · 9 similar faces',
sampleSub: 'Name one to link the rest',
},
];
function StylePreview({ style }: { style: StyleOption }) {
return (
<div className={cn('rounded-xl overflow-hidden border border-black/10 shadow-sm', style.previewClass)}>
<div className="px-3 pt-3 pb-2 flex items-center justify-between gap-2">
<div className="text-[10px] uppercase tracking-[0.14em] opacity-70">JRCC gallery</div>
<span className={cn('text-[10px] font-semibold px-2 py-0.5 rounded-full', style.accentClass)}>
preview
</span>
</div>
<div className="px-3 pb-3">
<div className="text-sm font-semibold leading-tight">{style.sampleTitle}</div>
<div className="text-[11px] opacity-75 mt-0.5">{style.sampleSub}</div>
<div className="mt-3 grid grid-cols-3 gap-1.5">
{[0, 1, 2].map((i) => (
<div
key={i}
className="aspect-square rounded-md bg-black/10 dark:bg-white/10 flex items-center justify-center text-[10px] opacity-60"
>
photo
</div>
))}
</div>
<div className="mt-3 flex gap-1.5">
{style.palette.map((c) => (
<span
key={c}
title={c}
className="h-3 w-3 rounded-full border border-black/15"
style={{ backgroundColor: c }}
/>
))}
</div>
</div>
</div>
);
}
export default function StylesPickerClient() {
const [picked, setPicked] = useState<StyleId | null>(null);
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
const saved = window.localStorage.getItem(STORAGE_KEY) as StyleId | null;
if (saved && STYLES.some((s) => s.id === saved)) {
setPicked(saved);
}
setHydrated(true);
}, []);
const selected = useMemo(
() => STYLES.find((s) => s.id === picked) ?? null,
[picked]
);
const choose = (id: StyleId) => {
setPicked(id);
window.localStorage.setItem(STORAGE_KEY, id);
};
return (
<div className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur">
<div className="mx-auto max-w-6xl px-4 py-3 flex items-center justify-between gap-3">
<Link href="/" aria-label="JRCC home" className="min-w-0">
<JrccLogo heightClassName="h-10" />
</Link>
<div className="flex items-center gap-2">
<ThemeToggle />
<Link
href="/"
className="text-sm px-3 py-1.5 rounded-md border hover:bg-muted transition-colors"
>
Back to gallery
</Link>
</div>
</div>
</header>
<main id="main-content" className="mx-auto max-w-6xl px-4 py-8">
<div className="max-w-2xl">
<p className="text-xs uppercase tracking-[0.16em] text-muted-foreground mb-2">
Design lab · pick one
</p>
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight">
Nine styles for a gallery people return to
</h1>
<p className="mt-3 text-muted-foreground leading-relaxed">
Each card is a direction for more excitement and habit not a full rebuild yet.
Toggle light/dark above to check the JRCC logos. Tap a style to save your pick
(stored in this browser only).
</p>
</div>
{hydrated && selected && (
<div className="mt-6 rounded-xl border bg-card p-4 flex flex-col sm:flex-row sm:items-center gap-3 justify-between">
<div>
<div className="text-xs uppercase tracking-wide text-muted-foreground">Your pick</div>
<div className="font-semibold text-lg">{selected.name}</div>
<div className="text-sm text-muted-foreground">{selected.whyComeBack}</div>
</div>
<button
type="button"
onClick={() => {
window.localStorage.removeItem(STORAGE_KEY);
setPicked(null);
}}
className="text-sm px-3 py-1.5 rounded-md border hover:bg-muted self-start"
>
Clear pick
</button>
</div>
)}
<div className="mt-8 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{STYLES.map((style, index) => {
const isSelected = picked === style.id;
return (
<button
key={style.id}
type="button"
onClick={() => choose(style.id)}
className={cn(
'text-left rounded-2xl border p-3 transition-all hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
isSelected ? 'border-primary ring-2 ring-primary/40' : 'border-border'
)}
>
<div className="flex items-baseline justify-between gap-2 mb-2 px-0.5">
<span className="text-xs text-muted-foreground tabular-nums">
{String(index + 1).padStart(2, '0')}
</span>
{isSelected && (
<span className="text-xs font-medium text-primary">Selected</span>
)}
</div>
<StylePreview style={style} />
<div className="mt-3 px-0.5">
<h2 className="font-semibold text-base">{style.name}</h2>
<p className="text-sm text-muted-foreground">{style.tagline}</p>
<p className="mt-2 text-sm leading-snug">{style.whyComeBack}</p>
<p className="mt-2 text-[11px] text-muted-foreground">{style.fonts}</p>
<p className="text-[11px] text-muted-foreground">{style.mood}</p>
</div>
</button>
);
})}
</div>
<section className="mt-12 mb-8 rounded-2xl border p-5 bg-muted/30">
<h2 className="font-semibold text-lg">Logo usage (this pass)</h2>
<ul className="mt-2 text-sm text-muted-foreground space-y-1.5 list-disc pl-5">
<li>
<strong className="text-foreground">Light mode header:</strong> classic black
bilingual JRCC mark (`jrcc-logo-light.png`)
</li>
<li>
<strong className="text-foreground">Dark mode header:</strong> white + blue/gold
mark (`jrcc-logo-dark.png`)
</li>
<li>
<strong className="text-foreground">Photo watermark:</strong> translucent JRCC
overlay for guest downloads (`jrcc-watermark.png`)
</li>
</ul>
<p className="mt-3 text-sm text-muted-foreground">
Tell us which style number wins and we&apos;ll implement that direction next.
</p>
</section>
</main>
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
import type { Metadata } from 'next';
import StylesPickerClient from './StylesPickerClient';
export const metadata: Metadata = {
title: 'JRCC gallery — style picker',
description: 'Pick a visual direction for the JRCC photo gallery redesign.',
};
export default function StylesPage() {
return <StylesPickerClient />;
}
@@ -4,10 +4,10 @@ import { useRouter } from 'next/navigation';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { UploadContent } from './UploadContent';
import Image from 'next/image';
import Link from 'next/link';
import UserMenu from '@/components/UserMenu';
import { ThemeToggle } from '@/components/ThemeToggle';
import { JrccLogo } from '@/components/JrccLogo';
export function UploadPageClient() {
const router = useRouter();
@@ -21,16 +21,9 @@ export function UploadPageClient() {
<main id="main-content" className="w-full px-4 pt-3 pb-8">
{/* Header */}
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-2 mb-3 border-b">
<div className="mb-2 flex items-center justify-between">
<Link href="/" aria-label="Home">
<Image
src="/logo.png"
alt="PunimTag"
width={300}
height={80}
className="h-11 w-auto cursor-pointer hover:opacity-80 transition-opacity"
priority
/>
<div className="mb-2 flex items-center justify-between gap-3">
<Link href="/" aria-label="JRCC home" className="min-w-0 shrink">
<JrccLogo priority className="cursor-pointer hover:opacity-90 transition-opacity" />
</Link>
<div className="flex items-center gap-1">
<ThemeToggle />
+51
View File
@@ -0,0 +1,51 @@
'use client';
import Image from 'next/image';
import { useSyncExternalStore } from 'react';
import { useTheme } from 'next-themes';
import { cn } from '@/lib/utils';
const emptySubscribe = () => () => {};
function useMounted() {
return useSyncExternalStore(
emptySubscribe,
() => true,
() => false
);
}
type JrccLogoProps = {
className?: string;
/** Header height in CSS (Tailwind class). */
heightClassName?: string;
priority?: boolean;
alt?: string;
};
/**
* JRCC wordmark that swaps light/dark assets after hydration.
* Light: black bilingual classic. Dark: white + blue/gold mark.
*/
export function JrccLogo({
className,
heightClassName = 'h-11',
priority = false,
alt = 'JRCC — Jewish Russian Community Centre of Ontario',
}: JrccLogoProps) {
const { resolvedTheme } = useTheme();
const mounted = useMounted();
const isDark = mounted && resolvedTheme === 'dark';
const src = isDark ? '/brand/jrcc-logo-dark.png' : '/brand/jrcc-logo-light.png';
return (
<Image
src={src}
alt={alt}
width={420}
height={140}
className={cn(heightClassName, 'w-auto max-w-[min(100%,280px)] sm:max-w-[340px]', className)}
priority={priority}
/>
);
}
+5 -18
View File
@@ -1,10 +1,10 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import UserMenu from '@/components/UserMenu';
import { ActionButtons } from '@/components/ActionButtons';
import { ThemeToggle } from '@/components/ThemeToggle';
import { JrccLogo } from '@/components/JrccLogo';
interface PageHeaderProps {
photosCount: number;
@@ -35,18 +35,11 @@ export function PageHeader({
}: PageHeaderProps) {
return (
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-2 mb-3 border-b">
<div className="mb-2 flex items-center justify-between">
<Link href="/" aria-label="Home">
<Image
src="/logo.png"
alt="PunimTag"
width={300}
height={80}
className="h-11 w-auto cursor-pointer hover:opacity-80 transition-opacity"
priority
/>
<div className="mb-2 flex items-center justify-between gap-3">
<Link href="/" aria-label="JRCC home" className="min-w-0 shrink">
<JrccLogo priority className="cursor-pointer hover:opacity-90 transition-opacity" />
</Link>
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 shrink-0">
<ThemeToggle />
<UserMenu />
</div>
@@ -72,9 +65,3 @@ export function PageHeader({
</div>
);
}
+5 -18
View File
@@ -1,25 +1,18 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import UserMenu from '@/components/UserMenu';
import { ThemeToggle } from '@/components/ThemeToggle';
import { JrccLogo } from '@/components/JrccLogo';
export function SimpleHeader() {
return (
<div className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-2 mb-3 border-b">
<div className="mb-2 flex items-center justify-between">
<Link href="/" aria-label="Home">
<Image
src="/logo.png"
alt="PunimTag"
width={300}
height={80}
className="h-11 w-auto cursor-pointer hover:opacity-80 transition-opacity"
priority
/>
<div className="mb-2 flex items-center justify-between gap-3">
<Link href="/" aria-label="JRCC home" className="min-w-0 shrink">
<JrccLogo priority className="cursor-pointer hover:opacity-90 transition-opacity" />
</Link>
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 shrink-0">
<ThemeToggle />
<UserMenu />
</div>
@@ -30,9 +23,3 @@ export function SimpleHeader() {
</div>
);
}
+12
View File
@@ -0,0 +1,12 @@
# Brand assets (JRCC)
| File | Use |
|------|-----|
| `jrcc-logo-light.png` | Top-left header in **light** mode (classic B&W bilingual) |
| `jrcc-logo-dark.png` | Top-left header in **dark** mode (white + blue/gold) |
| `jrcc-watermark.png` | Diagonal guest watermark on photos |
| `../logo.png` | SSR / fallback copy of the light logo |
Source PDFs (not committed): `JRCC Logo - Rus-Eng.pdf`, `JRCC Logo 1 - Vector.pdf`.
Style picker (nine directions): `/styles`
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB