1/** Shared chrome for per-entity Open Graph images (ImageResponse cannot read CSS variables → token hex values inlined). */2export const INK = '#e9ebf0';3export const INK2 = '#a3a9b8';4export const INK3 = '#6f7688';5export const ACCENT = '#6d95ff';67function Mark({ px = 56 }: { px?: number }) {8 return (9 <svg width={px} height={px} viewBox="0 0 32 32" fill="none">10 <circle cx="16" cy="16" r="13" stroke={INK} strokeWidth="1.6" />11 <path d="M16 3c-4.4 3.2-6.6 7.6-6.6 13s2.2 9.8 6.6 13M16 3c4.4 3.2 6.6 7.6 6.6 13s-2.2 9.8-6.6 13M3 16h26" stroke={INK} strokeWidth="1.2" opacity="0.7" />12 <path d="M9.4 9.5L16 16l6.6-6.5M16 16l-5.2 8.4M16 16l7.4 5.6" stroke={INK} strokeWidth="1.2" opacity="0.55" />13 <circle cx="16" cy="16" r="2.4" fill={ACCENT} />14 <circle cx="9.4" cy="9.5" r="1.7" fill={INK} />15 <circle cx="22.6" cy="9.5" r="1.7" fill={INK} />16 <circle cx="10.8" cy="24.4" r="1.7" fill={INK} />17 <circle cx="23.4" cy="21.6" r="1.7" fill={INK} />18 </svg>19 );20}2122export function Frame({ children, footer }: { children: React.ReactNode; footer: string }) {23 return (24 <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', padding: '56px 72px', background: 'linear-gradient(160deg, #12151b 0%, #0b0d11 55%, #0b0d11 100%)', color: INK, fontFamily: 'sans-serif', position: 'relative' }}>25 <div style={{ position: 'absolute', right: -140, top: -160, width: 640, height: 640, borderRadius: 999, background: 'radial-gradient(circle at 40% 40%, rgba(109,149,255,0.18), rgba(11,13,17,0) 70%)', display: 'flex' }} />26 <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>27 <Mark />28 <div style={{ display: 'flex', fontSize: 32, fontWeight: 600, letterSpacing: -1 }}>29 <span>AI</span>30 <span style={{ color: INK2, marginLeft: 9, fontWeight: 500 }}>Atlas</span>31 </div>32 </div>33 {children}34 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', color: INK3, fontSize: 21 }}>35 <span>{footer}</span>36 <span>source-attributed · with history</span>37 </div>38 </div>39 );40}4142export function Eyebrow({ children, color = ACCENT }: { children: string; color?: string }) {43 return <div style={{ display: 'flex', fontSize: 18, letterSpacing: 3, textTransform: 'uppercase', color, fontWeight: 600 }}>{children}</div>;44}4546export function Title({ children }: { children: string }) {47 const long = children.length > 26;48 return (49 <div style={{ display: 'flex', fontSize: long ? 58 : 74, fontWeight: 600, lineHeight: 1.04, letterSpacing: -2.2, maxWidth: 1040, overflow: 'hidden', maxHeight: long ? 128 : 160 }}>50 <span style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis' }}>{children}</span>51 </div>52 );53}5455export function Facts({ items }: { items: [string, string][] }) {56 if (!items.length) return null;57 return (58 <div style={{ display: 'flex', gap: 52, marginTop: 10 }}>59 {items.map(([label, value]) => (60 <div key={label} style={{ display: 'flex', flexDirection: 'column' }}>61 <span style={{ fontSize: 15, letterSpacing: 2.5, textTransform: 'uppercase', color: INK3, fontWeight: 600 }}>{label}</span>62 <span style={{ fontSize: 34, fontWeight: 600, color: INK, letterSpacing: -1, marginTop: 6 }}>{value}</span>63 </div>64 ))}65 </div>66 );67}6869export function Fallback({ label }: { label: string }) {70 return (71 <Frame footer="www.ai-atlas.co">72 <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>73 <Eyebrow>{label}</Eyebrow>74 <Title>Explore the entire AI ecosystem.</Title>75 <div style={{ fontSize: 24, color: INK2, display: 'flex' }}>Models · companies · research · providers & pricing · benchmarks · hardware</div>76 </div>77 </Frame>78 );79}80