diff --git a/src/app/(teacher)/layout.tsx b/src/app/(teacher)/layout.tsx index 9ed4c4b4..6c0b2a1f 100644 --- a/src/app/(teacher)/layout.tsx +++ b/src/app/(teacher)/layout.tsx @@ -1,9 +1,18 @@ import { Nav } from '@/components/Nav'; +/** "learning-commons" → "Learning Commons" for the nav's source indicator. */ +function activeSourceLabel(): string { + const id = (process.env.STANDARDS_SOURCE ?? 'learning-commons').split(',')[0]?.trim() ?? ''; + return id + .split('-') + .map((word) => (word ? word[0].toUpperCase() + word.slice(1) : word)) + .join(' '); +} + export default function TeacherLayout({ children }: { children: React.ReactNode }) { return (
-
); diff --git a/src/app/(teacher)/pathways/[sessionId]/page.tsx b/src/app/(teacher)/pathways/[sessionId]/page.tsx index 35662c21..308c6930 100644 --- a/src/app/(teacher)/pathways/[sessionId]/page.tsx +++ b/src/app/(teacher)/pathways/[sessionId]/page.tsx @@ -23,16 +23,15 @@ export default async function PathwayReportPage({ href="/pathways" className="text-sm text-muted-foreground hover:text-foreground transition-colors" > - ← All Pathways + ← All pathways

- {session?.topic ?? 'Pathway Report'} + {session?.topic ?? 'Pathway report'}

diff --git a/src/app/(teacher)/pathways/page.tsx b/src/app/(teacher)/pathways/page.tsx index 5cd8f1b7..602f3eb6 100644 --- a/src/app/(teacher)/pathways/page.tsx +++ b/src/app/(teacher)/pathways/page.tsx @@ -4,8 +4,7 @@ export const metadata = { title: 'Pathways' }; export default function PathwaysPage() { return ( -
-

Pathways

+
); diff --git a/src/app/(teacher)/roster/page.tsx b/src/app/(teacher)/roster/page.tsx index 21cf3505..9c6b9192 100644 --- a/src/app/(teacher)/roster/page.tsx +++ b/src/app/(teacher)/roster/page.tsx @@ -1,7 +1,7 @@ import { Suspense } from 'react'; import { RosterPage } from '@/components/roster/RosterPage'; -export const metadata = { title: 'Class Roster' }; +export const metadata = { title: 'Class roster' }; export default function Page() { return ( diff --git a/src/app/api/telemetry/route.ts b/src/app/api/telemetry/route.ts index 085774b5..b1599456 100644 --- a/src/app/api/telemetry/route.ts +++ b/src/app/api/telemetry/route.ts @@ -42,7 +42,14 @@ export async function POST(request: Request) { learningComponentId: event.learningComponentId, elapsedMs: event.elapsedMs, correct: event.correct, - payload: truncate(event.payload), + // stepIndex rides inside payload — the interactions table has no + // column for it, and the report's per-step evidence strip reads it + // back out. Dropping it here is what made per-step reporting + // impossible before. + payload: + event.stepIndex != null + ? { ...truncate(event.payload), stepIndex: event.stepIndex } + : truncate(event.payload), })), ); } catch (error) { diff --git a/src/app/demo/defend-claim/page.tsx b/src/app/demo/defend-claim/page.tsx index 44c249fa..e37e09e8 100644 --- a/src/app/demo/defend-claim/page.tsx +++ b/src/app/demo/defend-claim/page.tsx @@ -118,7 +118,7 @@ export default function DefendClaimDemo() { > Widget gallery -

Defend a Claim

+

Defend a claim

Take a side on a claim historians argue about, defend it from the sources, then ask for feedback and revise. Nothing is judged until you press the button — and every reading diff --git a/src/app/demo/draft-meter/page.tsx b/src/app/demo/draft-meter/page.tsx index 9f7a902a..e767e54e 100644 --- a/src/app/demo/draft-meter/page.tsx +++ b/src/app/demo/draft-meter/page.tsx @@ -167,7 +167,7 @@ export default function DraftMeterDemo() { > Widget gallery -

Draft Meter

+

Draft meter

Question, textbox, one line. Same component and same scoring call in all four of these — what changes is the three things each one is looking for, and those come from the standard. diff --git a/src/app/demo/draw-the-curve/page.tsx b/src/app/demo/draw-the-curve/page.tsx index b11cc562..81e37293 100644 --- a/src/app/demo/draw-the-curve/page.tsx +++ b/src/app/demo/draw-the-curve/page.tsx @@ -156,7 +156,7 @@ export default function DrawTheCurveDemo() { > Widget gallery -

Draw the Curve

+

Draw the curve

Shape the line by dragging its points, then see the real one drawn over your guess. Checked on shape, not numbers — so a story's tension arc works the same way a motion graph does. diff --git a/src/app/demo/find-the-flaw/page.tsx b/src/app/demo/find-the-flaw/page.tsx index b9202290..6ac1d54c 100644 --- a/src/app/demo/find-the-flaw/page.tsx +++ b/src/app/demo/find-the-flaw/page.tsx @@ -151,7 +151,7 @@ export default function FindTheFlawDemo() { > Widget gallery -

Find the Flaw

+

Find the flaw

A worked example with one mistake in it. Find where it goes wrong, then say why — the same interaction whether the work is a calculation, an experiment, an argument or an explanation. diff --git a/src/app/demo/fraction-area-model/page.tsx b/src/app/demo/fraction-area-model/page.tsx index c280ccbf..f90d3356 100644 --- a/src/app/demo/fraction-area-model/page.tsx +++ b/src/app/demo/fraction-area-model/page.tsx @@ -44,7 +44,7 @@ export default function FractionAreaModelDemo() { > Widget gallery -

Fraction Area Model

+

Fraction area model

Partition a whole into equal parts and select segments to build a target fraction. Switch denominator choices to explore equivalent forms. diff --git a/src/app/demo/markdown-card/page.tsx b/src/app/demo/markdown-card/page.tsx index 2103482f..9dda7dd9 100644 --- a/src/app/demo/markdown-card/page.tsx +++ b/src/app/demo/markdown-card/page.tsx @@ -95,7 +95,7 @@ export default function MarkdownCardDemo() { > Widget gallery -

Markdown Card

+

Markdown card

Renders LLM-generated markdown as a reading card for reinforcement.

diff --git a/src/app/demo/narrated-card/page.tsx b/src/app/demo/narrated-card/page.tsx index b730f23f..f7a468d7 100644 --- a/src/app/demo/narrated-card/page.tsx +++ b/src/app/demo/narrated-card/page.tsx @@ -47,7 +47,7 @@ export default function NarratedCardDemo() { > Widget gallery -

Narrated Card

+

Narrated card

Reads content aloud sentence by sentence, highlighting each sentence as it is spoken.

diff --git a/src/app/demo/page.tsx b/src/app/demo/page.tsx index a18c92fb..1b370ca9 100644 --- a/src/app/demo/page.tsx +++ b/src/app/demo/page.tsx @@ -6,28 +6,28 @@ import { WidgetThumb } from '@/components/demo/WidgetThumb'; const WIDGETS = [ { slug: 'fraction-area-model', - name: 'Fraction Area Model', + name: 'Fraction area model', description: 'Partition a whole into equal parts and select segments to build a target fraction. Supports bar and circle representations with equivalent-fraction detection.', tags: ['fractions', 'visual', 'manipulative'], }, { slug: 'swiper-flashcard', - name: 'Swiper Flashcard', + name: 'Swiper flashcard', description: 'Swipe a card up or down to answer a question or sort a statement into a category. Supports drag gestures and emits a completion event with per-card results.', tags: ['flashcards', 'true/false', 'sorting'], }, { slug: 'draft-meter', - name: 'Draft Meter', + name: 'Draft meter', description: 'Write a short response and one line scores how strong the argument is, from a live model call on a debounce. Optionally supplies a source passage, so "evidence" means citing the text.', tags: ['writing', 'argument', 'live scoring'], }, { slug: 'draw-the-curve', - name: 'Draw the Curve', + name: 'Draw the curve', description: 'Shape a line by dragging its points to predict how something changes, then see the real curve drawn over your guess. Checked on shape rather than numbers, so a story arc works the same way a motion graph does.', tags: ['predict-then-reveal', 'any subject', 'chart'], @@ -41,28 +41,28 @@ const WIDGETS = [ }, { slug: 'writing-workshop', - name: 'Writing Workshop', + name: 'Writing workshop', description: 'Long-form writing — essay, lab report, research proposal, short story — reviewed only when the student asks, and marked up on the sentences themselves with a note on each.', tags: ['long-form', 'on demand', 'annotation'], }, { slug: 'find-the-flaw', - name: 'Find the Flaw', + name: 'Find the flaw', description: 'A worked example with one deliberate mistake — a solution, an experiment, an argument, an explanation. The student finds the step where it goes wrong, then diagnoses why. Checked locally, so there is no wait.', tags: ['error analysis', 'any subject', 'metacognition'], }, { slug: 'defend-claim', - name: 'Defend a Claim', + name: 'Defend a claim', description: 'Take a side on a contestable historical claim, defend it from two conflicting sources, then request feedback and revise. Every reading answers with a counterargument. Typed or dictated. History, grade 7+.', tags: ['history', 'argument', 'voice', 'revision'], }, { slug: 'timeline-builder', - name: 'Timeline Builder', + name: 'Timeline builder', description: 'Drag historical events from a bank into labeled period zones on a horizontal timeline. Supports 3–5 zones with per-event correctness feedback.', tags: ['history', 'sequencing', 'timeline'], @@ -90,20 +90,20 @@ const WIDGETS = [ }, { slug: 'narrated-card', - name: 'Narrated Card', + name: 'Narrated card', description: 'Reads content aloud using the browser\'s text-to-speech engine, revealing each sentence as it is spoken. Steps stack up as they complete.', tags: ['audio', 'narration', 'accessibility'], }, { slug: 'step-reveal', - name: 'Step Reveal', + name: 'Step reveal', description: 'Walk through a concept or worked example one step at a time. Each step stacks up so students can follow the full chain of reasoning.', tags: ['worked example', 'step-by-step', 'reasoning'], }, { slug: 'markdown-card', - name: 'Markdown Card', + name: 'Markdown card', description: 'Renders LLM-generated markdown as a clean reading card — headings, bold, bullets, blockquotes, and an optional tip callout. Used to re-teach a concept a student is struggling with.', tags: ['reading', 'remediation', 'markdown'], diff --git a/src/app/demo/step-reveal/page.tsx b/src/app/demo/step-reveal/page.tsx index 8074d6d1..b58bb6f5 100644 --- a/src/app/demo/step-reveal/page.tsx +++ b/src/app/demo/step-reveal/page.tsx @@ -71,7 +71,7 @@ export default function StepRevealDemo() { > Widget gallery -

Step Reveal

+

Step reveal

Walk through a worked example one step at a time. Each step stacks up so students can follow the full chain of reasoning.

diff --git a/src/app/demo/swiper-flashcard/page.tsx b/src/app/demo/swiper-flashcard/page.tsx index 1c12f958..dc679d09 100644 --- a/src/app/demo/swiper-flashcard/page.tsx +++ b/src/app/demo/swiper-flashcard/page.tsx @@ -83,7 +83,7 @@ export default function SwiperFlashcardDemo() { > Widget gallery -

Swiper Flashcard

+

Swiper flashcard

Sort each clue into the correct plate boundary type. Drag the card up or down — or use the buttons. An{' '} onComplete{' '} diff --git a/src/app/demo/timeline-builder/page.tsx b/src/app/demo/timeline-builder/page.tsx index 4e21fc74..700edfa0 100644 --- a/src/app/demo/timeline-builder/page.tsx +++ b/src/app/demo/timeline-builder/page.tsx @@ -39,7 +39,7 @@ export default function TimelineBuilderDemo() { > Widget gallery -

Timeline Builder

+

Timeline builder

Drag events from the bank into the correct period on the timeline.

diff --git a/src/app/demo/writing-workshop/page.tsx b/src/app/demo/writing-workshop/page.tsx index c4d5b7ea..c185def3 100644 --- a/src/app/demo/writing-workshop/page.tsx +++ b/src/app/demo/writing-workshop/page.tsx @@ -118,7 +118,7 @@ export default function WritingWorkshopDemo() { > Widget gallery -

Writing Workshop

+

Writing workshop

Long-form writing, marked up when you ask. Nothing watches while you draft — then the read comes back on the sentences themselves, underlined where they work and where they diff --git a/src/app/games/page.tsx b/src/app/games/page.tsx index e526af72..c24bda3b 100644 --- a/src/app/games/page.tsx +++ b/src/app/games/page.tsx @@ -11,14 +11,14 @@ type GameType = 'menu' | 'memory' | 'scramble' | 'pacman'; const GAMES = [ { id: 'memory' as const, - name: 'Memory Match', + name: 'Memory match', emoji: '🎴', description: 'Find matching pairs', component: MemoryMatch, }, { id: 'scramble' as const, - name: 'Word Scramble', + name: 'Word scramble', emoji: '🔤', description: 'Unscramble the letters', component: WordScramble, diff --git a/src/app/globals.css b/src/app/globals.css index 960ba3a9..2e35fb73 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -7,9 +7,10 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --font-heading: var(--font-sans); + --font-sans: var(--font-plex-sans); + --font-mono: var(--font-plex-mono); + --font-heading: var(--font-archivo); + --font-serif: var(--font-source-serif); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -44,6 +45,21 @@ --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); + /* Practice Pathways design tokens — the handoff palette, beyond shadcn's roles. */ + --color-sunk: var(--sunk); + --color-track: var(--track); + --color-ink: var(--foreground); + --color-ink-2: var(--ink-2); + --color-ink-3: var(--muted-foreground); + --color-brand-fill: var(--brand-fill); + --color-brand-fill-hover: var(--brand-fill-hover); + --color-brand-press: var(--brand-press); + --color-brand-text: var(--primary-tint-foreground); + --color-verified: var(--success); + --color-verified-tint: var(--verified-tint); + --color-verified-edge: var(--verified-edge); + --color-warning-tint: var(--warning-tint); + --color-warning-edge: var(--warning-edge); --radius-sm: calc(var(--radius) * 0.6); --radius-md: calc(var(--radius) * 0.8); --radius-lg: var(--radius); @@ -54,138 +70,185 @@ } /* - * `.light-surface` re-declares this same palette for one subtree. The student - * surfaces (/learn and a shared pathway) hard-code pastel light chrome — a - * violet/pink gradient and white cards, with no dark variants — but the widgets - * they host are token-themed. Without this, a student in dark mode gets - * `--foreground` near-white on those hard-coded white cards, i.e. invisible - * feedback text, and `bg-card` chips render near-black. + * Practice Pathways palette: warm graphite ink + one highlighter, semantic + * colours that never double as decoration, warm-biased neutrals, and light and + * dark designed as siblings. Every text/surface pair below was checked against + * WCAG 2.2 AA (4.5:1 body, 3:1 large/UI) — the ratios live in the design + * handoff; re-check any substitution. + * + * Two rules that are not stylistic: + * 1. The highlighter (--brand-fill) is a marker, never a surface: as a fill it + * always carries a 1px ink border, and it never appears in chip/status form. + * 2. Semantic colours always ship with an icon and a word; colour alone never + * conveys verification state. + * + * `.light-surface` still re-declares the light palette for the student + * subtree; it is scheduled for deletion in the walkthrough redesign slice, + * when /learn's hard-coded pastel chrome becomes token-themed. */ :root, .light-surface { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - /* - * The one confident brand hue. The base shadcn theme this builds on ships - * pure greyscale; this app has two faces — a teacher-facing builder and a - * student-facing /learn — and they need to read as the same product. - * /learn already has a violet identity (its gradient background, its mic - * button), so primary is built around that hue rather than introducing a - * third color language. Chosen for contrast, not just vibe: 6.31:1 against - * --primary-foreground, comfortably past the 4.5:1 text floor everything - * else in this file holds to. - */ - --primary: oklch(0.5 0.2 292); - --primary-foreground: oklch(0.985 0 0); - /* For brand-hued text sitting on a light primary-tinted surface (a badge, - not a solid button) — identical to --primary in light mode, where it - already clears 4.5:1; dark mode needs its own lighter value below. */ - --primary-tint-foreground: var(--primary); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - /* Widget feedback and manipulative fills. The base theme's palette is - greyscale, so these are the only chromatic tokens: correct, - needs-another-look, and the "student selected this part" fill shared - by every manipulative. */ - /* Lightness is set so feedback text clears WCAG AA (4.5:1) against --card, - matching the shipped --destructive. The obvious emerald/amber sit at - ~3.5:1 and fail — this is student-facing copy, so it has to hold. */ - --success: oklch(0.528 0.145 163.225); - --warning: oklch(0.56 0.153 70.08); - /* Selected vs unselected parts differ only by fill, so the pair has to clear - the 3:1 non-text contrast bar (WCAG 1.4.11) on its own. */ + --background: #fcfaf7; /* paper */ + --foreground: #1f1915; /* ink */ + --ink-2: #58514c; + --card: #ffffff; + --card-foreground: #1f1915; + --popover: #ffffff; + --popover-foreground: #1f1915; + --sunk: #f6f3ef; + --track: #e8e4dd; /* progress tracks — one step deeper than sunk */ + /* Primary action = the highlighter with ink text (12.33:1). The mandatory + 1px ink border is applied by the button styles, not the token. */ + --primary: #e3df41; + --primary-foreground: #1f1915; + --brand-fill: #e3df41; + --brand-fill-hover: #f2ef63; + --brand-press: #8e8b0f; + /* Brand-coloured text on light surfaces (7.62:1). */ + --primary-tint-foreground: #595600; + --secondary: #f6f3ef; + --secondary-foreground: #1f1915; + --muted: #f6f3ef; + --muted-foreground: #736c66; /* ink-3, 5.18:1 on card */ + --accent: #f6f3ef; + --accent-foreground: #1f1915; + --destructive: #bd093f; /* 6.41:1 on card */ + --success: #1c6d26; /* verified, 6.45:1 on card */ + --verified-tint: #dbf3db; + --verified-edge: #b6e3b8; + --warning: #775400; /* unverified / needs review, 6.87:1 on card */ + --warning-tint: #fcebc7; + --warning-edge: #eed49a; + /* Manipulative selection fill — kept from the previous system; its hue + (237°) sits safely outside the brand/warning band the guest hues avoid. */ --selected: oklch(0.639 0.169 237.323); - --selected-foreground: oklch(0.985 0 0); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - /* Focus rings pick up the brand hue too, at low chroma — a warm glow - instead of a flat grey outline, without competing with --primary itself. */ - --ring: oklch(0.708 0.08 292); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - /* Rounder than the base theme's default (10px), short of /learn's explicit - rounded-2xl/3xl treatments on its own bespoke elements — the "same - warmth, calmer register" the builder and /learn are meant to share. - Radius alone never affects text contrast, so this is safe to raise - without re-checking any of the ratios documented above. */ - --radius: 0.875rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --selected-foreground: #ffffff; + --border: #e1ddd8; /* rule — decorative separation only */ + --input: #e1ddd8; + /* Focus is always visible and always ink: the highlighter is too + low-contrast (1.41:1) to be a focus ring. */ + --ring: #1f1915; + --chart-1: #e1ddd8; + --chart-2: #736c66; + --chart-3: #58514c; + --chart-4: #3d3731; + --chart-5: #1f1915; + /* Teacher register is square. The student register raises this via + `.register-student` below — the only systematic difference between the + two registers. */ + --radius: 0rem; + --sidebar: #fcfaf7; + --sidebar-foreground: #1f1915; + --sidebar-primary: #1f1915; + --sidebar-primary-foreground: #fcfaf7; + --sidebar-accent: #f6f3ef; + --sidebar-accent-foreground: #1f1915; + --sidebar-border: #e1ddd8; + --sidebar-ring: #1f1915; + /* Purpose tags for PathwayPlan.steps[].purpose — fills and AA-checked text. */ + --purpose-activate-bg: #fcebc7; + --purpose-activate-fg: #775400; + --purpose-model-bg: #e5e9f6; + --purpose-model-fg: #33477f; + --purpose-practice-bg: #dbf3db; + --purpose-practice-fg: #1c6d26; + --purpose-check-bg: #f6e3ea; + --purpose-check-fg: #9b1745; } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - /* - * The base theme's dark mode default inverts to a near-white button with - * dark text — a convention for a *monochrome* primary. A hued one reads - * better the conventional way: a rich, visible fill with light text on - * top. Lightened - * from the light-mode value to stay legible against a near-black page - * (3.63:1 against --card) while still clearing 4.5:1 against its own - * --primary-foreground text. - */ - --primary: oklch(0.565 0.19 292); - --primary-foreground: oklch(0.985 0 0); - /* --primary itself is tuned to sit under light text as a button fill - (3.63:1 against --card is fine for a large filled area, but text this - small needs 4.5:1) — too dark to also work as small text on a dark, - lightly-tinted card. Lightened until it clears 4.5:1 in that role. */ - --primary-tint-foreground: oklch(0.65 0.16 292); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --success: oklch(0.765 0.177 163.223); - --warning: oklch(0.828 0.189 84.429); + --background: #13100d; /* ground — warm charcoal, not an inverted document */ + --foreground: #f5f3f1; + --ink-2: #bbb7b2; + --card: #1f1b18; + --card-foreground: #f5f3f1; + --popover: #1f1b18; + --popover-foreground: #f5f3f1; + --sunk: #292623; /* card-2 */ + --track: #393430; + /* The highlighter is identical in both themes; its text flips to the dark + ground (13.45:1) so "ink on the marker" stays true as a sibling rule. */ + --primary: #e3df41; + --primary-foreground: #13100d; + --brand-fill: #e3df41; + --brand-fill-hover: #f2ef63; + --brand-press: #797600; + --primary-tint-foreground: #dcd848; /* brand text, 11.37:1 on card */ + --secondary: #292623; + --secondary-foreground: #f5f3f1; + --muted: #292623; + --muted-foreground: #98938d; /* 5.60:1 on card */ + --accent: #292623; + --accent-foreground: #f5f3f1; + --destructive: #f65b72; /* 5.42:1 on card */ + --success: #70d482; /* 9.36:1 on card */ + --verified-tint: #16361c; + --verified-edge: #2c5c34; + --warning: #e7ba54; /* 9.42:1 on card */ + --warning-tint: #3a2b07; + --warning-edge: #6e5310; --selected: oklch(0.746 0.16 232.661); - --selected-foreground: oklch(0.205 0 0); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0.08 292); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --selected-foreground: #13100d; + --border: #393430; + --input: #393430; + --ring: #f5f3f1; + --chart-1: #393430; + --chart-2: #98938d; + --chart-3: #bbb7b2; + --chart-4: #d8d4d0; + --chart-5: #f5f3f1; + --sidebar: #13100d; + --sidebar-foreground: #f5f3f1; + --sidebar-primary: #f5f3f1; + --sidebar-primary-foreground: #13100d; + --sidebar-accent: #292623; + --sidebar-accent-foreground: #f5f3f1; + --sidebar-border: #393430; + --sidebar-ring: #f5f3f1; + --purpose-activate-bg: #3a2b07; + --purpose-activate-fg: #e7ba54; + --purpose-model-bg: #232b40; + --purpose-model-fg: #a9bcf0; + --purpose-practice-bg: #16361c; + --purpose-practice-fg: #70d482; + --purpose-check-bg: #3c1220; + --purpose-check-fg: #f2a2bc; +} + +/* + * The student register: same palette, same type, same semantics — rounded. + * Cards 18–22px, inner elements 10–16px. Applied on the student subtree + * (/learn, shared pathways) alongside the register's own press-shadow + * button treatment. + */ +.register-student { + --radius: 1.25rem; +} + +/* + * Guest hue: each generated activity carries one model-chosen hue + * (--activity-h, a number), rendered at fixed lightness/chroma so contrast + * cannot move (any hue: 5.6–7.1:1 text-on-card light, 8.0–9.0:1 dark). The + * hue appears only on the activity's own skin — frame border, header fill, + * active drop target, kind label, score dial — never on chrome, buttons, + * chips, or states. + */ +.activity-skin { + --activity-text: oklch(0.48 0.14 var(--activity-h, 220)); + --activity-fill: oklch(0.62 0.16 var(--activity-h, 220)); + --activity-tint: oklch(0.95 0.04 var(--activity-h, 220)); + --activity-edge: oklch(0.85 0.06 var(--activity-h, 220)); +} +.dark .activity-skin { + --activity-text: oklch(0.78 0.13 var(--activity-h, 220)); + --activity-fill: oklch(0.7 0.15 var(--activity-h, 220)); + --activity-tint: oklch(0.3 0.05 var(--activity-h, 220)); + --activity-edge: oklch(0.42 0.11 var(--activity-h, 220)); } @layer base { * { - @apply border-border outline-ring/50; + @apply border-border; } body { @apply bg-background text-foreground; @@ -193,4 +256,31 @@ html { @apply font-sans; } -} \ No newline at end of file + /* Focus is always visible: 3px ink, offset 2 — on everything interactive. */ + :focus-visible { + outline: 3px solid var(--ring); + outline-offset: 2px; + } + h1, h2, h3, h4 { + font-family: var(--font-heading), var(--font-plex-sans), sans-serif; + font-stretch: 112%; + } +} + +@keyframes caret-blink { + 0%, 45% { opacity: 1; } + 50%, 95% { opacity: 0; } +} +.caret-blink { animation: caret-blink 1.1s steps(1) infinite; } +@media (prefers-reduced-motion: reduce) { + .caret-blink { animation: none; } +} + +@keyframes segment-grow { + from { width: 4%; } + to { width: 92%; } +} +.segment-grow { animation: segment-grow 9s cubic-bezier(0.2, 0.6, 0.3, 1) forwards; } +@media (prefers-reduced-motion: reduce) { + .segment-grow { animation: none; width: 100%; } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0e0a4d44..7ef65530 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,45 +1,58 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono, Lexend } from "next/font/google"; +import { Archivo, IBM_Plex_Mono, IBM_Plex_Sans, Source_Serif_4 } from "next/font/google"; import "./globals.css"; import { cn } from "@/lib/utils"; import { THEME_SCRIPT } from "@/lib/theme"; -const geistSans = Geist({ - variable: "--font-geist-sans", +/** + * The Practice Pathways type system, per the design handoff: + * Archivo carries headlines, UI labels and every numeral (its width axis is + * loaded so headlines can stretch 106–118%); IBM Plex Sans carries body copy + * and tables; IBM Plex Mono carries standard codes, timings and micro-labels; + * Source Serif 4 exists for student reading passages only — remediation + * prose, activity prompts, the student's own draft. + */ +const archivo = Archivo({ + variable: "--font-archivo", subsets: ["latin"], + axes: ["wdth"], }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const plexSans = IBM_Plex_Sans({ + variable: "--font-plex-sans", subsets: ["latin"], + weight: ["400", "500", "600", "700"], }); -/** - * Registered globally (fonts load once, here) but applied only locally on - * the pathway builder's own headings via `font-[family-name:var(--font-lexend)]` - * — not wired into the shared `--font-heading` token, which the Pathways - * dashboard, Roster, Upload, Nav, and the Crossword widget all still use. - * Lexend specifically: it's a typeface engineered and studied for reading - * proficiency, including K-12 classroom trials — an unusually literal fit - * for a tool that builds reading/learning pathways, not a default pick. - */ -const lexend = Lexend({ - variable: "--font-lexend", +const plexMono = IBM_Plex_Mono({ + variable: "--font-plex-mono", + subsets: ["latin"], + weight: ["400", "500", "600"], +}); + +const sourceSerif = Source_Serif_4({ + variable: "--font-source-serif", subsets: ["latin"], }); export const metadata: Metadata = { - title: "Topic to student pathway", + title: "Practice Pathways", description: - "Turn a topic into a standards-grounded learning pathway with an interactive widget.", + "Turn a topic into a standards-verified learning pathway of interactive activities.", }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/src/app/learn/page.tsx b/src/app/learn/page.tsx index c606e354..18dd6178 100644 --- a/src/app/learn/page.tsx +++ b/src/app/learn/page.tsx @@ -138,8 +138,8 @@ export default function LearnPage() { }); return ( -

-
+
+
{!session && ( - What do you want to{' '} - - learn? - + What do you want to learn? )} @@ -171,21 +168,21 @@ export default function LearnPage() { ? { repeat: Infinity, duration: 1.6 } : { type: 'spring', stiffness: 300, damping: 14 } } - className="flex size-36 flex-col items-center justify-center gap-1 rounded-full bg-gradient-to-br from-violet-500 to-pink-500 text-white shadow-[0_8px_0_0_#6d28d9] transition-transform active:translate-y-1 active:shadow-[0_3px_0_0_#6d28d9] disabled:opacity-50" + className="flex size-36 flex-col items-center justify-center gap-1 rounded-full border-2 border-foreground bg-brand-fill text-foreground shadow-[0_8px_0_0_var(--brand-press)] transition-transform active:translate-y-1 active:shadow-[0_3px_0_0_var(--brand-press)] disabled:opacity-50 motion-reduce:transition-none" > - {busy ? '✨' : voice.listening ? '👂' : '🎤'} + {busy ? '…' : voice.listening ? '●' : '🎤'} {busy ? 'Building' : voice.listening ? 'Listening' : 'Tap & talk'}
- {busy &&

{BUILDING_LINES[line]}

} + {busy &&

{BUILDING_LINES[line]}

} {!busy && voice.interim && ( -

“{voice.interim}”

+

“{voice.interim}”

)} {!busy && (error || voice.error) && ( -

🙃 {voice.error ?? error}

+

⚠ {voice.error ?? error}

)}
@@ -193,15 +190,15 @@ export default function LearnPage() { -

Did you say:

-

“{voiceCapture}”

+

Did you say:

+

“{voiceCapture}”

@@ -232,12 +229,12 @@ export default function LearnPage() { onChange={(event) => setTyped(event.target.value)} placeholder={voice.supported ? '…or type it here' : 'Type what you want to learn'} maxLength={200} - className="min-w-0 flex-1 rounded-2xl border-4 border-violet-200 bg-white px-5 py-3 font-semibold outline-none placeholder:text-violet-300 focus:border-violet-400" + className="min-w-0 flex-1 rounded-2xl border border-border bg-card px-5 py-3 font-semibold placeholder:text-muted-foreground focus:border-foreground" /> diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx index 5162f244..3a5905b1 100644 --- a/src/components/Nav.tsx +++ b/src/components/Nav.tsx @@ -5,33 +5,44 @@ import { usePathname } from 'next/navigation'; import { ThemeToggle } from '@/components/pathway/ThemeToggle'; const LINKS = [ - { href: '/', label: 'Pathway Builder' }, + { href: '/', label: 'Builder' }, { href: '/pathways', label: 'Pathways' }, { href: '/roster', label: 'Roster' }, { href: '/games', label: 'Games' }, ]; -export function Nav() { +/** + * Teacher-register chrome: flat card surface, 1px rule, square. The brand + * mark is the highlighter as a marker — a small filled square that always + * carries its 1px ink border (the fill is 1.41:1 on its own and never + * appears un-bordered). The active nav item is underlined by an inset + * brand-fill bar rather than a filled pill. + */ +export function Nav({ sourceLabel }: { sourceLabel?: string }) { const pathname = usePathname(); return ( -
-
- - Pathways - +
+
+ +