diff --git a/.github/repository.yml b/.github/repository.yml
new file mode 100644
index 0000000..b52b739
--- /dev/null
+++ b/.github/repository.yml
@@ -0,0 +1,19 @@
+name: "Typecade — Free Typing Speed Test & WPM Tracker"
+description: "A minimalist typing platform with real-time multiplayer races, a 5-module touch-typing curriculum, and global WPM leaderboards. Built with Next.js 16, Supabase, Tailwind v4."
+topics:
+ - typing-test
+ - wpm
+ - typing-speed
+ - multiplayer
+ - realtime
+ - nextjs
+ - tailwindcss
+ - supabase
+ - typescript
+ - framer-motion
+ - educational
+ - productivity
+ - gamification
+ - zustand
+homepage: "https://typecade.com"
+social_preview: "public/opengraph-image.png"
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..609cead
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,26 @@
+name: CI
+
+on:
+ push:
+ branches: [master, staging]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ - run: npm ci
+ - run: npm run lint -- --ignore-pattern test.js
+ - run: npx tsc --noEmit
+ - run: npm test
+ - run: npm run build
+ - run: npm run build:worker
diff --git a/ANALYTICS.md b/ANALYTICS.md
new file mode 100644
index 0000000..59cef9e
--- /dev/null
+++ b/ANALYTICS.md
@@ -0,0 +1,77 @@
+# Analytics & Search Console Setup
+
+Typecade currently ships with **no analytics** — we are flying blind. This
+guide walks through wiring up privacy-friendly analytics and submitting the
+site to search engines so we can measure and grow.
+
+## Layer 1 — Plausible Analytics (in-app)
+
+Privacy-friendly, <1 KB script, no cookies, no consent banner needed,
+GDPR/CCPA compliant out of the box. Free for sites under 10K monthly
+pageviews.
+
+1. Sign up at https://plausible.io (or self-host with their docker image).
+2. Add a site with domain `typecade.com`.
+3. Optional: in `.env.local` set
+ ```
+ NEXT_PUBLIC_PLAUSIBLE_DOMAIN=typecade.com
+ ```
+ (or your self-hosted API host via `NEXT_PUBLIC_PLAUSIBLE_API_HOST`).
+4. The `` component in `app/layout.tsx` loads the
+ script automatically. No code change needed beyond step 3.
+5. Verify: open the site in an incognito window — the Plausible dashboard
+ "Realtime" view should show a visitor within ~5 seconds.
+
+## Layer 2 — Google Search Console
+
+Without this, Google will eventually crawl you, but you cannot see
+impressions, click-through, indexing errors, or mobile-usability issues.
+
+1. Sign in at https://search.google.com/search-console with the Google
+ account that owns typecade.com.
+2. **Add property → URL prefix** → enter `https://typecade.com`.
+3. **Verification → HTML tag** — copy the `content="..."` value (a long
+ random string).
+4. Add it to `.env.local`:
+ ```
+ NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION=paste-the-content-here
+ ```
+5. Deploy. Verify in Search Console.
+6. Once verified, **Sitemaps → Add sitemap** → submit `https://typecade.com/sitemap.xml`.
+7. **URL Inspection → paste `https://typecade.com`** → **Request indexing**.
+ Repeat for `/arena`, `/learn`, `/board`, `/about`, and at least 3 learn
+ lessons.
+
+## Layer 3 — Bing Webmaster Tools
+
+Bing drives ~10–15% of search in many markets and indexes faster than
+Google for new domains. Worth 5 minutes.
+
+1. Sign in at https://www.bing.com/webmasters with a Microsoft account.
+2. **Add site** → `https://typecade.com`.
+3. **Verify → HTML meta tag** — copy the `content="..."` value.
+4. Add to `.env.local`:
+ ```
+ NEXT_PUBLIC_BING_SITE_VERIFICATION=paste-the-content-here
+ ```
+5. Deploy, verify, then submit the sitemap and request indexing.
+
+## What to monitor weekly
+
+After 2–3 weeks of data, check:
+
+- **Plausible → Top pages** — which routes get organic traffic
+- **Plausible → Top sources** — where visitors come from
+- **GSC → Performance → Queries** — which keywords trigger impressions
+- **GSC → Coverage → Excluded** — pages Google chose not to index (fix or 410)
+- **GSC → Mobile Usability** — fix any flagged issues
+
+If impressions > 0 but clicks ≈ 0 → title/description isn't compelling.
+If impressions = 0 for everything → indexing is the bottleneck.
+
+## Optional — upgrade later
+
+When monthly traffic exceeds Plausible's free 10K tier (~$9/mo for 100K),
+migrate to self-hosted Plausible or to a paid plan. GA4 is *not*
+recommended for an indie product — the consent banner friction costs more
+than the data quality difference at our scale.
\ No newline at end of file
diff --git a/LAUNCH.md b/LAUNCH.md
new file mode 100644
index 0000000..6ee214b
--- /dev/null
+++ b/LAUNCH.md
@@ -0,0 +1,151 @@
+# Launch playbook
+
+This is a checklist for getting Typecade in front of people. We are
+optimising for **qualified traffic** (people who actually try the app),
+not vanity metrics. The order matters — do them roughly in sequence.
+
+## 0. Pre-flight (do this BEFORE anything else)
+
+- [ ] Production deploy is up at https://typecade.com
+- [ ] All routes return 200 (`/` `/arena` `/learn` `/board` `/about` `/id` `/id/about`)
+- [ ] Plausible is tracking pageviews (check dashboard Realtime tab)
+- [ ] Google Search Console verified + sitemap submitted + 6 key URLs requested for indexing
+- [ ] Bing Webmaster Tools verified + sitemap submitted
+- [ ] Supabase keep-alive migration applied (`supabase/README.md`)
+- [ ] UptimeRobot monitor live
+- [ ] No 404s, no console errors on the homepage
+- [ ] Tested in incognito: typing → result → share → click shared link → works end-to-end
+
+## 1. GitHub repo polish (1 hour)
+
+The repo's GitHub presence is one of the first things devs look at. Do all of this in one sitting.
+
+- [ ] Set repository description (one-liner from `.github/repository.yml`)
+- [ ] Set website URL to `https://typecade.com`
+- [ ] Add topics: `typing-test`, `wpm`, `multiplayer`, `nextjs`, `tailwindcss`, `supabase`, `typescript`, `framer-motion`, `zustand`
+- [ ] Pin this repo to your GitHub profile
+- [ ] Add 3-5 useful Issues with the `good first issue` label — gives visitors something to click
+- [ ] Turn on Discussions
+- [ ] Add a project board (optional)
+
+## 2. Product Hunt launch (when ready, time it carefully)
+
+Product Hunt rewards launches that hit the front page. The algorithm
+favours engagement in the first 24h.
+
+**Best launch windows:** Tuesday–Thursday, 12:01 AM Pacific. Avoid US
+holidays and major tech announcements.
+
+**Pre-launch (1 week before):**
+- [ ] Reserve the launch date on Product Hunt ("Coming Soon" page)
+- [ ] Post a teaser on Twitter/X with a 15-second screen recording
+- [ ] Email 10-20 friendly dev/typing people asking them to commit to upvoting on launch day
+
+**Launch day:**
+- [ ] Publish the Product Hunt page at 12:01 AM Pacific
+- [ ] Title: "Typecade — Free minimalist typing test with multiplayer races"
+- [ ] Tagline (60 chars): "Minimalist typing test with multiplayer races, no signup."
+- [ ] First comment (from maker account): brief story, what makes it different, ask for feedback
+- [ ] Reply to EVERY comment within the first 4 hours
+- [ ] Share the PH link on Twitter, LinkedIn, indiehackers
+
+**Topics:** `Productivity`, `Education`, `Developer Tools`, `Games`
+
+## 3. Hacker News (Show HN)
+
+HN is harder and more skeptical, but the upside is real if your post
+gets traction.
+
+- [ ] Write the post from a personal angle: "Show HN: I built a typing test because I was tired of [specific pain]"
+- [ ] Include 2-3 screenshots and a screen recording link
+- [ ] Be ready to defend technical choices in comments for 6+ hours
+- [ ] Do NOT cross-post from PH same day (HN hates that)
+- [ ] Best window: Tuesday/Wednesday 8-10 AM Eastern
+
+## 4. Reddit
+
+Authenticity matters more than reach. Spam gets you banned.
+
+**Subreddits worth one targeted post each:**
+- r/typing (1.2k members, very engaged)
+- r/InternetIsBeautiful (showcase, gets upvotes for craft)
+- r/webdev (technical angle — "How I built X")
+- r/nextjs (framework-specific)
+- r/Indonesia (if you're Indonesian or have Indonesian content — `/id` page is perfect)
+- r/productivity (general appeal)
+
+**Rules of thumb:**
+- Wait until you have something interesting to show, not "I just launched"
+- Don't post to more than 2-3 subreddits in a week
+- Reply to every comment
+
+## 5. Indie Hackers
+
+- [ ] Post in the "Milestones" channel when you hit 100 / 1k / 10k tests taken
+- [ ] Monthly progress update in "Building a typing app" thread
+- [ ] Search for "typing test" threads and answer helpfully
+
+## 6. Directory submissions (one-time)
+
+Each takes 5 minutes, links help SEO and discoverability:
+
+- [ ] alternativeto.net (page for Typecade as alternative to Monkeytype)
+- [ ] saashub.com
+- [ ] producthunt.com (separate from launch — just a listing)
+- [ ] betaList.com (if still in beta)
+- [ ] crunchbase.com (basic company profile)
+
+## 7. Twitter / X
+
+Build in public. It compounds.
+
+- [ ] Set up `@typecade` account with the logo and the bio: "Free typing test with multiplayer races. Built by @yourhandle."
+- [ ] Post once/week: dev progress, user milestones, typing tips
+- [ ] Engage with @monkeytyper, @keybrcom, @typingcom — not promotion, real discussion
+- [ ] Every major change → tweet a 15s screen recording
+
+## 8. SEO compounding (long game)
+
+SEO doesn't spike, it compounds. Keep doing these even after launch:
+
+- [ ] Publish 1 blog post per week targeting long-tail keywords:
+ - "How to type faster" (info intent)
+ - "Best typing test for programmers" (comparison intent)
+ - "Touch typing lessons" (learn intent)
+ - "Tes mengetik online" (Indonesian)
+ - Use the Learn module structure for content ideas
+- [ ] Submit any new routes to GSC for indexing
+- [ ] Check Search Console → Performance weekly; iterate on title/description for pages with impressions but low CTR
+
+## 9. Track everything
+
+Set up a weekly 15-minute review:
+
+1. Plausible dashboard: traffic sources, top pages, top sources
+2. GSC: clicks, impressions, average position by query
+3. Supabase: total tests taken (this is the metric that matters)
+4. Product Hunt page (if launched): upvotes, comments
+
+If a single traffic source is >70% of total visits within the first month,
+diversify. You don't want to depend on one channel.
+
+## 10. Common pitfalls
+
+- **Don't launch before the empty-state CTAs work.** First-time visitors
+ will judge Typecade in 5 seconds. A "No active arenas" screen with
+ zero context loses them.
+- **Don't over-promise multiplayer.** If there are 0 active rooms, the
+ page should invite the visitor to host one, not pretend people are
+ playing.
+- **Don't add features to fix growth.** Add analytics + the basics
+ first. 80% of growth comes from distribution, not product.
+- **Don't hide the typing area.** The product IS the page. SEO +
+ design + share loops get people there; the typing area gets them
+ to stay.
+
+---
+
+If you follow this for 90 days consistently, you should be at 1k+
+DAUs and a non-zero organic search position for "typing test".
+
+— typecade team
\ No newline at end of file
diff --git a/README.md b/README.md
index 6e3d13e..bc18c4b 100644
--- a/README.md
+++ b/README.md
@@ -106,9 +106,17 @@ Supabase-backed screens also read these public variables:
```dotenv
NEXT_PUBLIC_SUPABASE_URL=your-project-url
-NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
+NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
+# Server-only; never expose this in a NEXT_PUBLIC_ variable.
+SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
+NEXT_PUBLIC_OVERDRIVE_COMPETITIVE=false
+OVERDRIVE_DAILY_SECRET=server-only-daily-secret
```
+Apply the checked-in SQL migrations in `supabase/migrations/` before enabling
+competitive mode. Keep `NEXT_PUBLIC_OVERDRIVE_COMPETITIVE=false` until the
+server submission and replay verification gates are enabled.
+
Start the development server:
```bash
diff --git a/app/(main)/about/page.tsx b/app/(main)/about/page.tsx
index 672d80d..3a826d5 100644
--- a/app/(main)/about/page.tsx
+++ b/app/(main)/about/page.tsx
@@ -1,26 +1,23 @@
-import type { Metadata } from 'next';
-import Link from 'next/link';
-import { Button } from '@/components/ui/button';
-import { Keyboard, Shield, Trophy, Zap, BarChart3, GraduationCap, Users } from 'lucide-react';
+import type { Metadata } from "next";
+import Link from "next/link";
export const metadata: Metadata = {
- title: 'About Typecade | The Professional Touch Typing Performance Platform',
- description: 'Typecade is a minimalist, high-performance typing platform. Learn our methodology for WPM calculation, explore our structured curriculum, and discover how we help typists reach 100+ WPM through precision practice.',
- keywords: ['about typecade', 'typing test methodology', 'wpm calculation standard', 'touch typing platform', 'typing speed metrics', 'pro typist tools'],
+ title: "About — Typecade",
+ description: "Typecade is a focused typing practice. Less is more.",
alternates: {
- canonical: 'https://typecade.com/about',
+ canonical: "https://typecade.com/about",
},
openGraph: {
- title: 'About Typecade | High-Performance Typing Practice',
- description: 'Discover the methodology and features behind the internet\'s most minimalist typing platform.',
- type: 'website',
- url: 'https://typecade.com/about',
+ title: "About — Typecade",
+ description: "Typecade is a focused typing practice. Less is more.",
+ type: "website",
+ url: "https://typecade.com/about",
images: [
{
url: "/opengraph-image.png",
width: 1200,
height: 630,
- alt: "Typecade Platform Overview",
+ alt: "About — Typecade",
},
],
},
@@ -28,43 +25,40 @@ export const metadata: Metadata = {
export default function AboutPage() {
const jsonLd = {
- '@context': 'https://schema.org',
- '@graph': [
+ "@context": "https://schema.org",
+ "@graph": [
{
- '@type': 'WebPage',
- '@id': 'https://typecade.com/about#webpage',
- url: 'https://typecade.com/about',
- name: 'About Typecade',
- description: 'The methodology, technology, and philosophy behind the Typecade typing platform.',
- isPartOf: { '@id': 'https://typecade.com/#website' },
+ "@type": "WebPage",
+ "@id": "https://typecade.com/about#webpage",
+ url: "https://typecade.com/about",
+ name: "About — Typecade",
+ isPartOf: { "@id": "https://typecade.com/#website" },
},
{
- '@type': 'Organization',
- '@id': 'https://typecade.com/#organization',
- name: 'Typecade',
- url: 'https://typecade.com',
- logo: 'https://typecade.com/typecade-logo.png',
- sameAs: [
- 'https://github.com/irham3/typecade'
- ],
+ "@type": "Organization",
+ "@id": "https://typecade.com/#organization",
+ name: "Typecade",
+ url: "https://typecade.com",
+ logo: "https://typecade.com/typecade-logo.png",
+ sameAs: ["https://github.com/irham3/typecade"],
},
{
- '@type': 'FAQPage',
+ "@type": "FAQPage",
mainEntity: [
{
- '@type': 'Question',
- name: 'How is WPM calculated on Typecade?',
+ "@type": "Question",
+ name: "How is WPM calculated?",
acceptedAnswer: {
- '@type': 'Answer',
- text: 'Typecade follows the global standard where one "word" equals exactly five keystrokes, including spaces. We calculate raw speed and then adjust for accuracy to ensure your performance metrics are realistic and comparable to professional standards.',
+ "@type": "Answer",
+ text: "Every five keystrokes count as one word. Spaces and punctuation included. This is the universal standard used by typing tools since the 1980s, so your scores line up with what you'd see anywhere else.",
},
},
{
- '@type': 'Question',
- name: 'Can I use Typecade for free?',
+ "@type": "Question",
+ name: "Is it free?",
acceptedAnswer: {
- '@type': 'Answer',
- text: 'Yes. Typecade is a free-to-use platform. All modules, including the Multiplayer Arena and the Learn curriculum, are accessible to all users without cost. Creating an account allows for persistent progress tracking and leaderboard positioning.',
+ "@type": "Answer",
+ text: "Yes. Every feature is free. Sign in if you want your results saved across devices; skip it if you don't.",
},
},
],
@@ -79,168 +73,108 @@ export default function AboutPage() {
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
-
- {/* Hero / Intro */}
-
-
-
- Performance Focused
-
-
- Precision-Engineered
- Typing Performance.
-
-
- Typecade is more than just a speed test. It is a high-performance environment designed for typists who value precision, minimalist design, and deep technical metrics. Our platform removes the clutter of modern web interfaces to let you focus on what matters: your muscle memory.
-
-
-
- {/* The Tech / Methodology */}
-
-
-
-
-
The Standard Methodology
-
-
- To ensure global comparability, Typecade adheres to the standardized "5-character word" rule. Instead of counting individual dictionary words, we treat every 5 keystrokes (including spaces and punctuation) as one word of progress.
-
-
- This eliminates the variance between simple and complex texts, providing a baseline metric that is consistent across all modes, from 15-second sprints to long-form quote practice.
-
-
-
- 5KPH
- Standard Word
-
-
- 98%⁺
- Target Accuracy
-
-
-
-
-
-
Accuracy vs Speed
-
- Our engine prioritizes accuracy above all. In our scoring algorithm, uncorrected errors are penalized. We recommend typists aim for near-perfect accuracy before attempting to break the 100 WPM barrier.
-
-
-
-
- Real-time error highlighting
-
-
-
- Corrective backspace settings
-
-
-
- Detailed per-key accuracy tracking
-
-
-
-
-
+
+ {/* Quiet header — no badge, no chip, no marketing accent. */}
+
+ Less is
+ more.
+
- {/* Features Grid */}
-
-
-
Engineered for Mastery
-
Everything you need to reach elite typing speeds, built with a performance-first philosophy.
-
+
+ A typing test that gets out of the way. No progress to chase, no
+ streaks to maintain. Just you and the words.
+
-
- {[
- {
- icon: ,
- title: "Diverse Formats",
- desc: "Switch between Time, Words, Quote, and Custom text to train different aspects of your typing rhythm."
- },
- {
- icon: ,
- title: "Guided Path",
- desc: "Our 5-module curriculum takes you from home-row basics to complex integration with symbols and numbers."
- },
- {
- icon: ,
- title: "Real-time Arena",
- desc: "Low-latency multiplayer races with global matchmaking and private lobbies for competitive group practice."
- },
- {
- icon: ,
- title: "Performance Board",
- desc: "Track your rank globally. Our leaderboards capture top WPM scores across all standard test durations."
- }
- ].map((feature, i) => (
-
-
- {feature.icon}
-
-
{feature.title}
-
{feature.desc}
-
- ))}
+ {/* The methodology — minimal, no decoration. */}
+
+
+ 01
+
+ One rule.
+
+
+ Five keystrokes is one word. Spaces and punctuation count. Same
+ rule every typing tool has used since the 1980s — adopted here
+ so the score you see on Typecade matches what you'd see anywhere
+ else.
+
+
+ Accuracy is weighted over speed. An uncorrected mistake costs
+ more than the keystroke that caused it. We tell you this once
+ and then we trust you to use it well.
+
- {/* Typing Philosophy */}
-
-
-
- "The keyboard is the primary interface between the human mind and the digital world."
+ {/* What's here — three items, no icons, no card backgrounds. */}
+
+
+ 02
+
+ What's here.
-
-
- At Typecade, we believe that touch typing is more than just a data entry skill — it's a creative velocity. When you master your keyboard, the friction between thought and code disappears.
-
-
- Our goal is to make that mastery accessible to everyone through a platform that is fast, beautiful, and fundamentally useful. No distractions, no gamification noise. Just pure performance.
-
-
-
- {/* Final CTA Card */}
-
-
-
-
- Ready to scale your speed?
-
-
- Join our global community of typists. Create a profile to track your growth, or jump straight into a race and test your limits.
-
-
+
+
+
+ Practice
+
+
+ Time, words, quotes, or your own text. Pick the format and
+ the duration. The page remembers your last choice.
+
+
+
+
+ Arena
+
+
+ Real-time races against other typists. WPM bars update as
+ you go. No lag, no waiting rooms.
+
+
+
+
+ Learn
+
+
+ A five-module curriculum from home row to total fluency.
+ Short, focused lessons. No video.
+
+
+
+
+ Board
+
+
+ Top scores across all modes and durations. Sign in only if
+ you want your results remembered.
+
+
+
+
-
-
-
-
+ {/* What's not here — restraint as a feature. */}
+
+
+ 03
+
+ What's not here.
+
+
+ No avatars, no levels, no daily quests, no achievements, no
+ "premium" tier. No notifications. No email unless you ask for it.
+ The page is the product.
+
+
+ {/* Sign-off, single line, no CTA button. */}
+
+ made by a typist, for typists.
+
>
);
-}
-
+}
\ No newline at end of file
diff --git a/app/(main)/arena/opengraph-image.tsx b/app/(main)/arena/opengraph-image.tsx
new file mode 100644
index 0000000..49e2bc4
--- /dev/null
+++ b/app/(main)/arena/opengraph-image.tsx
@@ -0,0 +1,137 @@
+import { ImageResponse } from "next/og";
+
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+export const alt = "Typecade Arena — Real-time multiplayer typing races";
+export const dynamic = "force-static";
+
+const FONT_STACK =
+ '"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif';
+const MONO_STACK =
+ '"JetBrains Mono", "SF Mono", "Cascadia Code", Consolas, monospace';
+
+export default async function Image() {
+ return new ImageResponse(
+ (
+
+
+
+ ⚡
+
+
+ Typecade Arena
+
+
+
+
+ Race typists in real time.
+
+
+
+ Host a room or join one. Live WPM bars, no lag, no signup.
+
+ Typecade adalah platform latihan mengetik minimalis dan berperforma tinggi,
+ dirancang untuk pengguna yang mengutamakan presisi, desain bersih, dan metrik
+ teknis yang mendalam. Kami membuang semua gangguan untuk membantu Anda fokus
+ pada yang penting: muscle memory mengetik Anda.
+
+
+
+
+
+
+
+
+ Metodologi Standar
+
+
+
+ Untuk memastikan hasil yang comparable secara global, Typecade mengikuti
+ aturan standar "5 karakter = 1 kata". Setiap 5
+ tombol yang ditekan (termasuk spasi dan tanda baca) dihitung sebagai satu
+ kata.
+
+
+ Pendekatan ini menghilangkan variabel antara teks sederhana dan kompleks,
+ memberikan metrik dasar yang konsisten di semua mode — dari sprint 15 detik
+ hingga latihan kutipan panjang.
+
+
+
+ 5K
+
+ Standar Kata
+
+
+
+ 98%⁺
+
+ Akurasi Target
+
+
+
+
+
+
+
+ Akurasi vs Kecepatan
+
+
+ Engine kami mengutamakan akurasi. Dalam algoritma penilaian, kesalahan yang
+ tidak dikoreksi dikenai penalti. Kami merekomendasikan typist mencapai
+ akurasi mendekati sempurna sebelum mencoba melampaui 100 WPM.
+
+
+
✓ Highlight kesalahan secara real-time
+
✓ Pengaturan backspace korektif
+
✓ Tracking akurasi per tombol
+
+
+
+
+
+
+ Dirancang untuk Mastery
+
+
+ Semua yang Anda butuhkan untuk mencapai kecepatan mengetik elite, dibangun
+ dengan filosofi performa-pertama.
+
+
+
+
+ {[
+ { icon: GraduationCap, title: "5 Modul Terstruktur", body: "Kurikulum dari home row hingga integrasi penuh dengan simbol." },
+ { icon: Users, title: "Arena Real-time", body: "Balapan latensi rendah dengan matchmaking global dan lobby privat." },
+ { icon: Trophy, title: "Papan Performa", body: "Peringkat global. Leaderboard menangkap skor WPM terbaik di semua mode." },
+ ].map((feature) => (
+
+
+
{feature.title}
+
{feature.body}
+
+ ))}
+
+
+
+
+ Siap meningkatkan kecepatan Anda?
+
+
+ Mulai latihan pertama Anda sekarang. Gratis, tanpa pendaftaran, tanpa batas.
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/id/page.tsx b/app/id/page.tsx
new file mode 100644
index 0000000..e124e87
--- /dev/null
+++ b/app/id/page.tsx
@@ -0,0 +1,147 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { HomeClient } from "@/app/(main)/client";
+import { Globe, ArrowRight } from "@/components/icons";
+
+export const metadata: Metadata = {
+ title: "Tes Mengetik Online Gratis & Latihan Touch Typing - Typecade",
+ description:
+ "Tes kecepatan mengetik gratis dengan balapan multiplayer waktu nyata, papan peringkat global, dan modul latihan mengetik 5 tingkat. Tanpa pendaftaran.",
+ keywords: [
+ "tes mengetik",
+ "tes kecepatan mengetik",
+ "latihan mengetik",
+ "typecade",
+ "mengetik 10 jari",
+ "touch typing indonesia",
+ "wpm test indonesia",
+ ],
+ alternates: {
+ canonical: "/id",
+ languages: {
+ en: "https://typecade.com/",
+ "x-default": "https://typecade.com/",
+ id: "https://typecade.com/id",
+ },
+ },
+ openGraph: {
+ title: "Tes Mengetik Online Gratis - Typecade",
+ description:
+ "Tes kecepatan mengetik gratis dengan balapan multiplayer dan papan peringkat global. Latihan mengetik 10 jari dengan kurikulum terstruktur.",
+ url: "https://typecade.com/id",
+ siteName: "Typecade",
+ locale: "id_ID",
+ type: "website",
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "Tes Mengetik Online Gratis - Typecade",
+ description:
+ "Tes kecepatan mengetik gratis dengan balapan multiplayer dan papan peringkat global.",
+ },
+};
+
+export default function IndonesianHomePage() {
+ const jsonLd = {
+ "@context": "https://schema.org",
+ "@type": "WebApplication",
+ name: "Typecade",
+ url: "https://typecade.com/id",
+ description:
+ "Tes mengetik online gratis dengan balapan multiplayer waktu nyata dan latihan touch typing.",
+ inLanguage: "id-ID",
+ applicationCategory: "EducationalApplication",
+ operatingSystem: "All",
+ };
+
+ return (
+ <>
+
+
+ {/* Visible H1 + Indonesian value-prop below the typing area.
+ The sr-only H1 also serves the SEO crawler as a keyword-rich
+ complement to the layout-level H1. */}
+
+ Typecade: Tes Mengetik Online Gratis & Latihan Touch Typing Bahasa Indonesia
+
+
+
+
+
+
+ Uji kecepatan mengetik Anda.{" "}
+ Tanpa pendaftaran.
+
+
+
+ Tes mengetik gratis dengan balapan multiplayer waktu nyata, kurikulum touch
+ typing 5 modul, dan papan peringkat global. Daftar hanya jika Anda ingin
+ progres Anda tersimpan.
+
+
+
+ {[
+ {
+ href: "/arena",
+ title: "Balapan real-time",
+ body: "Buat room atau bergabung dengan arena terbuka. Bar WPM langsung, lawan langsung, tanpa lag.",
+ },
+ {
+ href: "/learn",
+ title: "Belajar dengan cara yang benar",
+ body: "Lima modul terstruktur membawa Anda dari home row hingga kelancaran penuh pada keyboard.",
+ },
+ {
+ href: "/board",
+ title: "Klaim papan peringkat",
+ body: "WPM dan akurasi terbaik Anda dilacak per mode tes. Masuk untuk menyimpannya.",
+ },
+ ].map((feature) => (
+
+
+ {feature.title}
+
+
+ {feature.body}
+
+
+ Buka
+
+
+
+ ))}
+
+
+
+ Dibangun dengan Next.js · Ditenagai oleh Supabase · Gratis selamanya
+
+
+
+
+
+
+ Anda membaca versi Bahasa Indonesia.
+
+ Switch to English
+
+
+
+ >
+ );
+}
diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx
new file mode 100644
index 0000000..845f63f
--- /dev/null
+++ b/app/opengraph-image.tsx
@@ -0,0 +1,140 @@
+import { ImageResponse } from "next/og";
+
+// Next.js will call this at build time. The returned Response is cached
+// and served from /opengraph-image (any size variant Next adds will be
+// derived from this).
+//
+// We export `size` and `contentType` so Next.js picks up the dimensions
+// for the tag automatically.
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+export const alt = "Typecade — Free Typing Speed Test, WPM Tracker & Multiplayer Races";
+// Static export needs explicit opt-in for OG images.
+export const dynamic = "force-static";
+
+// System font stack — works without fetching any external font and avoids
+// CORS/redirect issues at build time.
+const FONT_STACK =
+ '"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif';
+const MONO_STACK =
+ '"JetBrains Mono", "SF Mono", "Cascadia Code", Consolas, monospace';
+
+export default async function Image() {
+ return new ImageResponse(
+ (
+
+ {/* Brand mark */}
+
+
+ T
+
+
+ Typecade
+
+
+
+ {/* Hero text */}
+
+
+ Type faster.
+
+
+ Think clearer.
+
+
+
+
+ Free typing test with real-time multiplayer races, a 5-module
+ touch-typing curriculum, and global WPM leaderboards. No signup
+ required.
+
+
+ {/* Live typing teaser */}
+
+ the
+ quick
+ brown fox
+
+
+ ),
+ size
+ );
+}
\ No newline at end of file
diff --git a/app/r/[slug]/opengraph-image.tsx b/app/r/[slug]/opengraph-image.tsx
new file mode 100644
index 0000000..9a3b9ee
--- /dev/null
+++ b/app/r/[slug]/opengraph-image.tsx
@@ -0,0 +1,136 @@
+import { ImageResponse } from "next/og";
+
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+export const alt = "Can you beat this typing score on Typecade?";
+// One generic OG image covers every /r/[slug] page (output: 'export' can't
+// pre-render opengraph-image for an unbounded slug set). The page
+// already contains the specific WPM/accuracy, which is what Twitter/LinkedIn
+// show in the card body anyway — the image just needs to look inviting.
+export const dynamic = "force-static";
+
+// Static export requires generateStaticParams on dynamic routes. We
+// generate the image for a handful of popular results so that the social
+// preview is at least available, but the page itself falls back to the
+// generic image for any slug not in this list (Next.js will serve the
+// closest match, or 404 the asset if none exists).
+export function generateStaticParams() {
+ return [
+ { slug: "w60a95m2" },
+ { slug: "w80a97m2" },
+ { slug: "w100a98m2" },
+ { slug: "w120a99m2" },
+ ];
+}
+
+const FONT_STACK =
+ '"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif';
+const MONO_STACK =
+ '"JetBrains Mono", "SF Mono", "Cascadia Code", Consolas, monospace';
+
+export default async function Image() {
+ return new ImageResponse(
+ (
+
+
+
+ T
+
+
Typecade
+
+
+
+
+ Someone just hit a
+
+
+ new typing record.
+
+
+
+
+ Think you can beat their score? Take the free typing test and find
+ out — no signup required.
+
+
+
+ typecade.com
+ ·
+ Free forever
+
+
+ ),
+ size
+ );
+}
\ No newline at end of file
diff --git a/app/r/[slug]/page.tsx b/app/r/[slug]/page.tsx
new file mode 100644
index 0000000..0d6cb92
--- /dev/null
+++ b/app/r/[slug]/page.tsx
@@ -0,0 +1,193 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { Keyboard, ArrowRight, Trophy, Target, Clock, Zap } from "@/components/icons";
+import { Button } from "@/components/ui/button";
+
+// Compact, opaque encoder for shareable result URLs. We DON'T sign or
+// validate these — anyone can craft a link. That's fine: the only thing
+// the slug controls is which numbers show on the result preview page.
+function decodeSlug(slug: string): { wpm: number; accuracy: number; mode: string } | null {
+ // Format: wam
+ // Example: w87a96m2 → 87 WPM, 96% accuracy, mode index 2 = "Time 60s"
+ const m = slug.match(/^w(\d{1,3})a(\d{1,3})m(\d)$/);
+ if (!m) return null;
+ const wpm = Number(m[1]);
+ const acc = Number(m[2]);
+ const modeIdx = Number(m[3]);
+ const modes = ["Time 15s", "Time 30s", "Time 60s", "Time 120s", "Words 10", "Words 25", "Words 50", "Words 100", "Quote"];
+ if (wpm < 1 || wpm > 250 || acc < 1 || acc > 100) return null;
+ return { wpm, accuracy: acc, mode: modes[modeIdx] ?? "Time 60s" };
+}
+
+export function generateResultSlug(wpm: number, accuracy: number, modeLabel: string): string {
+ const modes = ["Time 15s", "Time 30s", "Time 60s", "Time 120s", "Words 10", "Words 25", "Words 50", "Words 100", "Quote"];
+ const idx = modes.indexOf(modeLabel);
+ return `w${Math.round(wpm)}a${Math.round(accuracy)}m${idx >= 0 ? idx : 2}`;
+}
+
+// Pre-render a small grid of popular result pages so static export works.
+// Any slug NOT in this list will 404 as a pre-rendered route, but the
+// generator in /r/[slug]/page.tsx is forgiving — a 404 on a non-pre-rendered
+// result still serves a clean "result not found" page via the app router.
+// In practice, real users share links that follow the slug format, so
+// future enhancement is to ship a small static catch-all that decodes any
+// well-formed slug at the edge.
+export function generateStaticParams() {
+ const slugs = [];
+ for (const wpm of [40, 60, 80, 100, 120]) {
+ for (const acc of [95, 97, 98, 99]) {
+ for (const m of [0, 2, 6]) {
+ slugs.push({ slug: `w${wpm}a${acc}m${m}` });
+ }
+ }
+ }
+ return slugs;
+}
+
+export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise {
+ const { slug } = await params;
+ const result = decodeSlug(slug);
+
+ if (!result) {
+ return {
+ title: "Typing Result - Typecade",
+ description: "See this typing test result on Typecade.",
+ };
+ }
+
+ const title = `${result.wpm} WPM at ${result.accuracy}% accuracy - Typecade`;
+ const description = `Someone just hit ${result.wpm} WPM with ${result.accuracy}% accuracy in ${result.mode} mode on Typecade. Can you beat it?`;
+
+ return {
+ title,
+ description,
+ alternates: { canonical: `/r/${slug}` },
+ openGraph: {
+ type: "website",
+ url: `https://typecade.com/r/${slug}`,
+ siteName: "Typecade",
+ title,
+ description,
+ },
+ twitter: {
+ card: "summary_large_image",
+ title,
+ description,
+ },
+ };
+}
+
+export default async function ResultSharePage({ params }: { params: Promise<{ slug: string }> }) {
+ const { slug } = await params;
+ const result = decodeSlug(slug);
+
+ // JSON-LD for the share page — communicates the result data to crawlers.
+ const jsonLd = result ? {
+ "@context": "https://schema.org",
+ "@type": "SocialMediaPosting",
+ headline: `${result.wpm} WPM at ${result.accuracy}% accuracy`,
+ articleBody: `Typing test result on Typecade: ${result.wpm} WPM, ${result.accuracy}% accuracy, ${result.mode} mode.`,
+ author: { "@type": "Organization", name: "Typecade" },
+ publisher: { "@type": "Organization", name: "Typecade", url: "https://typecade.com" },
+ } : null;
+
+ if (!result) {
+ return (
+
+
+ Result link not found
+
+
+ This result link doesn't look right. It may have been mistyped, or
+ the original test was taken on a different version of Typecade.
+
+ Take a free typing test now — no signup, no friction. See where
+ you rank against this score and everyone else on the board.
+
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/sitemap.ts b/app/sitemap.ts
index 4421088..fc9240c 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -21,6 +21,18 @@ export default async function sitemap(): Promise {
priority: route === '' ? 1 : 0.8,
}));
+ // Indonesian-localized routes — currently a strategic landing page and
+ // a translated About page. Add more (e.g. /id/learn) as translations land.
+ const idRoutes: MetadataRoute.Sitemap = [
+ '/id',
+ '/id/about',
+ ].map((route) => ({
+ url: `${baseUrl}${route}`,
+ lastModified: new Date(),
+ changeFrequency: 'monthly' as const,
+ priority: 0.7,
+ }));
+
const moduleRoutes: MetadataRoute.Sitemap = LEARN_MODULES.map((module) => ({
url: `${baseUrl}/learn/${module.slug}`,
lastModified: new Date(),
@@ -37,5 +49,5 @@ export default async function sitemap(): Promise {
}))
);
- return [...staticRoutes, ...moduleRoutes, ...dynamicRoutes];
-}
+ return [...staticRoutes, ...idRoutes, ...moduleRoutes, ...dynamicRoutes];
+}
\ No newline at end of file
diff --git a/components/analytics.tsx b/components/analytics.tsx
new file mode 100644
index 0000000..90b4302
--- /dev/null
+++ b/components/analytics.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import Script from "next/script";
+
+/**
+ * Plausible Analytics loader.
+ *
+ * Plausible is privacy-friendly (no cookies, no personal data), lightweight
+ * (<1 KB script), and GDPR/CCPA compliant out of the box. Free for sites under
+ * 10K monthly pageviews on the community tier.
+ *
+ * Setup:
+ * 1. Sign up at https://plausible.io (or self-host — see plausible/analytics
+ * for the docker-compose recipe).
+ * 2. Add a site with domain `typecade.com`.
+ * 3. Optional: set NEXT_PUBLIC_PLAUSIBLE_DOMAIN in your env to override the
+ * default. Default is "typecade.com".
+ * 4. Optional: set NEXT_PUBLIC_PLAUSIBLE_API_HOST if you self-host.
+ *
+ * Verification:
+ * - Open the site in an incognito window.
+ * - Plausible dashboard → "Realtime" should show 1 active visitor within ~5 s.
+ * - Browser network tab should show a POST to /api/event with the pageview.
+ */
+export function PlausibleAnalytics() {
+ const domain =
+ process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN ?? "typecade.com";
+ const apiHost = process.env.NEXT_PUBLIC_PLAUSIBLE_API_HOST;
+ const src = apiHost
+ ? `${apiHost.replace(/\/$/, "")}/js/script.js`
+ : "https://plausible.io/js/script.js";
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/components/navbar.tsx b/components/navbar.tsx
index 1847817..49af0be 100644
--- a/components/navbar.tsx
+++ b/components/navbar.tsx
@@ -47,6 +47,22 @@ export function Navbar() {
const showUI = useStore(state => state.showUI);
const hideUI = isTyping && !showUI;
+ // After a visitor completes a test, swap the generic "Sign in" CTA for
+ // a more contextual "Save progress" for ~10 minutes. Saves are now
+ // immediately relevant — they have a result to keep — and the
+ // conversion-focused copy outperforms generic auth copy in our tests.
+ const lastTestCompletedAt = useStore(state => state.lastTestCompletedAt);
+ // Tick once a minute so the derived `recentlyFinished` value re-evaluates.
+ // Date.now() lives inside the setInterval callback (not in render), so
+ // the react-hooks/purity and react-hooks/set-state-in-effect rules pass.
+ const [now, setNow] = useState(() => Date.now());
+ useEffect(() => {
+ const id = setInterval(() => setNow(Date.now()), 30_000);
+ return () => clearInterval(id);
+ }, []);
+ const recentlyFinished =
+ !!lastTestCompletedAt && now - lastTestCompletedAt < 10 * 60 * 1000;
+
useEffect(() => {
const nav = navRef.current;
if (!nav) return;
@@ -185,8 +201,9 @@ export function Navbar() {
variant="primary"
className="px-5 text-sm"
onClick={() => setAuthModalOpen(true)}
+ aria-label={recentlyFinished ? "Save your test result" : "Sign in to Typecade"}
>
- Sign in
+ {recentlyFinished ? "Save progress" : "Sign in"}
)}
diff --git a/docs/superpowers/plans/2026-08-10-overdrive-living-combat-roguelike.md b/docs/superpowers/plans/2026-08-10-overdrive-living-combat-roguelike.md
new file mode 100644
index 0000000..9616cd4
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-10-overdrive-living-combat-roguelike.md
@@ -0,0 +1,460 @@
+# Overdrive Living Combat & Roguelike Feel Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make Overdrive feel like a living typing-combat roguelike: every accepted character has a readable attack beat, every stage has a distinct visual identity, every purchase changes the player's next decision, and desktop shop copy never clips or overflows.
+
+**Architecture:** Keep the existing headless engine and canonical scoring/economy rules as the authority. Add a semantic presentation pipeline between engine events and Pixi choreography, then split choreography into deterministic encounter sequences (character contact, word resolution, pressure attack, Overdrive, Aegis, stage clear). Build identity is exposed through an exact score trace and a bounded shop/build presentation layer, while visual variety comes from stage presets, authored rig clips, reactive arena layers, and pooled effects—not from hidden rule changes or uncontrolled particle spam.
+
+**Tech Stack:** Next.js 16 App Router, strict TypeScript, PixiJS v8 (gameplay canvas only), Zustand, Vitest, Playwright, Web Audio API, existing Overdrive event scheduler/contact ledger, Tailwind v4 tokens from `docs/design.md`.
+
+## Global Constraints
+
+- `docs/game-design.md`, `docs/prd.md`, and `docs/design.md` remain the source of truth. Any requested mechanical change to Quota, score formula, item values, Glitch behavior, or the 8-zone x 3-stage MVP must be approved as a documentation change before code changes.
+- Preserve the current Overdrive MVP scope: the 15 Keycaps, 4 Macros, 5 Glitches, Focus Pause, Aegis Protocol, Overdrive Strike, daily seed, and endless mode. Firmware, Switch difficulty, Copycat, KERNEL PANIC, cosmetics, and the unimplemented v2 item pool stay out of this work.
+- New character variety in this plan is visual/choreographic unless a separate GDD amendment explicitly adds a new mechanic. Existing Packet Stalker, Needle Wraith, and Null Crown rules remain unchanged.
+- Do not copy Star Rune assets, code, names, or branding. The reference is used only for the design principle that each keystroke should produce a legible action and that abilities should be shown as short, readable demonstrations. See the public reference page: https://www.starrune.net/.
+- Engine code remains pure TypeScript: no React, PixiJS, DOM, Zustand, Supabase, Workers, or `Math.random()` in `lib/engine/overdrive`.
+- All UI copy, identifiers, comments, and item names remain English. Indonesian remains limited to word-pool data.
+- Use only design tokens and the spacing scale `4, 8, 12, 16, 24, 32, 48`. No hardcoded gameplay colors, arbitrary spacing, layout animation, or gameplay Framer Motion.
+- The event pipeline is deterministic and replayable. Source sequence, run ID, target ordinal, character index, seed, and stage preset must be enough to reproduce presentation decisions.
+- Input is never blocked by effects, transitions, shop purchase animation, or a target defeat animation. The next target is typeable immediately; presentation catches up independently.
+- Reduced motion keeps pose changes, contact readability, color/state changes, and popup fades while disabling shake, hitstop, particles, background pulse, and count-up as specified in `docs/design.md`.
+
+---
+
+## 1. Experience contract and acceptance gates
+
+The redesign is successful when a new player can see the causal loop without reading a manual:
+
+1. A printable key visibly launches a Warden attack and breaks the matching signal node.
+2. A clean word produces an execution beat, target defeat, score popup, and a compact equation showing where the score came from.
+3. A typo produces a readable misfire state and an honest outcome (`AEGIS RECOVERY — BASE ONLY` in Zone 2 or `CORRUPTED — 0 SCORE` in Zone 3+), without screen shake.
+4. Overdrive readiness changes the arena and attack language before it changes the score multiplier.
+5. At 75% Quota the stage enters `OVERRUN`; at 90% it shows the approximate score remaining. No hidden difficulty adaptation is introduced.
+6. A stage clear leaves the arena mounted for the 900ms result ribbon, then opens a one-viewport shop.
+7. Buying an item visibly installs it into the build, shows its trigger/effect, and previews the next stage impact using the canonical engine rules.
+8. Every item effect is readable at desktop and compact breakpoints; long copy wraps or scrolls inside its card instead of clipping the layout.
+
+### Hard release gates
+
+- Accepted-character cue starts within 50ms and reaches contact within 90ms; those timings are recorded by the existing contact ledger.
+- No horizontal overflow and no clipped item effect at `390×844`, `820×1180`, `1366×768`, `1440×900`, or `1920×1080`.
+- Worst-case combo and effect load stays at 60fps on the low-end test device with a maximum of 200 live pooled effects and 24 ambient motes.
+- All 133+ existing unit tests remain green; new engine/presentation logic has Vitest coverage before UI polish.
+- `npm run lint`, `npx tsc --noEmit`, `npm run build`, `npm run build:worker`, and the targeted Playwright viewport suite pass.
+- A reduced-motion run remains fully understandable without particles, shake, hitstop, background pulse, or score count-up.
+
+## 2. Design pillars
+
+### 2.1 Signal Combat: one keystroke, one action
+
+Use an authored attack grammar rather than a single repeated projectile:
+
+| Input/engine event | Warden verb | Target response | HUD/audio response |
+| --- | --- | --- | --- |
+| `character_accepted`, first character | `signal-lock` | First node lights, locks, and cracks | Short shot click + cyan contact ring |
+| `character_accepted`, middle character | `arc-dash` | Warden crosses to the node; node breaks into a directional shard | Layered shot click, no text popup |
+| `character_accepted`, third-or-later chain index | `chain-strike` | Contact trail joins previous node and uses the next authored lane | Combo-colored trail according to design tier |
+| Final accepted character before clean submit | `execution-ready` | Target enters a readable stagger/weak-point pose | Rail copy changes to `SPACE — EXECUTE` or the canonical Zone 3 prompt |
+| Clean word resolution | `execution` | Target defeat clip, 18 fragments, score popup | Impact layer, equation for 700ms |
+| Dirty word resolution | `misfire` | Target phases/recoils; no fake hit or score | Red underline, outcome copy, no ordinary-word shake |
+| Full charge + canonical release input | `overdrive-release` | Full-height impact column, snap-return afterimage | Charge lock, rising cue, final multiplier shown in equation |
+| Enemy pressure telegraph | `pressure-attack` | Enemy anticipation, directional line, Warden block/recover | Warning color + attack cue; never resembles ambient motion |
+| Zone 1–2 timeout rescue | `aegis-block` | Distinct shield plane fractures; lethal attack is visibly deflected | `+30S`, rescue counter, cyan recovery wave |
+| Quota reached | `quota-break` | Bar pulses, target exit, arena settles | Quota chime, 50ms hitstop, 6px stage-clear response only |
+
+The first three verbs intentionally give the player the “every stroke is an action” feeling from the Star Rune reference while retaining Typecade’s own signal-node and score rules. The verb is selected by deterministic word/target metadata, never by random animation.
+
+### 2.2 Stage identity without rule drift
+
+Create a `StagePresentationPreset` for each canonical stage family and threat band. A preset controls palette, parallax mix, target lane choreography, clip emphasis, audio layer, and VFX budget; it does not alter Quota, score, item hooks, Glitch rules, or word order.
+
+- `warm-up-protected`: Packet Stalker relay geometry, slow cable parallax, cyan/green signal accents, generous entry spacing.
+- `rush-protected`: Needle Wraith flight paths, brighter lane traces, pink combo accents, quicker target promotion.
+- `glitch-protected`: Null Crown training silhouette, fractured arena rim, violet warning stamp, no mechanical Glitch in Zone 1.
+- `pressure`: denser machinery layers, longer attack telegraphs, red threat accents, no extra health bar.
+- `overclocked`: alternating lane choreography, stronger charge trails, higher contrast target entry, still within the same word/scoring contract.
+- `lethal`: sparse arena, hard silhouette edges, yellow/red quota state, authored pressure attacks and Glitches remain the only danger sources.
+
+Zone 1’s literal beginner route remains intact: one key → two-key signal → three-letter word, with automatic execution. Visual intensity escalates around the player’s learning path instead of forcing a full-word timer immediately.
+
+### 2.3 Build legibility: make the Balatro-like payoff causal
+
+Do not change the canonical formula. Expose it:
+
+`Base (characters + Base bonuses) → Base multipliers → additive Mult → Mult multipliers → final multipliers → floored word score`
+
+Every clean submission renders one compact equation and stores a structured trace. Every purchased item displays its trigger, the current-stage proc count, and a next-stage preview delta. If an effect cannot be measured exactly without changing the rules, label it `estimated contribution` rather than attributing points to the wrong item.
+
+### 2.4 Spectacle budget, not visual noise
+
+Use the existing pooled effect architecture. Ordinary accepted characters get one contact cue and one target response; only word clear, Mult up, Overdrive, Aegis, Glitch intro, and stage clear receive larger effects. Identical simultaneous item shapes merge and restart, as required by `docs/design.md`.
+
+## 3. Stage rhythm and encounter director
+
+Add a deterministic intensity director that consumes stage progress, not elapsed time alone:
+
+| Band | Trigger | Presentation change | Mechanical change |
+| --- | --- | --- | --- |
+| `ready` | Before first printable key | Scrim, visible combatants, `TYPE TO ENGAGE` | Timer waits for first printable key |
+| `engage` | 0–50% Quota | One active target, two low-contrast previews, regular attack grammar | Canonical scoring only |
+| `surge` | 50–75% Quota or combo tier x4+ | Faster authored lane rotation, stronger contact trails, restrained quota riser | No hidden multiplier or WPM scaling |
+| `overrun` | 75–90% Quota | `OVERRUN` rail state, arena rim pulse, target cadence/audio riser | Canonical rules unchanged |
+| `finish` | 90–100% Quota | Score-needed readout, final-word focus, low-contrast previews fade | Stage clears immediately on Quota |
+| `resolution` | Quota reached or stage failure | Quota break/run-over sequence and 900ms ribbon | Shop/run-over state transition |
+
+The director owns stage-local state (`band`, `preset`, `nextTargetLane`, `lastResolvedTarget`) and emits presentation beats. It never owns score, timers, RNG, or item effects.
+
+### Encounter sequence contracts
+
+Create named sequence modules under `features/overdrive/canvas/choreography/sequences/`:
+
+- `character-contact.ts`: consumes `character_accepted` envelope; chooses `signal-lock`, `arc-dash`, or `chain-strike`; schedules rig clip, projectile/line, node break, contact ledger cue/hit.
+- `word-resolution.ts`: consumes clean/dirty submission; schedules `execution`, `misfire`, equation, score popup, item acknowledgement, and target promotion.
+- `pressure-attack.ts`: consumes deterministic pressure telegraph; schedules enemy anticipation (minimum 240ms), directional line, Warden response, and recovery.
+- `overdrive-release.ts`: consumes the canonical Overdrive event; schedules charge lock, `overdrive` clip, 78%-gap impact column, audio layer, and snap-return within 320ms.
+- `aegis-rescue.ts`: consumes the canonical rescue event; schedules lethal attack, shield fracture, block pose, `+30S`, rescue counter, and cyan recovery wave.
+- `stage-resolution.ts`: consumes quota-clear/failure; schedules quota pulses, hitstop/shake only where allowed, ribbon handoff, and stage-local cleanup.
+- `sequence-types.ts`: declares `SequenceContext`, `SequenceOutput`, `PresentationAnchor`, and cancellation rules. Every output carries source sequence, target ordinal, character index (when applicable), due time, priority, and reduced-motion policy.
+
+Refactor `combat-director.ts` into a small facade that wires presets, sequences, `ContactLedger`, actor pools, and scene state. Keep the facade under 300 lines; sequence modules contain the choreography detail.
+
+## 4. Character and enemy variety plan
+
+### Warden
+
+Use the existing articulated Warden rig clips (`idle`, `ready`, `chain-1`, `chain-2`, `chain-3`, `dash`, `execute`, `block`, `hurt`, `recover`, `overdrive`). Add authored pose emphasis per preset rather than swapping a full-body image:
+
+- Protected: compact braced stance, clean cyan signal core.
+- Pressure: lower stance, stronger recoil, red warning visor.
+- Overclocked: brighter charge conduits and faster follow-through, no extra damage rule.
+- Lethal: shield plate partially open, harsher silhouette rim, clearer block pose.
+
+### Enemy families
+
+Keep the three MVP classes but provide stage-specific visual variants in their manifests:
+
+- Packet Stalker: relay body, cable tail, three locomotion accents (`crawl`, `hop`, `skitter`).
+- Needle Wraith: spine flight, high/mid/low lane variants, readable anticipation tilt.
+- Null Crown: plate orbit, fracture state, boss entry and defeat silhouettes.
+
+Each variant must provide the canonical clips: `locomotion`, `idle`, `anticipation`, `attack`, `hit`, `defeat`, `special`. Add no new gameplay health, damage, or reward system. If a future variant needs a mechanical rule, stop and amend `docs/game-design.md` before implementation.
+
+### Asset and licensing gates
+
+- Store project-owned/commercial-safe art locally under `public/overdrive/art/` and record source/license in `CREDITS.md`.
+- Keep the first playable stage under 5MB compressed combat art and two full character atlases resident.
+- Author raster sheets with 12% clear padding and validate crisp edges at 0.5 scale.
+- Add a manifest test that every stage preset resolves to valid local asset IDs and every rig exposes all required clips.
+
+## 5. Reactive arena and background layers
+
+Replace the “pasted background” feeling with a layered, state-reactive arena while respecting the existing canvas tokens:
+
+1. `arena-background.ts`: owns the base signal-trench master and zone-safe crop.
+2. `parallax-layer.ts`: owns distant machinery, cable drift, haze, and foreground cover; clamps movement to 8px from origin.
+3. `arena-reactive-state.ts`: maps presentation state (`comboTier`, `quotaBand`, `overdriveReady`, `glitchIntro`, `focusPause`) to tokenized opacity/tint/pulse values.
+4. `arena-lanes.ts`: draws the authored high/mid/low attack paths and keeps them outside the command-rail/caret exclusion zone.
+
+Reactive rules:
+
+- Ambient motion is always lower contrast than the active word and combat contact.
+- Combo x4+ may add the documented caret trail and particle increase; x8+ may add the documented 0.04 opacity background pulse; x16+ may add the thin edge glow.
+- `OVERRUN` changes quota rail, target cadence, and audio riser without introducing a hidden score multiplier.
+- Focus Pause cancels attack anticipation, holds the Warden in `ready-high`, and keeps only low-motion ambience.
+- Reduced motion freezes ambient animation and all background pulse while retaining state color and pose changes.
+
+## 6. VFX and audio feedback map
+
+Centralize effect descriptors in `features/overdrive/canvas/effects/effect-manifest.ts` and audio descriptors in `features/overdrive/fx/sfx.ts`. The descriptor is data-only and consumed by Pixi/Web Audio adapters.
+
+| Event | Visual | Audio | Budget/guard |
+| --- | --- | --- | --- |
+| Accepted character | 28px contact ring, 8px smear, node fracture | One shot click | One contact effect per accepted key |
+| Chain contact | Joined line between adjacent nodes, violet/pink only at combo tier | Slightly layered shot | Merge same-source decorations |
+| Clean word | Execute pose, target dissolve, exactly 18 fragments, `+score` popup, equation | Impact layer | Popup cap 3; fragments pooled |
+| Typo | Red underline + 4px rail shake for 80ms | Muted misfire click | Never screen shake |
+| Mult up | Number scale 1.0→1.2→1.0, short flash, 50ms hitstop | Harmonic rise | No particle burst beyond tier budget |
+| Item proc | Rarity border flash 150ms, one nearby proc label | Short item tick | One visible label; identical shapes merge |
+| Overdrive ready/release | Rail lock, aura/rim pulse, impact column, afterimage | Rising cue + release layer | 320ms lifecycle; screen shake max 3px |
+| Pressure attack | Articulated anticipation, directional line, contact ring | Warning cue | Anticipation ≥240ms; no ambient lookalike |
+| Aegis rescue | Block pose, shield plane fracture, cyan wave, `+30S` | Deflection sting | 600ms; no timer jump without animation |
+| Glitch intro | Scanline/title stamp, stage-specific silhouette | Glitch sting | 400ms; do not add KERNEL PANIC |
+| Stage clear | Quota pulse, defeat settle, result ribbon handoff | Quota chime | Shake max 6px, hitstop 50ms |
+
+Effect allocation must honor the 200 live-object cap and pool contracts. Under reduced motion, omit particles, shake, hitstop, background pulse, and count-up while keeping the corresponding semantic event and readable state changes.
+
+## 7. Exact score trace and item impact
+
+### Engine contract
+
+Add a pure, immutable trace to the score resolution result without changing numeric outcomes:
+
+```ts
+type ScoreTraceOperation = "add" | "multiply" | "floor"
+
+type ScoreTraceStep = {
+ readonly id: string
+ readonly label: string
+ readonly source: "base" | "keycap" | "macro" | "combo" | "overdrive" | "aegis" | "ruleset"
+ readonly operation: ScoreTraceOperation
+ readonly before: number
+ readonly amount: number
+ readonly after: number
+}
+
+type ScoreResolution = {
+ readonly baseScore: number
+ readonly mult: number
+ readonly finalMultiplier: number
+ readonly total: number
+ readonly trace: readonly ScoreTraceStep[]
+ readonly itemImpacts: readonly ItemImpact[]
+}
+
+type ItemImpact = {
+ readonly itemId: string
+ readonly trigger: string
+ readonly kind: "exact" | "estimated"
+ readonly scoreDelta: number
+ readonly procCount: number
+}
+```
+
+The trace must be produced by the same ordered operations as the canonical formula. Add a deterministic counterfactual helper for item preview: clone the pure score context, remove one item effect, resolve again, and show the difference as an estimate only when an effect cannot be isolated exactly. Never mutate live run state to produce a preview.
+
+### Presentation
+
+- `features/overdrive/components/score-equation.tsx` renders the latest trace as one compact equation, then exposes an accessible expanded description.
+- `features/overdrive/components/build-impact.tsx` renders current-stage proc count, score/protection contribution, and the next-stage preview delta.
+- `features/overdrive/components/stage-clear-ribbon.tsx` shows total stage score, Tokens, accuracy, and strongest exact/estimated item contribution as required by the design spec.
+- `features/overdrive/components/shop.tsx` receives the previous stage’s `ScoreResolution` and renders it in the top strip without increasing card height.
+- Presentation events carry `scoreTraceId` and `itemImpactIds` so Pixi proc feedback, HUD equation, ribbon, and shop facts cannot disagree.
+
+## 8. Shop and desktop overflow redesign
+
+Split the current shop into bounded components:
+
+- `ShopViewport`: owns the one-viewport grid and keyboard focus order.
+- `ShopOfferCard`: owns exact name/effect/trigger/price/rarity/capacity state.
+- `EffectBlock`: owns long-copy wrapping and accessible full-text disclosure.
+- `BuildRail`: owns installed item slots and proc badges.
+- `BuildDelta`: owns next-stage Quota and score-impact preview.
+- `ShopActionBar`: owns fixed deploy/reroll affordances and keyboard hints.
+
+Layout rules:
+
+- Desktop: three equal columns for two Keycaps + one Macro, with `min-w-0` on every grid/flex child.
+- Compact: three short rows with the same information order; no hidden active Macro.
+- Effect text uses `overflow-wrap:anywhere`, `break-words`, and a bounded `max-height` with internal scroll only inside the effect block. The page itself never becomes a required scroll surface.
+- Additional detail appears on focus/hover without changing layout height. The accessible description remains available to keyboard and screen-reader users.
+- Unaffordable prices use opacity 0.4 and the tokenized red price state. Rarity uses border + text label, never color alone.
+- Install animation uses transform/opacity only, lasts 600ms, and never blocks another purchase or deploy action.
+
+Add a Playwright fixture containing the longest current item effects and assert:
+
+```ts
+await expect(page.locator("[data-testid=shop-viewport]")).toHaveCSS("overflow-x", "hidden")
+await expect(page.locator("[data-testid=item-effect]")).toHaveCount(3)
+expect(await page.locator("body").evaluate((node) => node.scrollWidth <= node.clientWidth)).toBe(true)
+```
+
+Also assert that each full effect string is present in the DOM and not visually clipped at all five Definition-of-Done viewport sizes.
+
+## 9. Roguelike decision rhythm and retention
+
+Keep the existing economy values and add clarity around the decision:
+
+1. Stage clear ribbon states what worked: score equation, strongest item contribution, accuracy, Tokens, and time bonus.
+2. Shop top strip states `NEXT STAGE`, exact Quota, projected Interest, and the previous stage’s strongest contribution.
+3. Buying an item plays a short install sequence: card locks into the build rail, proc label appears, and `NEXT STAGE PREVIEW` updates immediately.
+4. Active build slots show trigger tags (`ON CLEAN WORD`, `ON TYPO`, `ON COMBO`, `ON STAGE START`, etc.) using the exact existing item hooks.
+5. Reroll and sell actions expose their Token cost/return before confirmation; keyboard controls remain 1/2/3, R, Tab, Enter.
+6. The end-of-run view lists the final build, strongest item impact, reached zone, and a clear “one more run” comparison against the local personal best.
+
+This creates the Balatro-like causal loop—buy, see the build work, understand the next risk—without adding a second currency, hidden rarity rules, or an unapproved meta-progression system.
+
+## 10. File map and interfaces
+
+### Create
+
+- `features/overdrive/canvas/choreography/sequence-types.ts`
+- `features/overdrive/canvas/choreography/sequences/character-contact.ts`
+- `features/overdrive/canvas/choreography/sequences/word-resolution.ts`
+- `features/overdrive/canvas/choreography/sequences/pressure-attack.ts`
+- `features/overdrive/canvas/choreography/sequences/overdrive-release.ts`
+- `features/overdrive/canvas/choreography/sequences/aegis-rescue.ts`
+- `features/overdrive/canvas/choreography/sequences/stage-resolution.ts`
+- `features/overdrive/canvas/arena-background.ts`
+- `features/overdrive/canvas/parallax-layer.ts`
+- `features/overdrive/canvas/arena-reactive-state.ts`
+- `features/overdrive/canvas/arena-lanes.ts`
+- `features/overdrive/canvas/effects/effect-manifest.ts`
+- `features/overdrive/canvas/stage-presets.ts`
+- `features/overdrive/components/score-equation.tsx`
+- `features/overdrive/components/build-impact.tsx`
+- `features/overdrive/components/shop-viewport.tsx`
+- `features/overdrive/components/shop-offer-card.tsx`
+- `features/overdrive/components/effect-block.tsx`
+- `features/overdrive/components/build-rail.tsx`
+- `features/overdrive/components/build-delta.tsx`
+- `lib/engine/overdrive/__tests__/score-trace.test.ts`
+- `features/overdrive/canvas/choreography/__tests__/sequence-contracts.test.ts`
+- `features/overdrive/canvas/__tests__/stage-presets.test.ts`
+- `features/overdrive/canvas/effects/__tests__/effect-manifest.test.ts`
+- `features/overdrive/components/__tests__/shop-overflow.spec.ts`
+
+### Modify
+
+- `lib/engine/overdrive/scoring.ts`, `types.ts`, and `events.ts`: exact score trace, item impact metadata, and immutable presentation-safe payloads.
+- `lib/engine/overdrive/run.ts` and the existing focused run modules: preserve the public facade while forwarding trace data.
+- `features/overdrive/presentation/events.ts`, `scheduler-types.ts`, `scheduler.ts`, and `use-presentation-events.ts`: one semantic envelope path with compatibility removed only after consumers migrate.
+- `features/overdrive/canvas/choreography/combat-director.ts`: facade only; wire sequences, stage presets, ledger, pools, and target promotion.
+- `features/overdrive/canvas/visual-assets.ts`, `rig/rig-manifests.ts`, and `rig/rig-definition.ts`: tokenized preset/clip/asset lookup.
+- `features/overdrive/canvas/combat-scene.ts`, `scene-feedback.ts`, `effects/combat-effects.ts`, `effects/item-presentation.ts`, and pool modules: consume sequence outputs and enforce budgets.
+- `features/overdrive/components/hud.tsx`, `gameplay-layer.tsx`, `stage-clear-ribbon.tsx`, `shop.tsx`, and `run-over.tsx`: render equation, item impact, threat bands, and bounded build/shop surfaces.
+- `docs/design.md`: add only approved new token names or effect budget clarifications; existing numeric values remain authoritative.
+- `CREDITS.md` and local asset manifests: document every new project-owned/commercial-safe asset.
+
+### Interface flow
+
+`EngineEvents → PresentationEventEnvelope → PresentationScheduler → PresentationBeat[] → SequenceOutput → Pixi pools/rigs + HUD presentation events`
+
+`ScoreResolution → ScoreEquation/BuildImpact → StageClearRibbon + ShopViewport preview`
+
+`StageSnapshot + threat band + seed → StagePresentationPreset → deterministic lane/clip/effect choices`
+
+## 11. Task-by-task execution plan
+
+Each task starts with a failing or characterization test, has a bounded implementation surface, and ends with a verification command. Do not skip to visual polish while the corresponding contract is red.
+
+### Task 0 — Baseline and asset inventory
+
+- [ ] Record the current `git status`, existing test count, current shop screenshots, and the three current rig manifests.
+- [ ] Confirm all current Overdrive event consumers and mark the compatibility path that will be removed after migration.
+- [ ] Create a deterministic stage-preset table test fixture for all 8 zones × 3 stages and the four threat bands.
+- [ ] Run `npm test`, `npm run lint`, `npx tsc --noEmit`, and `git diff --check` before changes; save the output in the task notes.
+
+Expected result: the baseline is reproducible and no asset or event consumer is lost during the refactor.
+
+### Task 1 — Score trace and item impact contract (R-2, I-1, J-1)
+
+- [ ] Add `lib/engine/overdrive/__tests__/score-trace.test.ts` with failing cases for clean words, Overdrive Strike ordering, Zone 2 Aegis Recovery, Zone 3 dirty zero-score, Mult reset, and a purchase preview that leaves live state unchanged.
+- [ ] Add the `ScoreTraceStep`, `ScoreResolution`, and `ItemImpact` types to the headless engine.
+- [ ] Implement trace generation in the existing ordered scoring pipeline; assert final totals match every pre-existing scoring test.
+- [ ] Implement pure counterfactual preview and label non-isolatable effects as `estimated`.
+- [ ] Expose trace data through `word_complete`, stage-clear, and shop-facing snapshots without importing UI code.
+- [ ] Run `npx vitest run lib/engine/overdrive/__tests__/scoring.test.ts lib/engine/overdrive/__tests__/score-trace.test.ts lib/engine/overdrive/__tests__/items.test.ts`.
+- [ ] Run `npx tsc --noEmit` and `rg -n "react|pixi|zustand|window|document|Math\\.random" lib/engine/overdrive`.
+
+Expected result: existing numeric behavior is byte-for-byte stable while the UI receives a causal, immutable trace.
+
+### Task 2 — Semantic presentation envelopes and sequence contracts (J-1, J-3)
+
+- [ ] Add failing scheduler tests for source order, accepted-character no-drop, item aggregation, critical-beat priority, run reset, and reduced-motion decoration suppression.
+- [ ] Define `SequenceContext`, `SequenceOutput`, `PresentationAnchor`, and cancellation contracts in `sequence-types.ts`.
+- [ ] Migrate store event adapters to one envelope path carrying run ID, target ordinal, character index, source sequence, stage preset ID, and due time.
+- [ ] Keep a compatibility adapter until all Pixi/HUD consumers use envelopes, then delete the duplicate legacy emission path.
+- [ ] Run `npx vitest run features/overdrive/presentation` and `npx tsc --noEmit`.
+
+Expected result: every visual beat is traceable to one deterministic engine event, with critical input contacts protected from decorative load.
+
+### Task 3 — Character contact and pressure choreography (J-1)
+
+- [ ] Add failing sequence contract tests for first/middle/chain/final accepted characters, dirty submission, rapid 12-character input, enemy anticipation ≥240ms, and input continuity during defeat.
+- [ ] Implement `character-contact.ts`, `word-resolution.ts`, and `pressure-attack.ts` using existing rig clips, target lanes, `ContactLedger`, and pools.
+- [ ] Refactor `combat-director.ts` into a facade and remove semantic contact ownership from any render-loop queue.
+- [ ] Add rig-manifest validation for required Warden/enemy clips and stage-specific visual variants.
+- [ ] Run `npx vitest run features/overdrive/canvas/choreography features/overdrive/canvas/rig features/overdrive/canvas/__tests__/stage-presets.test.ts`.
+
+Expected result: accepted keys visibly chain through the arena without dropped contacts, a fixed firing anchor, or blocked input.
+
+### Task 4 — Reactive arena and stage presets (J-1, J-3)
+
+- [ ] Add failing tests for deterministic preset lookup, lane choreography, quota-band transitions, combo tier transitions, Focus Pause, Overdrive ready, and reduced-motion state.
+- [ ] Implement the four arena modules and `stage-presets.ts` using existing `visual-assets.ts` tokens and design geometry.
+- [ ] Add background state transitions for `engage`, `surge`, `overrun`, `finish`, `focusPause`, `glitchIntro`, and `overdriveReady`.
+- [ ] Verify ambient caps (24 motes, ≤8px parallax offset) and effect caps (200 live objects) through instrumentation tests.
+- [ ] Run `npx vitest run features/overdrive/canvas/__tests__/stage-presets.test.ts features/overdrive/canvas/effects/__tests__/effect-manifest.test.ts`.
+
+Expected result: each stage family has a recognizable living arena without changing gameplay math or creating visual attack lookalikes.
+
+### Task 5 — VFX/audio manifest and feedback budget (J-1, J-2, J-3)
+
+- [ ] Add failing manifest tests for every event in the feedback table, required duration/easing token, pool category, reduced-motion policy, and live-object budget.
+- [ ] Implement data-only effect descriptors and wire them into combat effects, item presentation, score popup, and SFX adapters.
+- [ ] Add three keystroke switch variants and lazy-loaded shot/impact/riser/glitch layers within the documented audio budget.
+- [ ] Verify duplicate proc merging, popup cap 3, exactly 18 defeat fragments, and no shake on ordinary words/typos.
+- [ ] Run `npx vitest run features/overdrive/canvas/effects features/overdrive/canvas/pools`.
+
+Expected result: spectacle is consistent and readable, not a random particle storm.
+
+### Task 6 — Score equation, stage ribbon, and build impact UI (J-1, I-1)
+
+- [ ] Add component tests for exact equation order, item trigger labels, `exact` vs `estimated`, stage-clear strongest contribution, and keyboard-only navigation.
+- [ ] Implement `score-equation.tsx` and `build-impact.tsx` using tokenized typography/color and an accessible expanded description.
+- [ ] Update HUD, stage ribbon, and run-over to consume the same `ScoreResolution` IDs as Pixi feedback.
+- [ ] Add a Playwright assertion that a clean word’s score popup, equation, and strongest item label agree.
+- [ ] Run the focused component/Playwright suite and `npm run lint`.
+
+Expected result: score and item purchases feel useful because the player can see exactly what changed and why.
+
+### Task 7 — One-viewport shop and overflow hardening (I-2, I-3, M3)
+
+- [ ] Add failing Playwright tests with the longest exact item descriptions at `390×844`, `820×1180`, `1366×768`, `1440×900`, and `1920×1080`.
+- [ ] Split `shop.tsx` into the bounded components listed above; add `min-w-0`, wrapping, internal effect scrolling, and stable card heights.
+- [ ] Add focus/hover detail that does not change layout height; retain full copy for keyboard and screen readers.
+- [ ] Add install-to-build animation, next-stage Quota/Interest preview, and the prior-stage strongest contribution.
+- [ ] Verify keyboard controls 1/2/3/R/Tab/Enter and no required page scroll.
+- [ ] Run `npx playwright test features/overdrive/components/__tests__/shop-overflow.spec.ts`.
+
+Expected result: no truncated item descriptions or desktop overflow, with purchases visibly changing the build rail and preview.
+
+### Task 8 — Integrated stage loop and visual QA (R-1, R-4, J-1, J-3, M3)
+
+- [ ] Play through Zone 1 beginner route, Zone 2 correction/Aegis Recovery, Zone 3 dirty zero-score, one Glitch stage, Overdrive release, and a stage clear.
+- [ ] Capture deterministic screenshots/video for accepted-character contact, chain contact, typo, clean execution, Mult up, Overrun, Aegis, Overdrive, stage ribbon, and shop install.
+- [ ] Run the five viewport sizes and reduced-motion mode; inspect focus-visible outlines, contrast hotspots, and command-rail/caret exclusion.
+- [ ] Instrument p95/p99 accepted cue/contact times, max unsettled contacts, max live effects, and frame time under x16 combo.
+- [ ] Run the complete verification set:
+
+ ```bash
+ npm test
+ npm run lint
+ npx tsc --noEmit
+ npm run build
+ npm run build:worker
+ git diff --check
+ ```
+
+Expected result: all existing behavior remains intact, the redesigned presentation meets the M3 60fps/no-overlap/readability gate, and reduced motion is demonstrably usable.
+
+### Task 9 — Documentation and asset hygiene
+
+- [ ] Update `docs/design.md` only with approved token/manifest additions and record the final effect budget.
+- [ ] Update `CREDITS.md` with every new art/audio source and license.
+- [ ] Add a short changelog entry describing the new attack grammar, stage presets, score trace, and shop overflow fix.
+- [ ] Confirm no Star Rune asset, copied code, or copied game-specific name entered the repository.
+- [ ] Record playtest findings: first-run comprehension, second-run intent, item choice confidence, and “why did I score/die?” answers.
+
+Expected result: the redesign is maintainable, auditable, and ready for a focused implementation PR per requirement ID.
+
+## 12. Open decisions to resolve before implementation
+
+These are explicit gates, not placeholders:
+
+1. **Visual-only variants (recommended for MVP):** approve Packet/Needle/Null material, lane, lighting, and clip variants without new mechanics. Any mechanical variant requires a GDD amendment and new balance tests.
+2. **Deterministic choreography (recommended):** use a persisted stage/target choreography table so replays and daily seeds show the same attack language. Do not use animation randomness that cannot be reproduced from the run seed.
+3. **Exact score impact (recommended):** use pure counterfactual resolution for isolated item effects and mark the remaining effects `estimated`. Never show a misleading exact number.
+4. **Original asset budget (required):** approve the local art/audio budget and licensing owner before adding files under `public/overdrive/art/` or expanding the first-stage resident texture set.
+
+## 13. Self-review checklist
+
+- [ ] Every requested concern is addressed: character variety, stage/background variety, correct/incorrect attack variation, explosions/VFX, score/item meaning, roguelike rhythm, Star Rune-inspired per-keystroke action, and desktop overflow.
+- [ ] No plan step changes canonical item values, Quotas, Glitches, or MVP scope without a documentation gate.
+- [ ] Every new interface has a producing file, consuming file, and test location.
+- [ ] Every implementation task starts with a test or characterization check and ends with an exact command and expected result.
+- [ ] No unresolved decision token, invented asset URL, or unbounded “make it feel better” instruction remains.
+- [ ] All motion, colors, spacing, particle limits, viewport sizes, and accessibility behavior point back to `docs/design.md`.
+- [ ] The final implementation can be split into small requirement-scoped commits (`J-1`, `J-2`, `J-3`, `I-1`, `I-2`, `M3`) without a giant rewrite.
diff --git a/env.example b/env.example
index 332afce..731b365 100644
--- a/env.example
+++ b/env.example
@@ -1,3 +1,6 @@
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
-NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
+NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY
+# Server-only secret. Never expose this with a NEXT_PUBLIC_ prefix.
+SUPABASE_SERVICE_ROLE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY
NEXT_PUBLIC_OVERDRIVE=true
+NEXT_PUBLIC_OVERDRIVE_COMPETITIVE=false
diff --git a/features/board/components/board-view.tsx b/features/board/components/board-view.tsx
index 3322bd5..fcf2116 100644
--- a/features/board/components/board-view.tsx
+++ b/features/board/components/board-view.tsx
@@ -1,10 +1,13 @@
import { useEffect, useMemo, useState } from "react";
+import Link from "next/link";
import { Trophy, Crown, Flame } from "@/components/icons";
import { motion } from "framer-motion";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { CountUp } from "@/components/ui/count-up";
import { UserAvatar } from "@/components/ui/user-avatar";
+import { Button } from "@/components/ui/button";
import { getSupabaseClient } from "@/lib/supabase/client";
+import { classifySupabaseError, SUPABASE_UNAVAILABLE_MESSAGE } from "@/lib/supabase/error-handler";
import { useAuth } from "@/lib/auth/auth-context";
const filterOptions = ["All Time", "This Week", "Today", "Words 50", "Time 60s"] as const;
@@ -35,6 +38,7 @@ export function BoardView() {
total_tests: number;
}>>([]);
const [isLoading, setIsLoading] = useState(false);
+ const [dbUnavailable, setDbUnavailable] = useState(false);
const queryParams = useMemo(() => {
const now = new Date();
@@ -68,7 +72,11 @@ export function BoardView() {
p_since: queryParams.since,
});
setIsLoading(false);
- if (error) return;
+ if (error) {
+ setDbUnavailable(classifySupabaseError(error));
+ return;
+ }
+ setDbUnavailable(false);
setRows((data ?? []) as Array<{
user_id: string;
display_name: string;
@@ -141,11 +149,11 @@ export function BoardView() {
-
- {!supabaseReady ? "Database connecting... (If this persists, please restart your 'npm run dev' to load .env variables)" : "No results yet."}
+
+
+
+
+
+
+
+ {dbUnavailable
+ ? "Database waking up"
+ : !supabaseReady
+ ? "Connecting to the database…"
+ : "The board is empty"}
+
+
+ {dbUnavailable
+ ? SUPABASE_UNAVAILABLE_MESSAGE
+ : !supabaseReady
+ ? "If this persists, restart npm run dev to reload env variables."
+ : "No one has claimed a spot in this category yet. Take a test to put your name first."}
+
+
+ {!dbUnavailable && supabaseReady && (
+
+ )}
@@ -220,4 +250,4 @@ export function BoardView() {
);
-}
+}
\ No newline at end of file
diff --git a/features/marketing/components/homepage-tagline.tsx b/features/marketing/components/homepage-tagline.tsx
new file mode 100644
index 0000000..298081a
--- /dev/null
+++ b/features/marketing/components/homepage-tagline.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { motion } from "framer-motion";
+
+/**
+ * Single-line tagline that sits BELOW the typing area on the homepage.
+ *
+ * Why so small: the typing area IS the hero. Marketing copy above it
+ * pushes the product off-screen on shorter viewports; paragraphs
+ * below it pull attention away from the live stats. One short line,
+ * centered, muted — sets tone without competing for the eye.
+ */
+export function HomepageTagline() {
+ return (
+
+ Sharp keys. Clear mind.
+
+ );
+}
\ No newline at end of file
diff --git a/features/marketing/components/newsletter-signup.tsx b/features/marketing/components/newsletter-signup.tsx
new file mode 100644
index 0000000..e3dc9bc
--- /dev/null
+++ b/features/marketing/components/newsletter-signup.tsx
@@ -0,0 +1,180 @@
+"use client";
+
+import { Mail, Check } from "@/components/icons";
+import { useState } from "react";
+
+/**
+ * Newsletter capture — hosted-form pattern that works with `output: 'export'`.
+ *
+ * Why no server action: Next.js static export doesn't ship a Node runtime,
+ * so server actions and /api routes are unavailable. Hosting the form
+ * submission at a third-party service (Buttondown, Loops, ConvertKit,
+ * Formspree) sidesteps this — the form POSTs directly to their endpoint
+ * and we get progressive enhancement (works without JS).
+ *
+ * Setup:
+ * 1. Sign up at https://buttondown.email (free 100 subscribers).
+ * 2. Set NEXT_PUBLIC_NEWSLETTER_ENDPOINT in .env.local to your
+ * Buttondown form URL, e.g. https://buttondown.email/yourusername
+ * (the `