diff --git a/src/components/blocks/VoteWidget.astro b/src/components/blocks/VoteWidget.astro
new file mode 100644
index 0000000..a3b99ad
--- /dev/null
+++ b/src/components/blocks/VoteWidget.astro
@@ -0,0 +1,146 @@
+---
+// VoteWidget — the "which draft would you ship?" reader poll.
+//
+// Static markup only. The fetch to vote-service, the localStorage voter id,
+// and the click handler are task 16's job. The button toggle state lives
+// here so the layout matches the legacy page at first render.
+//
+// CSS hooks asserted by scripts/verify.mjs that must survive in the new
+// architecture:
+// .vote-widget — the outer wrapper
+// .vote-buttons — the button row
+// [aria-pressed=true] — the selected state
+//
+// The aria-label / role="group" on the inner cluster carries the state to
+// assistive tech — colour alone is not enough. This is asserted in the
+// task brief and lives in the same hook surface.
+
+interface VoteTally {
+ original: number;
+ improved: number;
+}
+
+interface Props {
+ /** Skill id this widget votes for. */
+ skillId: string;
+ /** Current vote tallies. Zeros render as "0 · 0%". */
+ tally: VoteTally;
+ /** The current visitor's vote, if any. */
+ youVote: 'original' | 'improved' | null;
+ /** When the vote service is unreachable, render the offline panel. */
+ unavailable?: boolean;
+}
+
+const { skillId, tally, youVote, unavailable = false } = Astro.props;
+const total = (tally.original || 0) + (tally.improved || 0);
+const share = (count) => (total ? Math.round((count / total) * 100) : 0);
+---
+
+{
+ unavailable ? (
+
+ ) : (
+
+ )
+}
+
+