diff --git a/src/app/globals.css b/src/app/globals.css
index 447835d..6a8e0bf 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -43,6 +43,15 @@
--accent-lo: color-mix(in srgb, var(--accent), black 30%);
--accent-soft: color-mix(in srgb, var(--accent), transparent 88%);
+ /* HexAvatar initials: dark muted gradients, same in both themes. */
+ --hex-oxblood: linear-gradient(145deg, #6e1a22, #3a0d12);
+ --hex-navy: linear-gradient(145deg, #243a63, #121d36);
+ --hex-forest: linear-gradient(145deg, #25573f, #112a1f);
+ --hex-slate: linear-gradient(145deg, #4a5663, #262d35);
+ --hex-plum: linear-gradient(145deg, #55294f, #2a1328);
+ --hex-graphite: linear-gradient(145deg, #3d3d42, #1c1c20);
+ --avatar-ink: #F4F2EE;
+
/* Shape and motion. */
--r-panel: 18px;
--r-card: 16px;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b847dd7..c8b6545 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -4,6 +4,7 @@ import { Cormorant_Garamond, Manrope } from "next/font/google";
import { DEFAULT_THEME, THEME_SCRIPT } from "@/lib/theme";
import "./globals.css";
import "../styles/type.css";
+import "../styles/avatar.css";
import "../styles/legacy.css";
const manrope = Manrope({
diff --git a/src/components/ExpertSearch.tsx b/src/components/ExpertSearch.tsx
index eb7e30d..edfb1bf 100644
--- a/src/components/ExpertSearch.tsx
+++ b/src/components/ExpertSearch.tsx
@@ -1,11 +1,11 @@
"use client";
+import { HexAvatar } from "@/components/HexAvatar";
import { useEffect, useRef, useState } from "react";
import type { DiscoverCategory } from "@/lib/discover";
import { fetchDiscover } from "@/lib/discover-client";
import type { ExpertResult } from "@/lib/experts-search";
import { fetchExperts } from "@/lib/experts-search-client";
-import { initials } from "@/lib/feed-model";
export type SearchStatus = "idle" | "loading" | "ready" | "error";
@@ -61,12 +61,7 @@ export function ExpertSearchView(p: ExpertSearchViewProps) {
{p.results.map((e) => (
-
- {e.pictureUrl ? (
- // eslint-disable-next-line @next/next/no-img-element
-
- ) : (
- {initials(e.displayName)}
- )}
+
{e.displayName || "Expert"}
{e.byline ?
{e.byline} : null}
diff --git a/src/components/FeaturedExpertsRail.tsx b/src/components/FeaturedExpertsRail.tsx
index a057e91..0b97c2e 100644
--- a/src/components/FeaturedExpertsRail.tsx
+++ b/src/components/FeaturedExpertsRail.tsx
@@ -1,5 +1,5 @@
+import { HexAvatar } from "@/components/HexAvatar";
import type { DiscoverExpert } from "@/lib/discover";
-import { initials } from "@/lib/feed-model";
function Rating({ rating, reviews }: { rating: number | null; reviews: number | null }) {
if (rating === null && reviews === null) return null;
@@ -19,12 +19,7 @@ export function FeaturedExpertsRail({ experts }: { experts: DiscoverExpert[] })
{experts.map((e) => (
-
- {e.pictureUrl ? (
- // eslint-disable-next-line @next/next/no-img-element
-
- ) : (
- {initials(e.displayName)}
- )}
+
{e.displayName || "Expert"}
{e.byline ?
{e.byline} : null}
diff --git a/src/components/HexAvatar.tsx b/src/components/HexAvatar.tsx
new file mode 100644
index 0000000..7106516
--- /dev/null
+++ b/src/components/HexAvatar.tsx
@@ -0,0 +1,41 @@
+import { initials } from "@/lib/feed-model";
+
+export type HexAvatarSize = "lg" | "md" | "sm";
+
+/** Width by height in px for each spec size (ratio 0.866). */
+export const HEX_SIZES: Record
= {
+ lg: { width: 46, height: 53 },
+ md: { width: 38, height: 44 },
+ sm: { width: 30, height: 35 },
+};
+
+export const HEX_TONES = ["oxblood", "navy", "forest", "slate", "plum", "graphite"] as const;
+export type HexTone = (typeof HEX_TONES)[number];
+
+/** Same name always gives the same tone. */
+export function toneFor(name: string): HexTone {
+ let sum = 0;
+ for (const ch of name.trim().toLowerCase()) sum = (sum * 31 + ch.codePointAt(0)!) >>> 0;
+ return HEX_TONES[sum % HEX_TONES.length];
+}
+
+export interface HexAvatarProps {
+ name: string;
+ pictureUrl?: string | null;
+ size?: HexAvatarSize;
+}
+
+export function HexAvatar({ name, pictureUrl, size = "md" }: HexAvatarProps) {
+ const { width, height } = HEX_SIZES[size];
+ if (pictureUrl) {
+ return (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ );
+ }
+ return (
+
+ {initials(name)}
+
+ );
+}
diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx
index fb87ac7..d49d3a4 100644
--- a/src/components/PostCard.tsx
+++ b/src/components/PostCard.tsx
@@ -1,9 +1,10 @@
"use client";
import { useState } from "react";
+import { HexAvatar } from "@/components/HexAvatar";
import { CommentsSection } from "@/components/CommentsSection";
import { VideoPlayer } from "@/components/VideoPlayer";
-import { initials, relativeTime, type CardPost } from "@/lib/feed-model";
+import { relativeTime, type CardPost } from "@/lib/feed-model";
export interface PostCardProps {
post: CardPost;
@@ -36,14 +37,7 @@ export function PostCard({ post, now, onToggleLike, likeBusy, likeError, onSigne
return (