# Project export: Rung

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: One assignment, 30 diagnoses. Rung finds the exact sub-skill each student is missing, tutors them there, and turns the class's gaps into tomorrow's lesson plan.
- Devpost: https://devpost.com/software/rung-th1xcd
- GitHub: https://github.com/Sravik-Sajja/rung
- Demo: https://rung-gray.vercel.app/
- Video: https://www.youtube.com/embed/yDUmcTjCnQE?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — ravadashreyas (26 commits), Sravik Sajja (18 commits), Vercel (1 commits)

## Devpost submission (written by the team)

### Inspiration

In high school, we took a calculus class with a teacher named Ms. Collins, and we struggled. Not the kind of struggle you fix with pure hard work and grit, but the kind where the class moves on, and you quietly fall further behind every week. What made Ms. Collins different is that she took time out of class and, based on daily quizzes, she would then match students into groups based on what they struggled with on the quiz, and had the students do problems together for the rest of class, then step in to interact with each mini group. She would never treat the class as one group moving at one speed. She figured out exactly what level each student was at, met us there, and never gave us the answer, only stopping by and giving well-placed hints to let us find the answer ourselves. She changed what learning felt like for us, and she made us want to turn around and help others teach the way she taught us. When this hackathon started, we read that students across the world are still performing below pre-pandemic levels. That statistic hit us hard, because we knew what being behind feels like from the inside, and we knew what it took to get out: not more content, but a teacher who could see precisely where we were stuck and respond to that. Most students will never get a Ms. Collins. This felt like the moment to take the imprint she left on us and build it into something anyone, anywhere, could use. That's why we built Rung, a differentiated-instruction platform that does what she did: find the exact rung each learner is standing on, and help them take the next one.

### What it does

The teacher sets up a digital classroom for students, and when students join, they must take a short diagnostic. After students have taken it, based on the results, Rung builds a separate focused practice plan for every skill that needs work. When they get stuck, an AI tutor offers three escalating levels of help: a nudge, a hint, and a guided step, but never the answer, because Ms. Collins never just gave us the answer either. On the other side, teachers get a live heatmap of exactly where each student stands, one-click actions from that evidence, and AI-drafted mini-lessons for small groups who share the same gap that include YouTube videos that the students can watch for guidance.

### How we built it

We built Rung on Next.js, Supabase, and GPT 5.6. We drew a clear line between what GPT helps with and what the app decides itself. GPT helps with explanations, tutoring students through a problem, creating practice sets, and crafting lessons for teachers. The app handles answer scoring, skill updating, and group formation. Before writing any code, we created our features list and crafted architecture.md and contracts.md so we had clear guidance for Codex to build the data flow and UI. This made it much easier to efficiently implement each feature, make modifications where necessary, and verify the functionality of the app. We validated model outputs before showing them, prevented any answer leaks in hints, and used safe fallbacks when the model failed to provide valid input. Before pushing a new feature, we iterated on our test cases to ensure functionality was preserved, growing our test suite up to 230 tests across 36 files.

### Challenges we ran into

The hardest challenge was preventing AI from being used to shortcut learning. We did not want a tutor that simply reveals an answer after one wrong attempt. The hint flow needed to progress from a small nudge to a more direct hint to one actionable guided step, while still leaving the student to do the actual problem. Generated practice created a similar challenge. Early outputs could be malformed, target the wrong kind of problem, repeat an item, or use math parameters that did not make sense. We solved that by limiting what the model can return. It only proposes structured problem parameters; server code validates those parameters, rejects anything outside the supported formats, and then constructs the prompt, number-line visual, and answer rule itself. The model never supplies the answer key. Moreover, we built fallback behavior for every AI feature so the core student and teacher experience still works if an API key, model call, or network request is unavailable. Lastly, we ran into quite a few UI edge cases that Codex did not flag and were only seen through the app itself. This made us much more intentional about manually testing each flow after implementation. We had to verify loading states, ensure the application acts differently when within a workspace versus sample data, and provide stylistic updates to better structure the UI.

### What we learned

We learned that great teaching is an algorithm and a heart, and software can only carry the first part, so it had better carry it faithfully. Practically, we learned that writing precise specs up front is the single biggest multiplier when building with AI; that the right question isn't "what can the model do?" but "what should the model never do?"; and that deterministic guarantees are what turn an impressive demo into something a real teacher could trust. Mostly, though, we learned that the way one teacher made us feel in a calculus classroom could be translated into product decisions: diagnose precisely, meet learners where they are, hint before you tell, and always show them the next rung. Ms. Collins did that for one classroom. Rung is our attempt to do it for every student the pandemic left behind.

## README (from the GitHub repository)

# Rung

Rung is a differentiated-instruction prototype for middle-school math. A learner takes a short check-in, receives a separate AI-assisted focused-practice plan for each skill that needs work, and gets answer-safe AI support. Teachers can view evidence in a heatmap, take a quick action, and open an AI-assisted small-group mini-lesson.

Correctness, scoring, and mastery are deterministic server decisions. GPT-5.6 helps with bounded explanation, tutoring, practice planning, and lesson drafting; it never decides whether an answer is correct or whether a learner has mastered a skill.

## What judges should try

### Student flow

1. Visit `/demo`, enter any name, and start the five-question check-in.
2. Finish the check-in. Rung creates one selectable practice plan for each skill that was not mastered; completed plans remain marked when you return.
3. In a plan, submit one wrong answer. Use the three tutor levels—nudge, hint, and guided step—and then submit again.
4. Finish the plan and open the practice summary. It shows correct answers, first-try successes, total attempts, retries, and the questions that needed another try without revealing answer keys.
5. Use **Plan**, **My Work**, and **Progress** in the student header. Plan returns to the current class’s assigned practice sets; My Work shows completed evidence; Progress shows the current skill climb.

Example learner name: `Alex`.

### Teacher flow

1. Visit `/teacher-workspace` and create a workspace with any teacher/class name.
2. Copy its join code or link; open it in an incognito window or separate browser profile. Join as a learner and complete the check-in. For a suggested group, have two learners miss the same skill.
3. Return to the teacher workspace. The heatmap is based on those workspace learners and their stored evidence, not the fictional sample class.
4. Hover a `Needs support` or `Developing` cell and choose **Assign 3Q**. Return to that learner, refresh, then open **Plan** to find the assignment under **From your teacher**.
5. Open **Mini Lesson** from a suggested group (or a `Needs support` cell). The route displays the workspace group, an AI-assisted 15–20 minute lesson, validated AI-generated matched practice, and a vetted skill-aligned video.
6. `Not started` offers a one-time reminder. `Mastered` is informational only.

Example teacher/class: `Ms. Jordan`, `Period 3 fractions`.

The fixed fictional sample heatmap is also available at `/teacher/dashboard`.

## How we used Codex and GPT-5.6

Codex accelerated the project from scaffold through polished demo: it helped establish the Next.js structure, implement API routes and student/teacher flows, refine UI interactions, resolve merges, add tests, and maintain the project documentation. We directed and reviewed the product decisions throughout. We kept a running log of the work in [CODEX_LOG.md](./CODEX_LOG.md) and [IMPLEMENTATION_LOG.md](./IMPLEMENTATION_LOG.md).

Our approach was to figure out what GPT-5.6 is actually good at and hand it that work, instead of just prompting it for whatever came next.

It's at its best when the target is precisely specified. So we wrote `architecture.md` and `contracts.md` ourselves, and had Codex freeze the typed contracts, seed IDs, and fallback shapes before any feature work started. When the contract was locked, Codex was fast and rarely wrong. When it was vague, we lost the time savings to re-review. Writing the spec up front was the biggest speedup of the whole project, and it also let us run Codex sessions on the server logic, the AI layer, and the student UI in parallel without them stepping on each other.

It's also better than us at precision-heavy code. Exact fraction arithmetic, database migrations, schema changes: the kind of code where a small mistake shows up weeks later as a blank dashboard with no error message. Codex wrote all of it, and caught at least one of those silent failures before it shipped.

It's good at reading everything. When a tester hit a bug we were sure was caching, Codex traced the whole data path and found the actual cause, which was somewhere none of us had looked. It even found and fixed a leak that its own fix introduced. Before the demo we also pointed a GPT-5.6 Sol review at the codebase, and it flagged the seams most likely to break live; we had Codex fix them. And it wrote tests right after implementing each behavior, which is exactly the point where we would have skipped them. The suite ended at 219 tests across 35 files, and we didn't accept a session's output until types, tests, and a production build passed.

That same strength made working as a team easier. We split the project across parallel tracks, so each of us regularly had to pick up code the other had written days earlier. Instead of reading a big merge cold, we'd have Codex walk through what actually changed and how it connected to the contracts before touching anything near it. At one point a 65-file, nine-migration chunk of work landed without a log entry; Codex reconstructed what it did from the migrations and source, wrote the missing log entry after the fact, and flagged a stale function overload the commit had left behind. Catching up on your partner's work went from an afternoon of reading to a short conversation.

We also matched the model variant to the job: Luna for high-volume runtime calls like tutor hints, Terra available for heavier drafts like diagnosis explanations and teacher lessons, and Sol for the code review.

What it's not good at is judgment, so the product decisions stayed with us:

* Keep learning decisions deterministic; the model is never the grader.
* Generate practice only through supported, validated problem formats with server-derived answers.
* Make tutoring progressive and answer-safe rather than providing completed solutions.
* Give teachers immediate actions from evidence, then connect shared gaps to a practical mini-lesson.
* Keep the public walkthrough structurally separate from a teacher workspace's learner evidence.

The running app follows the same idea: GPT-5.6 does the language and planning, deterministic server code decides everything that matters. Its outputs are validated, cached when appropriate, rate-limited, and replaced by safe fallbacks if an API key, network call, or schema check is unavailable.

## How the AI is used

- **GPT-5.6:** diagnosis language, nudge/hint/guided-step tutoring, validated student and teacher matched-practice parameters, and mini-lesson drafts.
- **Deterministic server code:** answer scoring, mastery updates, practice progression, group membership, and answer-format validation.
- **Safety boundary:** model outputs are schema-validated and answer-leak checked before display. Generated practice is reconstructed and scored from server-derived math data.
- **Caching:** validated teacher lessons, matched practice, and other eligible AI results are stored in `ai_runs`; reopening the same plan uses the cache rather than making another OpenAI call.

## Local setup

### Prerequisites

- Node.js 20 or later
- npm
- Optional: OpenAI API key for live model responses
- Optional locally, required for a durable/shared demo: a Supabase project

```bash
npm install
cp .env.example .env.local
npm run dev
```

Open [http://localhost:3000](http://localhost:3000).

The local walkthrough works with `DEMO_MODE=true` and deterministic fallbacks. It uses process memory, so restarting the server resets local learner and workspace state.

### Environment variables

| Variable | Needed for | Purpose |
| --- | --- | --- |
| `DEMO_MODE=true` | Local demo | Enables temporary learner and teacher-workspace flows. |
| `OPENAI_API_KEY` | Live AI | Enables live GPT-5.6 calls; without it, typed safe fallbacks keep the demo usable. |
| `NEXT_PUBLIC_SUPABASE_URL` | Durable/shared data | Supabase project URL. |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Durable/shared data | Supabase public `anon` key. |
| `SUPABASE_SERVICE_ROLE_KEY` | Durable/shared data | 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 203 recognized source files, 1227 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (120 of 210)

```
.env.example
.gitignore
.nvmrc
architecture.md
CODEX_LOG.md
contracts.md
DESIGN.md
IMPLEMENTATION_LOG.md
LICENSE
next-env.d.ts
next.config.ts
package.json
postcss.config.mjs
README.md
src/app/api/classes/[classId]/dashboard/route.ts
src/app/api/demo/participant/route.test.ts
src/app/api/demo/participant/route.ts
src/app/api/diagnostics/[assignmentId]/complete/route.test.ts
src/app/api/diagnostics/[assignmentId]/complete/route.ts
src/app/api/diagnostics/[assignmentId]/route.ts
src/app/api/peer-attempts/route.ts
src/app/api/peer-solutions/[itemId]/route.ts
src/app/api/practice/[sessionId]/route.ts
src/app/api/practice/[sessionId]/summary/route.ts
src/app/api/responses/route.test.ts
src/app/api/responses/route.ts
src/app/api/students/[studentId]/current-diagnostic/route.ts
src/app/api/students/[studentId]/mastery/route.ts
src/app/api/students/[studentId]/work/route.ts
src/app/api/teacher-groups/[groupId]/plan/route.ts
src/app/api/teacher-workspace/join-preview/route.test.ts
src/app/api/teacher-workspace/join-preview/route.ts
src/app/api/teacher-workspace/session/route.ts
src/app/api/teacher-workspace/student-session/route.test.ts
src/app/api/teacher-workspace/student-session/route.ts
src/app/api/teacher-workspace/students/[studentId]/assign-practice/route.ts
src/app/api/teacher-workspace/students/[studentId]/route.ts
src/app/api/tutor/hint/route.test.ts
src/app/api/tutor/hint/route.ts
src/app/api/work-help/route.test.ts
src/app/api/work-help/route.ts
src/app/demo/page.tsx
src/app/globals.css
src/app/join-class/page.tsx
src/app/layout.tsx
src/app/page.tsx
src/app/student/diagnosis/page.tsx
src/app/student/diagnostic/page.tsx
src/app/student/mastery/page.tsx
src/app/student/practice/[sessionId]/page.tsx
src/app/student/practice/[sessionId]/summary/page.tsx
src/app/student/work/page.tsx
src/app/teacher-workspace/groups/[groupId]/loading.tsx
src/app/teacher-workspace/groups/[groupId]/page.tsx
src/app/teacher-workspace/page.tsx
src/app/teacher/dashboard/page.tsx
src/app/teacher/groups/[groupId]/loading.tsx
src/app/teacher/groups/[groupId]/page.tsx
src/components/app-shell.tsx
src/components/demo/start-climb-form.tsx
src/components/landing/how-it-works-section.tsx
src/components/landing/section-connector.tsx
src/components/landing/step-vignettes.tsx
src/components/page-placeholder.tsx
src/components/rung-wordmark.tsx
src/components/student/diagnosis-result.tsx
src/components/student/fraction-input.tsx
src/components/student/fraction.tsx
src/components/student/hint-ladder.tsx
src/components/student/mastery-badge.tsx
src/components/student/models/equivalent-fraction-model.tsx
src/components/student/models/fraction-bar-model.tsx
src/components/student/models/item-model.tsx
src/components/student/models/model-math.test.ts
src/components/student/models/model-math.ts
src/components/student/models/model-readout.tsx
src/components/student/models/number-line-model.tsx
src/components/student/models/parts-stepper.tsx
src/components/student/number-line-question.tsx
src/components/student/peer-gate-card.tsx
src/components/student/persisted-practice-loop.tsx
src/components/student/practice-session.tsx
src/components/student/practice-summary.tsx
src/components/student/progress-indicator.tsx
src/components/student/surface/rung-progress.tsx
src/components/student/surface/streak-chip.tsx
src/components/student/surface/student-nav.tsx
src/components/student/surface/student-shell.tsx
src/components/student/surface/win-feedback.tsx
src/components/student/work-help-card.tsx
src/components/student/work-item-review.tsx
src/components/teacher-workspace/setup-form.tsx
src/components/teacher-workspace/student-join-form.tsx
src/components/teacher-workspace/workspace-dashboard.tsx
src/components/teacher/dashboard-view.tsx
src/components/teacher/group-card.tsx
src/components/teacher/lesson-plan.tsx
src/components/teacher/mastery-heatmap.tsx
src/components/teacher/practice-set.tsx
src/components/teacher/response-evidence.test.tsx
src/components/teacher/response-evidence.tsx
src/components/teacher/student-detail.tsx
src/components/teacher/student-roster.tsx
src/components/teacher/video-recommendation.tsx
src/components/theme-toggle.tsx
src/components/ui/badge.tsx
src/components/ui/button.tsx
src/components/ui/card.tsx
src/components/ui/cn.ts
src/components/ui/eyebrow.tsx
src/components/ui/index.ts
src/components/ui/page-header.tsx
src/components/ui/video-embed.tsx
src/lib/ai/adapter.ts
src/lib/ai/contracts.ts
src/lib/ai/fixtures.ts
src/lib/ai/leakage.test.ts
src/lib/ai/leakage.ts
src/lib/ai/rate-limit.ts
src/lib/ai/runtime.test.ts
[90 more files omitted for size]
```

### Dependencies

- package.json: @supabase/supabase-js@^2.49.1, @types/node@^22.13.4, @types/react@^19.0.8, @types/react-dom@^19.0.3, @vercel/analytics@^2.0.1, autoprefixer@^10.4.20, next@^15.2.0, openai@^4.85.0, react@^19.0.0, react-dom@^19.0.0, tailwindcss@^3.4.17, tsx@^4.19.3, typescript@^5.7.3, vitest@^3.0.7, zod@^3.24.2

### Recent commits (newest first)

- Update readme
- Added assign questions:teacher ui and student logic + ui
- Ui and bug fixes
- updated ReadMe
- Updated readme
- Merge pull request #1 from Sravik-Sajja/vercel/install-and-configure-vercel-w-bmshnl
- Install and configure Vercel Web Analytics
- Added license and prepare for deploy
- Demo access for prod
- Merge Conflicts
- Ian Changes
- Implemented post practice lesson summary
- Updated readme, implemented all-completed screen, and rate limits
- Added videos
- Ui and prompt adjustments
- Supabase support works, added teacher workspace, ability to join class
- Adjusted student flow ui
- Removed maya walkthrough plus added correct answers
- Implemented teacher quick actions on dashboard
- Added teacher support for seeing questions and answers

## Key source files (fetched from GitHub, selected and truncated for size)

### CODEX_LOG.md

```markdown
# Codex work log

This is a concise record of Codex-assisted implementation decisions and verification. It complements the factual feature handoff in [IMPLEMENTATION_LOG.md](./IMPLEMENTATION_LOG.md); [architecture.md](./architecture.md) remains the source of truth for product and system decisions.

## 2026-07-16 - durable walkthrough review and implementation

### Review focus

- A GPT-5.6 Sol review identified the seams most likely to break a demo: nondeterministic generated-plan ordering, missing durable support state, temporary learner data falling into local state, and a mismatch between written contracts and browser/API DTOs.
- The implementation kept the visual work isolated where possible. Data/identity propagation was added without replacing the independently evolving student UI composition.

### Decisions implemented

- A visitor starts with a short first name or nickname, not Maya. The server creates a fictional temporary learner and binds it to an opaque httpOnly cookie; the visible ID is only a consistency check. Maya remains the prepared fallback walkthrough.
- Correctness, mastery, practice progression, and eligibility remain deterministic server decisions. GPT-5.6 can explain, hint, propose bounded practice parameters, and coach from work; it cannot score or unlock anything.
- Work help is earned by the server-recorded sequence `miss -> hint/guided_step -> later miss`. Work/photo input remains request-memory only, and a failed AI call releases the one-time claim.
- Durable generated plans carry explicit order and validation provenance. UI order never depends on coincident timestamps.

### Verification performed

- Type check passed: `npx tsc --noEmit`.
- Full test suite passed: 23 files / 135 tests.
- Production build passed: `npm run build`.
- Diff whitespace check passed after documentation edits; it must remain part of the pre-commit check.
- Read-only browser smoke confirmed the temporary learner entry field, disabled-start state, and Maya fallback link.

### Remaining external checks

- Apply migrations `001`–`008` to a real Supabase project, seed it, and rehearse RLS with student and teacher identities.
- Build the production browser sign-in/session and teacher authorization flow.
- Exercise live OpenAI, verified cache, and outage fallback with real server-side credentials.
- Decide and implement scheduled cleanup for expired temporary demo participants before treating the mechanism as anything beyond a fictional non-production walkthrough.

```

### DESIGN.md

```markdown
# Rung Design System

Production UI contract. Every screen is built from these tokens and primitives — no ad-hoc colors, no hardcoded hex, no gradients.

## Identity

Rung is a differentiated-instruction math platform. The visual language is **calm, precise, and a little editorial** — a trustworthy instrument, not a toy. Two motifs: the **ladder/rung** (ordered progress) and a **blueprint** feel (mono labels, hairline rules, exact data).

## Hard rules (avoid AI tells)

- **No gradients.** Flat, considered fills only.
- No indigo/violet, no default Tailwind `slate/indigo/emerald` utility colors. Use tokens.
- No `rounded-lg` on everything — use the radius scale deliberately (`rounded-md` for controls, `rounded-xl` for cards, sharp for data grids).
- No emoji as section markers. No everything-centered layouts. No purple-to-blue anything.
- Color is never the only signal — mastery states always carry a text label too.
- Style through **tokens** (`bg-surface`, `text-ink`, `text-accent`, `border-border`). Never write raw hex or `dark:` variants — the CSS variables flip themes automatically.

## Color tokens

Defined as CSS variables in `globals.css` (light + dark), exposed to Tailwind as named colors.

| Token | Tailwind class | Role |
| --- | --- | --- |
| `--bg` | `bg-bg` | Page ground (warm green-biased off-white / near-black) |
| `--surface` | `bg-surface` | Cards, panels |
| `--surface-2` | `bg-surface-2` | Muted fills, table headers |
| `--border` | `border-border` | Hairline rules |
| `--border-strong` | `border-border-strong` | Emphasis borders |
| `--ink` | `text-ink` | Primary text |
| `--ink-muted` | `text-ink-muted` | Secondary text |
| `--ink-faint` | `text-ink-faint` | Captions, disabled |
| `--accent` | `text-accent` / `bg-accent` | Evergreen — primary action, links, focus |
| `--accent-hover` | `bg-accent-hover` | Hover state of accent fills |
| `--accent-contrast` | `text-accent-contrast` | Text on accent fills |
| `--accent-soft` | `bg-accent-soft` | Tinted accent backgrounds |

### Mastery scale (semantic — separate from accent)

`bg-mastery-{none,support,developing,mastered}` + matching `text-mastery-*-fg`. Grey → clay → amber → green. Always paired with a text label.

## Typography

- **Sans** (`font-sans`, Hanken Grotesk): UI + display. Headings tight (`tracking-tight`), heavy (600–800).
- **Mono** (`font-mono`, IBM Plex Mono): eyebrow labels (uppercase, `tracking-wider`, `text-xs`), data, tabular numbers (`tabular-nums`).
- Scale: display `text-4xl/5xl`, h2 `text-2xl`, h3 `text-lg`, body `text-base`, caption `text-sm`. Body line length ≤ ~68ch.

## Primitives (`src/components/ui/`)

- `Button` — variants: `primary` (accent fill), `secondary` (surface + border), `ghost`. Sizes `sm|md`. Visible focus ring.
- `Card` — surface, border, `rounded-xl`, subtle shadow. Optional `interactive` (hover border/lift).
- `Badge` — small mono/label pill; `tone` prop maps to mastery or neutral.
- `Eyebrow` — uppercase mono kicker.
- `PageHeader`
[truncated — 827 more characters]
```

### package.json

```
{
  "name": "rung",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "node ./node_modules/vitest/vitest.mjs run",
    "test:watch": "node ./node_modules/vitest/vitest.mjs",
    "seed": "tsx supabase/seed.ts",
    "reset-demo": "npm run seed"
  },
  "engines": {
    "node": ">=20"
  },
  "dependencies": {
    "@supabase/supabase-js": "^2.49.1",
    "@vercel/analytics": "^2.0.1",
    "next": "^15.2.0",
    "openai": "^4.85.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@types/node": "^22.13.4",
    "@types/react": "^19.0.8",
    "@types/react-dom": "^19.0.3",
    "autoprefixer": "^10.4.20",
    "tailwindcss": "^3.4.17",
    "tsx": "^4.19.3",
    "typescript": "^5.7.3",
    "vitest": "^3.0.7"
  }
}

```

### src/app/layout.tsx

```typescript
// Root document shell: loads self-hosted fonts, global tokens, and shared metadata.
import type { Metadata } from "next";
import { Hanken_Grotesk, IBM_Plex_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/next";
import "./globals.css";

const sans = Hanken_Grotesk({
  subsets: ["latin"],
  variable: "--font-sans",
  display: "swap"
});

const mono = IBM_Plex_Mono({
  subsets: ["latin"],
  weight: ["400", "500", "600"],
  variable: "--font-mono",
  display: "swap"
});

export const metadata: Metadata = {
  title: "Rung: differentiated fractions practice",
  description:
    "A classroom platform that turns one assignment into a per-student diagnostic, targeted practice, and a teacher-ready small-group plan."
};

// Runs before first paint so the stored theme preference (light/dark) is stamped onto <html>
// ahead of hydration, avoiding a flash of the wrong theme. "system" (or no stored value) leaves
// data-theme unset so the prefers-color-scheme media query in globals.css takes over.
const NO_FLASH_THEME_SCRIPT = `
(function () {
  try {
    var v = localStorage.getItem("rung-theme");
    if (v === "light" || v === "dark") {
      document.documentElement.setAttribute("data-theme", v);
    } else {
      document.documentElement.removeAttribute("data-theme");
    }
  } catch (e) {}
})();
`;

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" className={`${sans.variable} ${mono.variable}`} suppressHydrationWarning>
      <head>
        {/* Inline (not next/script) so this runs synchronously before first paint. */}
        <script dangerouslySetInnerHTML={{ __html: NO_FLASH_THEME_SCRIPT }} />
      </head>
      {/* Browser extensions (translation tools, etc.) inject attributes/classes onto <body>
          before React hydrates, causing a benign attribute mismatch. Suppress the warning on this
          one element — it only tolerates body-level extension noise, not real hydration bugs. */}
      <body suppressHydrationWarning>
        {children}
        <Analytics />
      </body>
    </html>
  );
}

```

### src/app/page.tsx

```typescript
// Landing page: states the product thesis, walks the student loop, and previews the teacher payoff.
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { HowItWorksSection } from "@/components/landing/how-it-works-section";
import { SectionConnector } from "@/components/landing/section-connector";
import { Badge, Card, Eyebrow, buttonClasses } from "@/components/ui";

const masteryPreview: Array<{ label: string; swatchClass: string }> = [
  { label: "Not started", swatchClass: "bg-mastery-none" },
  { label: "Needs support", swatchClass: "bg-mastery-support" },
  { label: "Developing", swatchClass: "bg-mastery-developing" },
  { label: "Mastered", swatchClass: "bg-mastery-mastered" }
];

// A small illustrative class heatmap — four students (rows) by four sub-skills (columns) — built
// from the same four mastery tokens as the legend below it, so "at a glance" actually shows one.
const heatmapPreview: string[][] = [
  [masteryPreview[3].swatchClass, masteryPreview[2].swatchClass, masteryPreview[1].swatchClass, masteryPreview[0].swatchClass],
  [masteryPreview[2].swatchClass, masteryPreview[3].swatchClass, masteryPreview[3].swatchClass, masteryPreview[1].swatchClass],
  [masteryPreview[1].swatchClass, masteryPreview[1].swatchClass, masteryPreview[2].swatchClass, masteryPreview[3].swatchClass],
  [masteryPreview[0].swatchClass, masteryPreview[2].swatchClass, masteryPreview[3].swatchClass, masteryPreview[2].swatchClass]
];

// Decorative ladder rungs for the side margins on very wide screens: the climb motif makes the
// quiet space beside the centered column read as designed, not empty. Purely visual — aria-hidden.
const RUNG_MOTIF_OPACITIES = [0.16, 0.3, 0.45, 0.62, 0.8];

function RungMotif({ side }: { side: "left" | "right" }) {
  return (
    <div
      aria-hidden="true"
      className={`pointer-events-none absolute inset-y-0 hidden flex-col justify-center gap-10 xl:flex 2xl:gap-12 ${
        side === "left" ? "left-2 2xl:left-10" : "right-2 2xl:right-10"
      }`}
    >
      {RUNG_MOTIF_OPACITIES.map((opacity, index) => (
        <span key={index} className="h-1 w-12 rounded-full bg-border-strong 2xl:w-16" style={{ opacity }} />
      ))}
    </div>
  );
}

function MiniHeatmap({ rows }: { rows: string[][] }) {
  return (
    <div className="grid grid-cols-4 gap-1.5" role="presentation">
      {rows.flatMap((row, rowIndex) =>
        row.map((swatchClass, colIndex) => (
          <span
            key={`${rowIndex}-${colIndex}`}
            aria-hidden
            className={`h-4 w-4 rounded-sm sm:h-5 sm:w-5 ${swatchClass}`}
          />
        ))
      )}
    </div>
  );
}

export default function HomePage() {
  return (
    <AppShell>
      <>
        <section className="relative pb-20 pt-6 sm:pt-10">
          {/* Soft spark-gold pool of light behind the hero card, echoing the diagnostic surface's
              ambient treatment so the marketing page reads as the same designed canvas. */}
          <div
            aria-hidden="true"
            className="pointer-events-none absolute left-1/2 top-1/2 h-[26rem] w-full max-w-[52rem] -translate-x-1/2 -translate-y-1/2 opacity-70 blur-3xl"
            style={{ background: "radial-gradient(closest-side, var(--spark-soft), transparent)" }}
          />
          <RungMotif side="left" />
          <RungMotif side="right" />

          <Card className="animate-rise !bg-elevated relative overflow-hidden p-8 shadow-lg sm:p-12">
            <div className="grid gap-10 lg:grid-cols-[1.4fr_1fr] lg:items-center">
              <div>
                <Eyebrow>Rung</Eyebrow>
                <h1 className="mt-3 text-balance text-4xl font-extrabold tracking-tight text-ink sm:text-5xl">
                  Meet every student on the rung they&rsquo;re actually on.
                </h1>
                <p className="mt-5 max-w-2xl text-lg text-ink-muted">
                  Teachers create a workspace and share a join code. Students take a short
                  check-in, choose focused practice for skills that are not yet mastered, and get safe
                  AI support when they are stuck. Their stored evidence becomes a teacher&rsquo;s next
                  small-group move.
                </p>
              </div>

              <div className="rounded-lg border border-border bg-surface p-5 shadow-sm">
                <p className="font-mono text-xs font-medium uppercase tracking-wider text-ink-muted">
                  Mastery, at a glance
                </p>
                <div className="mt-4">
                  <MiniHeatmap rows={heatmapPreview} />
                </div>
                <ul className="mt-4 grid grid-cols-2 gap-x-4 gap-y-2">
                  {masteryPreview.map((level) => (
                    <li className="flex items-center gap-2 text-xs text-ink" key={level.label}>
                      <span aria-hidden className={`h-2.5 w-2.5 shrink-0 rounded-sm ${level.swatchClass}`} />
                      {level.label}
                    </li>
                  ))}
                </ul>
                <p className="mt-4 border-t border-border pt-3 text-xs text-ink-faint">
                  Deterministic, stored evidence, never model-generated.
                </p>
              </div>
            </div>

            <div className="mt-8 flex flex-wrap items-center gap-x-4 gap-y-3">
              <Link href="/demo" className={buttonClasses("primary", "lg", "shadow-md hover:shadow-lg")}>
                Start the demo
              </Link>
              <Link href="/teacher-workspace" className={buttonClasses("secondary", "lg")}>
                Create a teacher workspace
              </Link>
              <Link
                href="/teacher/dashboard"
                className="text-sm font-medium text-accent underline-offset-4 hover:underline"
              >
                View sample class data <span aria-hidden="true">&rarr;</span>
              </Link>
            </div>
          
[truncated — 5911 more characters]
```

### src/components/ui/index.ts

```typescript
// Barrel for Rung UI primitives.
export { cn } from "./cn";
export { Button, buttonClasses, type ButtonVariant, type ButtonSize } from "./button";
export { Card } from "./card";
export { Badge, type BadgeTone } from "./badge";
export { Eyebrow } from "./eyebrow";
export { PageHeader } from "./page-header";
export { VideoEmbed } from "./video-embed";

```

### src/app/join-class/page.tsx

```typescript
import { Suspense } from "react";
import { StudentShell } from "@/components/student/surface/student-shell";
import { StudentJoinForm } from "@/components/teacher-workspace/student-join-form";

export default function JoinClassPage() {
  return (
    <StudentShell exitHref="/">
      <section className="relative flex flex-1 items-center justify-center">
        {/* Soft spark-gold pool of light behind the card, matching the diagnostic stage. */}
        <div
          aria-hidden="true"
          className="pointer-events-none absolute left-1/2 top-1/2 h-[26rem] w-full max-w-[52rem] -translate-x-1/2 -translate-y-1/2 opacity-70 blur-3xl"
          style={{ background: "radial-gradient(closest-side, var(--spark-soft), transparent)" }}
        />
        <div className="relative">
          <Suspense fallback={<p className="text-ink-muted">Loading class join…</p>}><StudentJoinForm /></Suspense>
        </div>
      </section>
    </StudentShell>
  );
}

```

### src/lib/supabase/server.ts

```typescript
// Server-only Supabase clients. Never import this module from browser code.
import { createClient } from "@supabase/supabase-js";

function requiredEnvironment(name: "NEXT_PUBLIC_SUPABASE_URL" | "NEXT_PUBLIC_SUPABASE_ANON_KEY" | "SUPABASE_SERVICE_ROLE_KEY") {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required environment variable: ${name}`);
  return value;
}

/**
 * Privileged server-only client. Service-role requests bypass RLS and therefore
 * belong only in validated route handlers, server actions, and seed scripts.
 */
export function createServerSupabaseClient() {
  return createClient(
    requiredEnvironment("NEXT_PUBLIC_SUPABASE_URL"),
    requiredEnvironment("SUPABASE_SERVICE_ROLE_KEY"),
    { auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false } },
  );
}

/**
 * RLS-respecting server client for a request that has already supplied a
 * verified Supabase access token. Route handlers should pass the bearer token
 * through this client rather than use the service role for user-scoped reads.
 */
export function createServerSessionSupabaseClient(accessToken: string) {
  if (!accessToken) throw new Error("A Supabase access token is required for a session client.");
  return createClient(
    requiredEnvironment("NEXT_PUBLIC_SUPABASE_URL"),
    requiredEnvironment("NEXT_PUBLIC_SUPABASE_ANON_KEY"),
    {
      auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
      global: { headers: { Authorization: `Bearer ${accessToken}` } },
    },
  );
}

```

### src/app/teacher-workspace/page.tsx

```typescript
import { cookies } from "next/headers";
import { AppShell } from "@/components/app-shell";
import { TeacherWorkspaceDashboard } from "@/components/teacher-workspace/workspace-dashboard";
import { TeacherWorkspaceSetupForm } from "@/components/teacher-workspace/setup-form";
import { isTeacherWorkspaceDemoMode, resolveTeacherWorkspaceSession, TEACHER_WORKSPACE_COOKIE } from "@/lib/teacher-workspace/session";
import { getTeacherAssignedFollowUps, getTeacherEvidenceByStudentIds } from "@/lib/teacher/repository";

export const dynamic = "force-dynamic";

export default async function TeacherWorkspacePage() {
  if (!isTeacherWorkspaceDemoMode()) return <AppShell active="teacher"><p className="text-ink-muted">This non-production teacher workspace is unavailable.</p></AppShell>;
  const token = (await cookies()).get(TEACHER_WORKSPACE_COOKIE)?.value;
  const result = await resolveTeacherWorkspaceSession(new Request("http://local/teacher-workspace", { headers: token ? { cookie: `${TEACHER_WORKSPACE_COOKIE}=${encodeURIComponent(token)}` } : {} }));
  if (result.kind !== "resolved") return <AppShell active="teacher" width="wide"><TeacherWorkspaceSetupForm /></AppShell>;
  // Scoped to this workspace's own roster, so it cannot surface an answer from
  // a learner in another class.
  const studentIds = result.workspace.students.map((student) => student.id);
  const [responseEvidenceByStudent, assignedFollowUps] = await Promise.all([
    getTeacherEvidenceByStudentIds(studentIds, result.workspace.classId),
    getTeacherAssignedFollowUps(studentIds),
  ]);
  return (
    <AppShell active="teacher" width="wide">
      <TeacherWorkspaceDashboard assignedFollowUps={assignedFollowUps} responseEvidenceByStudent={responseEvidenceByStudent} workspace={result.workspace} />
    </AppShell>
  );
}

```

### src/app/demo/page.tsx

```typescript
// Entry screen for starting a cookie-bound temporary learner session.
import { StudentShell } from "@/components/student/surface/student-shell";
import { StartClimbForm } from "@/components/demo/start-climb-form";
import { Card } from "@/components/ui";

// Decorative ladder rungs for the side margins on very wide screens — mirrors the diagnostic
// intro's motif so this "step zero" screen feels like the same composition. Purely visual —
// aria-hidden. Duplicated locally (not imported) per this page's file-ownership boundary.
const RUNG_MOTIF_OPACITIES = [0.16, 0.3, 0.45, 0.62, 0.8];

function RungMotif({ side }: { side: "left" | "right" }) {
  return (
    <div
      aria-hidden="true"
      className={`pointer-events-none absolute inset-y-0 hidden flex-col justify-center gap-10 xl:flex 2xl:gap-12 ${
        side === "left" ? "left-2 2xl:left-10" : "right-2 2xl:right-10"
      }`}
    >
      {RUNG_MOTIF_OPACITIES.map((opacity, index) => (
        <span
          key={index}
          className="h-1 w-12 rounded-full bg-border-strong 2xl:w-16"
          style={{ opacity }}
        />
      ))}
    </div>
  );
}

export default function DemoPage() {
  return (
    <StudentShell exitHref="/" size="wide">
      {/* One centered composition — headline above, the Maya card as the single lit focal
          object below — so this screen reads as the same house style as the diagnostic intro
          it leads into, rather than left-anchored content floating in a wide void. */}
      <section className="relative flex flex-1 items-center justify-center">
        {/* Soft spark-gold pool of light behind the card so the focal object sits in a lit spot
            on the canvas instead of floating on one flat wash. */}
        <div
          aria-hidden="true"
          className="pointer-events-none absolute left-1/2 top-1/2 h-[26rem] w-full max-w-[52rem] -translate-x-1/2 -translate-y-1/2 opacity-70 blur-3xl"
          style={{ background: "radial-gradient(closest-side, var(--spark-soft), transparent)" }}
        />
        <RungMotif side="left" />
        <RungMotif side="right" />

        <div className="relative mx-auto w-full max-w-2xl py-8">
          <div className="animate-rise mx-auto max-w-lg text-center">
            <h1 className="text-balance text-4xl font-extrabold tracking-tight text-ink sm:text-5xl">
              Let&rsquo;s find your next climb.
            </h1>
            <p className="mt-4 text-pretty text-lg text-ink-muted">
              Every learner starts on a different rung. Enter a name to take a quick check-in, work
              through focused practice, and see your own learning evidence appear in the class view.
            </p>
          </div>

          {/* The name step creates a server-owned temporary learner before the
              student route begins. The existing visual treatment stays intact. */}
          <Card className="animate-rise mt-10 flex flex-col items-center gap-5 rounded-2xl border-border-strong bg-elevated p-8 text-center shadow-lg">
            <StartClimbForm />
          </Card>
        </div>
      </section>
    </StudentShell>
  );
}

```

[190 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]