diff --git a/src/components/CouncilMark.astro b/src/components/CouncilMark.astro
new file mode 100644
index 0000000..17e1c68
--- /dev/null
+++ b/src/components/CouncilMark.astro
@@ -0,0 +1,145 @@
+---
+import type { UserPublic } from '../lib/db';
+import { getLitQuarters } from '../lib/db';
+
+interface Props {
+ member: UserPublic;
+ size?: 'sm' | 'md' | 'lg';
+}
+
+const { member, size = 'md' } = Astro.props;
+
+const SIZES = { sm: 48, md: 100, lg: 200 } as const;
+const px = SIZES[size];
+
+// 12 positions on a circle, i=0 = current quarter at 12 o'clock, going clockwise
+// into the past. A position is "lit" iff the member has a roadmap_attribution
+// to an item with shipped_at falling in that quarter. Pulse votes do NOT light.
+const lit = getLitQuarters(member.id);
+const litArr = [...lit].sort((a, b) => a - b);
+
+const CENTER = 60;
+const RADIUS = 42;
+function pointAt(i: number) {
+ const angle = -Math.PI / 2 + (i * Math.PI * 2) / 12;
+ return { x: CENTER + RADIUS * Math.cos(angle), y: CENTER + RADIUS * Math.sin(angle) };
+}
+
+const litPoints = litArr.map(pointAt);
+const polygonPoints = litPoints.length >= 3
+ ? litPoints.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ')
+ : null;
+
+function initials(name: string): string {
+ const parts = name.trim().split(/\s+/);
+ if (parts.length === 1) return (parts[0][0] ?? '').toUpperCase();
+ return ((parts[0][0] ?? '') + (parts[parts.length - 1][0] ?? '')).toUpperCase();
+}
+---
+
+
+