feat(brand): AV BEAT 2026 rebrand — blue/navy/white identity

- New AvBeatLogo inline-SVG component (rounded-square emblem with
  forward-leaning AV monogram + horizontal beam detail) at
  src/components/AvBeatLogo.tsx, three variants (full/wordmark/icon).
- Header.tsx now uses the responsive AV BEAT logo lockup in the
  top-left, links to /, full lockup on desktop (emblem+wordmark+tagline),
  emblem+wordmark on tablet, emblem-only on mobile.
- Removed DualSpeedTicker (the 'RECENT FORUM POSTS' ticker strip);
  the news ticker / glow chrome does not match the new aesthetic and the
  forum row was the explicit removal target.
- Tailwind theme + globals.css now expose the AV BEAT 2026 palette as
  semantic tokens: --brand-blue (#1D4ED8), --brand-blue-bright (#38BDF8),
  --brand-navy (#0F172A), --brand-bg (#F8FAFC), --brand-surface, --brand-border,
  --brand-text, --brand-text-muted. Legacy --color-* aliases re-point to the
  new tokens so any inline-styled component re-themes for free.
- Site-wide hex sweep migrates 2,769 hardcoded color literals across 144
  files from the old dark-broadcast palette to the new tokens (orange
  -> blue, dark-brown -> white surface / navy text, cream -> navy).
- Layout body class flipped from 'bb-neon' to 'bg-brand-bg text-brand-text'
  so the dark glow chrome no longer leaks through the new light theme.
This commit is contained in:
2026-06-03 12:07:18 +00:00
parent 39d2bf9073
commit 8042024c4a
146 changed files with 2219 additions and 2081 deletions

View File

@@ -31,14 +31,14 @@ interface Invitation {
type Tab = 'users' | 'invitations';
const ROLE_OPTIONS = [
{ value: 'reader', label: 'Reader', color: 'text-[#888] bg-[#231d18]' },
{ value: 'reader', label: 'Reader', color: 'text-[#888] bg-[#FFFFFF]' },
{ value: 'contributor', label: 'Contributor', color: 'text-blue-400 bg-blue-400/10' },
{ value: 'editor', label: 'Editor', color: 'text-purple-400 bg-purple-400/10' },
{ value: 'admin', label: 'Admin', color: 'text-red-400 bg-red-400/10' },
];
const ROLE_COLORS: Record<string, string> = {
reader: 'text-[#888] bg-[#231d18]',
reader: 'text-[#888] bg-[#FFFFFF]',
contributor: 'text-blue-400 bg-blue-400/10',
editor: 'text-purple-400 bg-purple-400/10',
admin: 'text-red-400 bg-red-400/10',
@@ -47,7 +47,7 @@ const ROLE_COLORS: Record<string, string> = {
const STATUS_COLORS: Record<string, string> = {
pending: 'text-yellow-400 bg-yellow-400/10',
accepted: 'text-green-400 bg-green-400/10',
revoked: 'text-[#555] bg-[#231d18]',
revoked: 'text-[#555] bg-[#FFFFFF]',
expired: 'text-orange-400 bg-orange-400/10',
email_failed: 'text-red-400 bg-red-400/10',
};
@@ -246,7 +246,7 @@ export default function AdminUsersPage() {
if (loading) {
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div>
);
}
@@ -276,7 +276,7 @@ export default function AdminUsersPage() {
</div>
<button
onClick={() => setShowInviteModal(true)}
className="flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors"
className="flex items-center gap-2 bg-[#1D4ED8] hover:bg-[#1E3A8A] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.41 2 2 0 0 1 3.6 1.22h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.96a16 16 0 0 0 6 6l.96-.96a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21.5 16.5z" />
@@ -306,7 +306,7 @@ export default function AdminUsersPage() {
onClick={() => setActiveTab(tab)}
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
activeTab === tab
? 'text-[#F0A623] border-[#F0A623]'
? 'text-[#1D4ED8] border-[#1D4ED8]'
: 'text-[#555] border-transparent hover:text-[#888]'
}`}
>
@@ -331,14 +331,14 @@ export default function AdminUsersPage() {
placeholder={activeTab === 'users' ? 'Search users...' : 'Search by email...'}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#1D4ED8]"
/>
</div>
{activeTab === 'users' && (
<select
value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)}
className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#F0A623]"
className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#1D4ED8]"
>
<option value="all">All Roles</option>
{ROLE_OPTIONS.map((r) => (
@@ -353,7 +353,7 @@ export default function AdminUsersPage() {
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div>
) : filteredUsers.length === 0 ? (
<div className="text-center py-16 text-[#555]">
@@ -379,7 +379,7 @@ export default function AdminUsersPage() {
<tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] font-bold text-sm flex-shrink-0 overflow-hidden">
{u.avatar_url ? (
<img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" />
) : (
@@ -393,7 +393,7 @@ export default function AdminUsersPage() {
</div>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[u.role] || 'text-[#888] bg-[#231d18]'}`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[u.role] || 'text-[#888] bg-[#FFFFFF]'}`}>
{u.role}
</span>
</td>
@@ -418,7 +418,7 @@ export default function AdminUsersPage() {
<button
onClick={() => openEditRole(u)}
disabled={processingIds.has(u.id)}
className="text-[#555] hover:text-[#F0A623] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50"
className="text-[#555] hover:text-[#1D4ED8] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50"
>
Edit Role
</button>
@@ -455,7 +455,7 @@ export default function AdminUsersPage() {
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#1D4ED8] border-t-transparent rounded-full animate-spin" />
</div>
) : filteredInvitations.length === 0 ? (
<div className="text-center py-16 text-[#555]">
@@ -465,7 +465,7 @@ export default function AdminUsersPage() {
<p className="text-sm">No invitations sent yet</p>
<button
onClick={() => setShowInviteModal(true)}
className="mt-3 text-[#F0A623] hover:text-[#F0A623] text-sm transition-colors"
className="mt-3 text-[#1D4ED8] hover:text-[#1D4ED8] text-sm transition-colors"
>
Send your first invitation
</button>
@@ -491,19 +491,19 @@ export default function AdminUsersPage() {
<tr key={inv.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] text-xs font-bold flex-shrink-0">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] text-xs font-bold flex-shrink-0">
{inv.email[0].toUpperCase()}
</div>
<span className="text-white text-sm">{inv.email}</span>
</div>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[inv.role] || 'text-[#888] bg-[#231d18]'}`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[inv.role] || 'text-[#888] bg-[#FFFFFF]'}`}>
{inv.role}
</span>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${STATUS_COLORS[displayStatus] || 'text-[#888] bg-[#231d18]'}`}>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${STATUS_COLORS[displayStatus] || 'text-[#888] bg-[#FFFFFF]'}`}>
{displayStatus.replace('_', ' ')}
</span>
</td>
@@ -564,7 +564,7 @@ export default function AdminUsersPage() {
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="contributor@example.com"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#1D4ED8]"
/>
</div>
<div>
@@ -574,7 +574,7 @@ export default function AdminUsersPage() {
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#1D4ED8]"
>
<option value="contributor">Contributor can submit articles for review</option>
<option value="editor">Editor can publish and manage articles</option>
@@ -590,7 +590,7 @@ export default function AdminUsersPage() {
onChange={(e) => setInviteMessage(e.target.value)}
placeholder="Add a personal note to the invitation email..."
rows={3}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623] resize-none"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#1D4ED8] resize-none"
/>
</div>
<div className="bg-[#0d1117] border border-[#1f2937] rounded-lg p-3">
@@ -609,7 +609,7 @@ export default function AdminUsersPage() {
<button
type="submit"
disabled={inviteSending || !inviteEmail.trim()}
className="flex-1 bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
className="flex-1 bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
>
{inviteSending ? (
<>
@@ -644,7 +644,7 @@ export default function AdminUsersPage() {
</div>
<div className="px-6 py-5 space-y-4">
<div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#1D4ED8] font-bold text-sm flex-shrink-0 overflow-hidden">
{editingUser.avatar_url ? (
<img src={editingUser.avatar_url} alt={editingUser.full_name || editingUser.email} className="w-full h-full object-cover" />
) : (
@@ -666,7 +666,7 @@ export default function AdminUsersPage() {
key={r.value}
className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
editRole === r.value
? 'border-[#F0A623] bg-[#F0A623]/10'
? 'border-[#1D4ED8] bg-[#1D4ED8]/10'
: 'border-[#1f2937] hover:border-[#2a3a50]'
}`}
>
@@ -679,9 +679,9 @@ export default function AdminUsersPage() {
className="sr-only"
/>
<span className={`w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${
editRole === r.value ? 'border-[#F0A623]' : 'border-[#444]'
editRole === r.value ? 'border-[#1D4ED8]' : 'border-[#444]'
}`}>
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#F0A623]" />}
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#1D4ED8]" />}
</span>
<span className={`text-xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded ${r.color}`}>
{r.label}
@@ -700,7 +700,7 @@ export default function AdminUsersPage() {
<button
onClick={handleEditSave}
disabled={editSaving || editRole === editingUser.role}
className="flex-1 bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
className="flex-1 bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
>
{editSaving ? (
<>