Release notes

What just shipped and what changed in recent audits. Spot something missing? Tell us.

Just shipped

  • Shipped

    Full-screen pre-test pages, true browser fullscreen, and passage-anchored ACT English / Reading

    Reinforced and verified the ACT English full-section path. FIX 1: The test-conditions and proctor steps are full-screen pages, not modal dialogs: position:fixed; inset:0; opaque background; role=main; site nav/footer/Pesty hidden by body.tp-pretest-active; scrollable content; and a sticky bottom bar that keeps Continue / Begin visible at all viewport heights. FIX 2: Start full section / Start full ACT call document.documentElement.requestFullscreen() in the click gesture, Begin re-requests fullscreen as a fallback, and the Exit icon calls document.exitFullscreen(); unsupported or blocked fullscreen fails gracefully while preserving the full-page flow. FIX 3: ACT English and ACT Reading banks now prioritize strict full-passage supplements and filter out every standalone/short/placeholder item: ACT English requires a 300-450 word multi-paragraph passage plus an underline target present in the passage; ACT Reading requires a 700-900 word article plus attribution; passages containing '...', '[continues]', or '//' are excluded. The ACT in-test renderer keeps desktop passages in the left scrollable pane and questions on the right, with mobile stacking passage above question. Added tests that verify fullscreen API calls, pre-test page layout/sticky controls, and strict ACT passage eligibility. Files changed: src/components/ExamSetup.tsx, src/lib/fullscreen.ts, src/routes/tests_.$testId.tsx, src/data/tests.ts, src/data/actFullPassageSupplements.ts, src/styles.css, tests/fullscreen.test.ts, tests/act-full-passage-flow.test.ts, src/routes/release-notes.tsx.

  • Shipped

    Expanded legal policies (Terms, Privacy, Cookie v2.0)

    Replaced the full body of /terms, /privacy, and /cookies with the finalized v2.0 documents (Terms still v1.0 per the source file; Privacy and Cookie are v2.0, last updated June 24, 2026). The markdown lives in src/content/legal/{terms,privacy,cookies}.md and is rendered through a new shared <LegalDoc> component (react-markdown + remark-gfm) wrapped in the site's existing <SiteNav> / <SiteFooter>, with readable typography (H2 dividers, GFM tables, anchored links). The 'Version ... Last updated ...' line renders directly under the H1 because it sits at the top of each source file. Signup acceptance now also links to the Cookie Policy alongside Terms, Privacy, and Acceptable Use. No site header, footer, nav, brand voice, or any other page changed.

  • Shipped

    Eight live-site bug fixes: explanation math, footer link, honest counts, branded 404, dedup topics, educators copy, mobile

    Targeted pass on eight separately-verified live-site bugs, each kept to its own small edit; no scoring or question data changed. (1) EXPLANATION MATH: extended src/components/MathText.tsx with a bare-LaTeX rescue inside transformMath so explanations that lost their backslashes during data sanitization no longer print 'cdot' or 'fracddx'. cdot becomes ·, fracddx becomes d⁄dx, frac{a}{b} / dfrac / tfrac become (a)⁄(b), and bare leq/geq/neq/pm/times/div/infty/Rightarrow/int/sum/prod/sqrt{...} render as ≤ ≥ ≠ ± × ÷ ∞ ⇒ ∫ ∑ ∏ √. Applies everywhere MathText is used (stems, choices, hints, explanations, score reports, multiplayer). The chain-rule item 'If f(x) = (2x + 8)^5' now renders the dot and d/dx fraction correctly. (2) FOOTER LINK: the footer entry already points to /educators in src/components/SiteChrome.tsx; added a permanent redirect route src/routes/for-educators.tsx so any old bookmark or cached social link to /for-educators redirects to /educators instead of 404ing. (3) HONEST COUNTS: the catalog tile and detail header now report the real bank size from getBankSize(testId).toLocaleString() plus 'unlimited AI-generated', so /tests and /tests/$testId agree for every subject and never overstate ('AP Calculus AB · 191 in bank · unlimited AI-generated'). Removed the fictional 't.meta' string in src/routes/tests.tsx. (4) BRANDED 404: the not-found component in src/routes/__root.tsx now renders <SiteNav /> and <SiteFooter /> (purple TestPest mascot + .ai BETA badge) and surfaces Home / Tests / Tutor / Dashboard quick links, replacing the off-brand 'TP' gradient pill. (6) TOPIC DEDUP: new src/lib/skill-canonical.ts collapses the duplicate skill names ('Chain Rule' vs 'Chain rule'; 'Definite Integrals' vs 'Definite integral evaluation'; 'Limits' vs 'Limits and Continuity' vs 'Evaluating limits'; 'Integrals' vs 'Integration - FTC') into one canonical name per skill, applied in src/lib/questions.bank.server.ts and src/data/tests.ts at load time. Questions stay mapped to the merged skill because the same canonical string is used everywhere topic is read; sampleBank also canonicalizes incoming topic filters so older callers keep working. (7) EDUCATORS COPY: '$20/month, billed monthly' on every card in src/routes/educators.tsx is now 'Free during launch. Future price: $20/month, billed monthly. Cancel anytime.', matching the header. (8) MOBILE: appended a 414px-and-below safety net to src/styles.css that prevents horizontal overflow on html/body/#root, clamps images/SVG/video/iframe, scrolls overlong KaTeX inline instead of forcing the page wide, enforces a 44px tap-target floor on .btn / .answer / .tutor-send, and forces dash-grid/test-grid/edu-grid/answer-grid/choices-grid to a single column. Covers home, /tests, in-test quiz answers and math, /dashboard, /tutor. (5) /EDUCATORS AUTH NAV: could not reproduce — /educators already uses the standard <SiteNav /> from src/components/SiteChrome.tsx, which branches on useAuth().user identically to /pricing, /contact, and every other public page. Flagged as no-op; if a stale build is showing the marketing nav while signed in, a hard refresh should pick up the standard nav. HOW TO TEST: open the AP Calc AB chain-rule item, click reveal — no 'cdot' or 'fracddx' in the explanation, real · and d⁄dx render; visit /for-educators — redirects to /educators; /tests shows '191 in bank · unlimited AI-generated' for AP Calc AB, detail page agrees; visit /this-page-does-not-exist — branded 404 with the purple mascot and four quick links; open Practice by topic on AP Calc AB — one 'Chain rule' entry, one 'Definite integrals' entry, one 'Limits' entry, one 'Integrals' entry; /educators shows 'Free during launch. Future price: $20/month'; preview at 375-414px on home, /tests, an in-test quiz, /dashboard, /tutor — no horizontal scroll, answer buttons stack and are tap-sized.

  • Shipped

    KaTeX math rendering across every test, math-capable font fallback

    Upgraded src/components/MathText.tsx to a mixed renderer: it now detects KaTeX-style math blocks ($...$, $$...$$, \(...\), \[...\]) inside any question stem, answer choice, passage, or explanation and typesets them via KaTeX (htmlAndMathml output, throwOnError:false, strict:'ignore') so equations like fractions, radicals, exponents, summations, integrals, limits, matrices, and Greek letters render correctly on SAT Math, ACT Math, SAT R&W, all AP math (Calc AB/BC, Stats, Physics 1/2/C, Chem), and the multiplayer game room. Outside KaTeX blocks the existing ASCII pass still converts x^2 to a superscript, sqrt(x) to √(x), <= >= != to ≤ ≥ ≠, +- to ±, deg to °, common Greek words (pi, theta, alpha, beta, gamma, delta, mu, sigma, lambda, omega, phi) to glyphs, and H_2 / CO_2 / x_1 to subscripts. Imported katex/dist/katex.min.css in src/styles.css and added a math-capable font fallback stack on .math-text (Inter, STIX Two Math, Cambria Math, Latin Modern Math, DejaVu Sans, Noto Sans Math, Segoe UI Symbol, Symbola) so no symbol ever renders as a tofu box or '?'. Audited the full bank (SAT Math 1000, ACT Math 763, AP Calc AB/BC, AP Stats, AP Physics): 0 mojibake or replacement characters, 0 raw \frac/\sqrt/\pi/\le/\ge LaTeX commands outside delimited blocks, 14 items contain $...$ blocks that now typeset through KaTeX, 292 items use caret exponents that continue to render as proper <sup>. Confirmed the public/generated-banks runtime merge in src/lib/questions.bank.server.ts is still active: any JSON dropped into public/generated-banks/{exam}-{section}.json by /admin/generate is concatenated into the bank by the Vite glob overlay and appears in Quick practice, topic drills, and full sections without a rebuild. Scoring, answer keys, and existing question data are unchanged.

  • Shipped

    Self-generating question skill, friendly tool icons, security advisor: 5/5 resolved

    Three-part shipment. (A) SELF-GENERATING QUESTIONS: new admin-only Question Generator at /admin/generate (route src/routes/_authenticated/admin.generate.tsx, gated by has_role(auth.uid(),'admin')). It calls a new Supabase edge function generate-questions (supabase/functions/generate-questions/index.ts) which talks to Lovable AI Gateway (google/gemini-2.5-pro) with a strict system prompt enforcing 100% original exam-accurate items, proper Unicode math characters (no '//', no '___', no '[blank]', no truncation), self-checked math answers, FULL passages (ACT Reading 700-900 words, ACT English full passage, SAT R&W short) with authentic source lines, and worked explanations. A server-side validator rejects any item missing required fields, with !=4 mcq choices, wrong answer letter, non-numeric grid-in, mojibake, banned placeholder tokens, too-short passage (<350 reading / <120 R&W), duplicate id, or a failing linear-equation spot-check. Accepted items are appended as net-new to public/generated-banks/{exam}-{section}.json (src/lib/admin-generate.functions.ts); the bank loader (src/lib/questions.bank.server.ts) now merges that overlay so generated items appear in Quick practice, topic drills, and full sections without a rebuild. Existing src/data/questions/** is never modified. Every batch is logged to a new generation_log table with admin-only RLS. (B) ICON PASS: replaced the letter/text in-test tool buttons with lucide-react icons - Highlighter for the highlighter tool (yellow/green/pink), Eraser for Eliminate (toolbar and per-choice strike), Flag for Flag, AlignJustify for Line reader, Contrast for Contrast cycle, Calculator for Calc, BookOpen for Ref, Pause/Play for Pause, ZoomIn/ZoomOut for A+/A-, LogOut for Exit. Answer choices keep letters A-D. (C) SECURITY ADVISOR: fixed all five remaining supabase_lov findings. profiles.parent_code is no longer readable cross-user (column-level grants restrict parent_code to service_role; my_parent_code() lets the owner read their own); school_invites UPDATE no longer has the 'any authenticated user can update any unclaimed invite' loophole - the USING clause now requires inviter, accepter, or same-school membership; frq_notify_signups gained explicit self-signup and self-read policies; organizations gained an explicit owner INSERT policy alongside the existing SECURITY DEFINER create_organization fn; newsletter_log is confirmed service-role-only. Guest multiplayer (game_rooms/players/answers via guest_token) and public marketing pages are unchanged. Scoring math is unchanged. SKIPPED: the 'six' advisor figure resolved to five concrete findings in the persisted scan; we fixed all five. The 59 SECURITY DEFINER 'Public Can Execute' linter warnings are pre-existing (every helper RPC used by the app) and are not part of this batch. Math spot-check covers linear-form items; other math relies on AI self-check. Files: supabase/functions/generate-questions/index.ts, src/lib/admin-generate.functions.ts, src/routes/_authenticated/admin.generate.tsx, src/lib/questions.bank.server.ts, public/generated-banks/.gitkeep, src/routes/tests_.$testId.tsx, src/routes/release-notes.tsx.

  • Shipped

    Idempotent bank import, official generator script, desktop hint, Print test

    Four-part shipment. (1) Re-ran the generated_questions.json import as idempotent: items whose id already exists in the bank are skipped, never replaced; net-new items would be appended to src/data/questions/{exam}/{section}.json. This pass added 0 net-new (the same 1,561-item set had already been imported in the previous batch, so all ids matched), confirming bank totals at SAT Math 1000, SAT R&W 436, ACT English 69, ACT Math 763, ACT Reading 55. ACT Reading and English passages continue to render via the existing q.passage path (left pane on two-pane consumers, above the question in the single-column player). (2) Added the official question generator at scripts/generate_questions.py with scripts/README.md describing how to run it (`python3 scripts/generate_questions.py`), how to bump COUNT_SAT_MATH / COUNT_ACT_MATH / COUNT_SAT_RW for more output, and the bank-file mapping the importer uses. (3) 'Best on desktop' guidance: every Full exam card on /full-exam and the Full-length exam + Full-length section cards on /tests/{id} now show a 'Best on desktop - a computer is recommended for full-length exams' hint; the /tests/{id} page also renders a small-screen banner (visible at <=720px) that warns 'full-length tests are best taken on a computer.' (4) Print test: new route /tests/{id}/print backed by a getPrintableTest server function (src/lib/print.functions.ts) renders a clean printer-friendly version of the bank - passage, question stem, answer bubbles (lettered circles), source attribution, page-breaking answer key at the end - with no nav chrome, sticky Print/Close toolbar that hides on print, @page 0.6in margins, and auto-triggers window.print() on load. A 'Print test' link on /tests/{id} opens it in a new tab. Scoring values, curves, and the sampleBank pipeline are unchanged. Files: scripts/generate_questions.py, scripts/README.md, src/lib/print.functions.ts, src/routes/tests_.$testId.print.tsx, src/routes/tests_.$testId.tsx, src/routes/full-exam.tsx, src/data/questions/sat/math.json, src/data/questions/sat/reading_writing.json, src/data/questions/act/english.json, src/data/questions/act/math.json, src/data/questions/act/reading.json.

  • Shipped

    Mobile, ACT chrome, detailed score reports, learning guides, donor section

    Nine-part upgrade pass. (1) MOBILE: added a 360-430px guard block to src/styles.css that forces single-column exam footer rows, wraps the header at small widths, makes choice rows full-width with min-height 44px, and removes hero two-column at <=480px; container padding tightened. (2) ACT in-test chrome: added a CONTRAST cycle button (default / inverted / sepia) that toggles a data-contrast attribute on .tp-exam-shell with !important CSS variants; renamed in-test footer to ACT-style 'PREV / INDEX / Review' and 'NEXT', the answered counter now reads 'ANSWERED X OF Y · N FLAGGED' in uppercase, and the toolbar Flag button is labeled FLAG. The two-pane left passage / right question split was NOT reinstated for the body because the prior implementation collapsed to height:0; passage continues to render above the question in the single-column scroll, which works at all widths. (3) Per-section timers in full ACT exam: verified already correct - perSectionRemaining in src/routes/tests_.$testId.tsx is computed per segment from FULL_LENGTH_MINUTES[sectionId] * timingMultiplier and tracked via segmentStart[sectionId], so each section gets its own countdown (Reading and Math are separate). No code change needed; documented here. (4) Detailed score reports: src/routes/report.$reportId.tsx now renders four (ACT) / three (SAT) ScoreCircle SVGs with the composite or total at the front, a 'Not for official use' badge in the Score card, raw-score Stat tiles per section, plus a 'View Score Details' collapsible that lists official reporting categories (ACT English: Production of Writing / Knowledge of Language / Conventions; ACT Math: PHM / IES / MDL; ACT Reading: KID / CS / IKI; ACT Science: IOD / SIN / EMI; SAT R&W: Information & Ideas / Craft & Structure / Expression of Ideas / Standard English Conventions; SAT Math: Algebra / Advanced Math / PSDA / Geometry & Trigonometry), time used, raw correct percentage, and top-3 strengths / top-3 work-on topics derived from skill_breakdown.topics. AP report unchanged in scoring math; same 'View Score Details' is added. (5) Pesty tutor button: verified - the floating mascot in src/components/PestyGuide.tsx already navigates directly to /tutor on click; the auto-tip bubble does not block it. No change. (6) Anchored onboarding: src/components/Coachmarks.tsx now picks the first matching element with a non-zero bounding rect (not just querySelector), so a tour step that targets [data-tour-id=nav-tests] finds the visible mobile drawer link instead of the display:none desktop link; added matching data-tour-id attributes to the mobile drawer links in src/components/SiteChrome.tsx so the welcome tour anchors correctly on mobile and desktop. (7) Topic learning guides: new src/data/learning_guides.ts with short concept summaries, step-by-step bullets, worked examples, and Khan Academy URLs per topic for SAT Math, SAT R&W, ACT English, ACT Math, ACT Reading, and ACT Science; new routes /learn and /learn/$exam/$skill render them with a 'Watch the lesson on Khan Academy' CTA. (8) Donor section on homepage: src/routes/index.tsx now includes a warm 'Keep TestPest free for the next student' band above the final CTA with three impact tiers ($5 / $25 / $100), a primary Donate to TestPest CTA, and a Sponsor a school secondary action. (9) Score-report-upload -> study-plan: traced upload -> score-reports.functions.ts -> ingest-score-report -> generate-study-plan -> /plan, no broken links found in the chain; no fix needed. Scoring values, curves, and calculations are unchanged across all nine items. Files: src/styles.css, src/routes/tests_.$testId.tsx, src/routes/report.$reportId.tsx, src/components/Coachmarks.tsx, src/components/SiteChrome.tsx, src/data/learning_guides.ts, src/routes/learn.tsx, src/routes/learn.$exam.$skill.tsx, src/routes/index.tsx.

  • Shipped

    Bank import: +1,561 SAT/ACT questions

    Appended generated_questions.json to the SAT and ACT MCQ banks as net-new items; nothing existing was deduped, dropped, or replaced. Added per exam/section: SAT Math +700, SAT Reading & Writing +250, ACT English +3, ACT Math +600, ACT Reading +8 (total +1,561). The 81 SAT Math grid-in entries are persisted in src/data/questions/sat/math.json for future use but are not yet surfaced by the MCQ-only bank loader. ACT Reading and English new items include full passages, which render on the passage pane via the existing q.passage path (single-column on the migrated player, left pane on any two-pane consumers). Items appear in Quick practice, topic drills, and full sections through the unchanged sampleBank pipeline. Scoring unchanged. Files: src/data/questions/sat/math.json, src/data/questions/sat/reading_writing.json, src/data/questions/act/english.json, src/data/questions/act/math.json, src/data/questions/act/reading.json.

  • Shipped

    Frozen pre-test timer preview and topic-validated passage attributions

    Two corrections to the stepped Full section / Full exam flow. (1) Both the Step 1 test-conditions page and the Step 2 proctor instructions page now render a 'Section clock(s) - frozen until you begin' preview that lists every section in order and shows its standard time multiplied by the chosen accommodation (Standard / +50% / +100% / Untimed). The timer is shown but NOT counting; the actual countdown only mounts and starts when Begin is pressed. Example: ACT English at Standard shows 35:00 frozen on both setup pages and starts ticking from 35:00 once the test loads. (2) Passage source attribution lines now run through a topic-validation guard (safeAttribution in src/lib/attribution.ts) before render. When the attribution claims a topic phrase (article on X, social science article on X, laboratory study of X, historian's commentary on X) and none of X's meaningful tokens (with simple plural/-ies stemming) appear in the passage text, the line is rewritten to a neutral form such as 'Adapted from a 2022 article by Name.' so we never mislabel a digital-divide passage as 'an article on chemistry'. Genuine matches and non-topic-phrase lines (e.g. titled novels, primary sources) are preserved exactly as before. Wired into all three exam render paths in src/routes/tests_.$testId.tsx. Scoring is unchanged. Files: src/components/ExamSetup.tsx, src/lib/attribution.ts, src/routes/tests_.$testId.tsx.

  • Shipped

    Full-test sessions now use the Quick-practice question renderer

    Replaced the unstable bespoke two-pane real-test body for Full section and Full exam sessions with the same proven single-column question renderer used by Quick practice: passage, prompt, figure, and all answer choices stack in one scrollable column inside clean exam chrome. Full SAT/ACT sections and exams now launch through a focused stepped flow: Page 1 test conditions (sections where applicable, +50%/+100%/untimed timing, feedback, text size), Page 2 proctor instructions, then Page 3 the test starts; the timer does not mount or run until Begin is pressed. The header keeps section name and timer, the footer keeps Back / Navigator / Review / Next, and mobile widths 360-430px use single-column reachable controls with no two-pane split. Also guarded exam-resume server calls so signed-out/public views no longer trigger the app error boundary from an unauthorized resume request. Scoring values and score calculations were not changed. Files: src/routes/tests_.$testId.tsx, src/components/ExamSetup.tsx, src/styles.css.

  • Shipped

    Published exam shell height override

    Added a high-specificity body.tp-exam-active CSS guard so the Full section and Full exam shell cannot lose to app-shell flex rules and compute to height:0 on the published site. The guard pins .tp-exam-shell to the viewport with position:fixed, inset:0, height/min-height:100dvh, display:flex, flex-direction:column, and z-index:50 using !important; .tp-exam-body keeps flex:1 1 auto, height:auto, min-height:0, and overflow:hidden; .tp-exam-split keeps height:100% and min-height:0; .tp-exam-pane keeps min-height:0 and internal overflow:auto. The existing per-item error card remains available only for questions that truly cannot render. No scoring changes. Files: src/styles.css.

  • Shipped

    Real-test player now fills the viewport

    Final CSS fix for the Full section and Full exam blank-body issue: .tp-exam-shell now fills the viewport with position:fixed; inset:0; fallback height:100vh; height:100dvh; display:flex; flex-direction:column; and a z-index above the app shell. This prevents the shell itself from computing to height:0 and clipping the already-rendered passage, question, answer choices, Eliminate buttons, and pinned footer. The body remains flex:1 1 auto with min-height:0, the split remains height:100% with min-height:0, and panes scroll internally with overflow:auto and min-height:0, including at the 390px mobile width. No scoring, timer, section-order, or player logic changed. Files: src/styles.css.

  • Shipped

    Real-test player blank-body CSS collapse fixed

    Root cause for the 'header + timer + Navigator render but the passage/question/choices area is empty' bug in Full section and Full exam: the in-test layout collapsed to zero height. .tp-exam-shell was a CSS grid (grid-template-rows: 56px 1fr 64px) whose 1fr body row collapsed to height:0 in certain ancestor contexts because .tp-exam-body had overflow:hidden with no real height, .tp-exam-split set height:100% against a zero-height parent, and the panes had no min-height:0 so the flex/grid intrinsic-size rules clipped everything. The DOM contained the passage, question text, all four choices, and Eliminate buttons — they were just clipped by zero-height ancestors. Fix in src/styles.css: .tp-exam-shell is now display:flex; flex-direction:column at all widths (replacing the grid rows); .tp-exam-header and .tp-exam-foot are flex:0 0 auto with the header keeping min-height:56px; .tp-exam-body is flex:1 1 auto with min-height:0 and NO overflow:hidden, so it occupies the remaining viewport height between header and foot; .tp-exam-split keeps height:100% and gains min-height:0; .tp-exam-pane gains min-height:0 alongside its existing overflow:auto so each pane scrolls internally instead of forcing the body to grow or collapse. The mobile media query (max-width:760px) was also updated to reaffirm display:flex; flex-direction:column on the shell instead of resetting grid-template-rows. Affects every full section and full exam on SAT and ACT (and any future exam family using the same chrome). No changes to scoring, per-section timers, question counts, the exam component tree, or the player logic. Files: src/styles.css.

  • Shipped

    Predicted exam readiness score on the dashboard

    New dismissible dashboard card (cardKey readiness_score) shows a predicted exam score derived from the student's own attempts data — clearly labeled an Estimate, not an official score. SAT students see a 400-1600 composite plus 200-800 Reading & Writing and Math sub-predictions; ACT students see a 1-36 composite plus per-section English/Math/Reading scaled scores (Science continues to feed STEM, not the composite, matching the current ACT). Each prediction includes a plus/minus confidence band that shrinks as more questions are answered, a low/medium/high confidence label, the total questions scored, and a 'Biggest area to improve' line that drills into the weakest section and, when available, the weakest skill from weak_topics on those attempts. Family pick: whichever target_test the user picked is sorted first; if both SAT and ACT have data, both predictions render. If the student has SAT/ACT as their target but no attempts yet, the card prompts them to practice a section to unlock the prediction. Implementation: pure helper in src/lib/readiness.ts (predictSat, predictAct, readinessFor) — SAT section scaling 200 + acc*600 -> 200-800, ACT section scaling 1 + acc*35 -> 1-36, SAT composite is the section sum, ACT composite is the average of English/Math/Reading; bands taper from ~90 to ~30 (SAT) and ~5 to ~2 (ACT) as sample size grows. No DB writes, no edge functions, no changes to scoring tables in src/lib/scoring.ts or to any submitted-score calculation; the existing per-attempt scoring path is untouched. Card updates automatically on every dashboard mount because it reads from the same attempts array (test_attempts via getProgress) the dashboard already loads, so it refreshes as the student practices. Files: src/lib/readiness.ts (new), src/routes/dashboard.tsx (state + ReadinessCard component placed above the Mistakes review card).

  • Shipped

    Personal Mistakes review deck with spaced repetition

    Every question answered incorrectly in any mode that posts to recordQuizAttempt (test player full exam/section/practice paths and Practice-from-materials) is now auto-captured into a personal Mistakes deck — a new table public.user_mistakes (user_id, question_id, test_id, topic, prompt, choices snapshot, correct_answer, explanation, box, attempts, correct_count, added_at, last_seen_at, next_review_at) with full RLS so students see only their own rows (own-row SELECT/INSERT/UPDATE/DELETE policies + service_role grants). Capture is upsert with ON CONFLICT DO NOTHING on (user_id, question_id) so existing spaced-repetition state is preserved when the same item is missed again; failures are swallowed and never block the attempt record. New /review route (gated by RequireAuth) renders one card at a time: prompt, the original choices, a Check answer step, then either 'Got it -> promote box' (Leitner: box +1 capped at 6, next interval 1/3/7/14/30/60 days) or 'Missed it -> box 1' (resets to box 1, next interval 1 day). Header shows three live counters — Due today, Total in deck, Mastered (box 6) — and remove-from-deck per card. Dashboard gains a dismissible 'Review your X missed questions' card (cardKey mistakes_review) calling a new mistakesCounts server fn; hidden when the deck is empty, otherwise routes to /review with the due count in the CTA. Server fns live in src/lib/mistakes.functions.ts: captureMistakes, listMistakes, mistakesCounts, reviewMistake, removeMistake — all .middleware([requireSupabaseAuth]). The existing test player and scoring are untouched: capture is performed inside recordQuizAttempt (a server fn), not inside the player; no change to scoring, no change to game modes (games use recordAttempt which is summary-only and intentionally does not capture per-item mistakes).

  • Shipped

    SAT and ACT structure now matches the real current exams

    Each exam's section list, question counts, timers, tools, and composite math now mirror the real current test. DIGITAL SAT (already adaptive): unchanged at 2 sections x 2 modules — Reading & Writing 27q/32min x 2 (54q/64min) with NO calculator and NO reference sheet, Math 22q/35min x 2 (44q/70min) with Desmos + the formula reference sheet always on; setup labels now spell that out. ENHANCED ACT: required core is English/Math/Reading; Science is OPTIONAL (added via the setup screen) and feeds a new STEM score, NOT the composite. Section counts/timers updated in src/data/tests.ts FULL_LENGTH + FULL_LENGTH_MINUTES to English 50q/35min, Math 45q/50min, Reading 36q/40min, Science 40q/40min (was 75/45, 60/60, 40/35, 40/35). EXAM_SECTIONS.act now lists the core three only so Full-exam launches from the catalog default to the composite-eligible sections; a new EXAM_OPTIONAL_SECTIONS export documents the optional set. TESTS metadata blurbs for act-english/math/reading/science rewritten to describe what each section actually is (English = underlined-portion revisions; Math = standalone, calculator allowed; Reading = long passages including literary narrative, social science, humanities, natural science, and paired; Science = data representation, research summaries, conflicting viewpoints). ExamSetup section labels updated to the new counts/timers; the proctor Tools-per-section bullet now states exactly which sections have the calculator and reference sheet and which do not, so users see up front that SAT R&W and ACT English/Reading/Science have neither. Tools gating in the player was already correct — calculator is shown only when activeTest.needsCalc (sat-math, act-math), and the math reference sheet (SatReferenceSheet) is shown only when activeSectionId is sat-math or act-math — so SAT R&W and ACT English/Reading/Science never show calculator or reference sheet. COMPOSITE: src/lib/score-reports.functions.ts ACT branch now averages ONLY act-english + act-math + act-reading for the 1-36 composite (was averaging every submitted section, which double-counted Science); a new actStem field on section_scores is the rounded average of act-math and act-science scaled scores when both are present, so Science still gets its scaled score AND contributes to STEM without contaminating the composite. SAT composite formula and the SAT/ACT scaled-score conversion curves are unchanged (no scoring-value changes). Files: src/data/tests.ts, src/components/ExamSetup.tsx, src/lib/score-reports.functions.ts.

  • Shipped

    Finish a section early in real-test mode

    Full exam and Full section now let a confident student end the current section before its timer runs out. The Review screen for a multi-section exam (SAT R&W + Math, ACT English/Math/Reading/Science) now shows only the questions in the current section with their per-section numbering and a green 'Finish section / Continue' button (or 'Finish section / Submit exam' on the last section). Clicking it opens a confirm dialog that names the section, calls out how many items will be left blank, and warns that returning to that section is not allowed — Keep working cancels, End section & continue advances. On confirm, the player locks any answered items in that section, marks the section expired (same path the hard time-0 auto-submit uses), shows the standard 10-minute break for SAT-RW->Math and ACT-Reading->Science (or whenever extraBreaks is on) then jumps to the first question of the next section; if it was the last section the exam ends and the score report renders. The existing hard auto-submit at section time 0 is unchanged; per-section timers, question counts, ordering, and scoring are unchanged. Files: src/components/ExamChrome.tsx (ExamReviewScreen gains an optional section prop with start/end/label/nextLabel/onFinishSection plus a confirm dialog and section-scoped grid + counts), src/routes/tests_.$testId.tsx (wires the section prop from the active plan segment and implements onFinishSection).

  • Shipped

    Branded 404, two-step proctor flow, onboarding sticks per user

    (1) NO DEAD 404s: replaced the bare 404 page in src/routes/__root.tsx with a branded TestPest 'Page not found' screen — header logo, eyebrow tag, headline, helper text, and four CTAs (Go home, Browse tests, Games hub, Ask the AI tutor) plus a small Path readout for debugging. URLs under /games or /games_ render the same branded shell with 'Coming soon' wording and a Beta tag instead of 404 wording, so any game mode that ships before its route is wired shows a friendly placeholder rather than a raw 404. Audited the Games hub (MODES array in src/routes/games.tsx) and every nav Link in src/components/SiteChrome.tsx against src/routes/*.tsx — all targets resolve (games_/time-attack, survival, daily, boss, sprint, multiplayer, $roomId, memory, missions, coins, friends, school, parent, teacher-portal, guardian, donate, status, release-notes, privacy, terms, cookies, acceptable-use, contact). The catch-all branded fallback now covers any future broken link. (2) PROCTOR FLOW is now two clean pages, not one long scroll. Step 1 of 2 (ExamSetupScreen in src/components/ExamSetup.tsx) is 'Test conditions': sections (required + optional), accommodations / extended time radios, Extra breaks toggle, Feedback mode (instant vs end-only), and a new Text size selector (Small 0.9x / Normal 1x / Large 1.15x / Extra large 1.35x) — Continue button. Step 2 of 2 (ProctorGuide) is 'Read this before you begin': numbered proctor instructions (phone away, section order, timing, breaks, tools, feedback, hard time limit), then an 'I understand, I am ready to begin' checkbox that turns the row green, and a Begin <first section> button that is DISABLED until the checkbox is ticked — the clock only starts after that explicit acknowledgement. Back button now exits to the conditions screen instead of silently skipping the briefing. Text size flows into SessionPrefs and seeds the in-test zoom for the locked session. (3) ONBOARDING never auto-repeats once completed OR skipped. src/components/Coachmarks.tsx now stores SEEN in two keys: the existing global tp-tour-<id>-seen AND a per-user tp-tour-<id>-seen:<userId> derived from the Supabase auth-token in localStorage (matched via /^sb-.*-auth-token$/). Auto-start checks BOTH keys and bails if either is set, so a user who completed/skipped on one browser also doesn't get re-prompted on another browser after signing in. Skip and Finish both write both keys. Replay from the Help/nav drawer still works (dispatches the tour:start event); the entry is the only way to re-trigger the tour. (4) Friend code and class join code: verified end to end without changes. FriendCodeCard (src/components/FriendCodeCard.tsx) calls regenerateFriendCode (server fn) with a regenerateFriendCodeDirect fallback if the server call fails — the QR code + copy + invite vs referral URL toggle all use the live displayCode state and update after a regen. joinClassByCode (src/lib/teacher-portal.functions.ts) trims/uppercases the entered code, looks up classes by join_code, inserts into class_members with a duplicate-friendly error filter, and returns { ok, class_name } — wired from src/routes/classes.tsx with toast on failure. Both flows tested through the UI; no changes required.

  • Shipped

    Real-test player no longer blanks; per-section timer and counts

    (1) The in-test player for Full exam and Full section ('real test') was rendering with a populated header, timer, Navigator, and Next button but a completely empty content body on some viewports — reproduced on ACT English full exam showing 'Question 3 of 175' with no passage, prompt, or choices. Root cause: the exam path rendered through a two-pane ExamSplit that could lay out with both panes off-screen on narrower widths. Fix in src/routes/tests_.$testId.tsx: the exam render path now uses the SAME single-column question renderer Quick practice uses — passage card stacked above the question text, figure, and every answer choice — wrapped in the existing .tp-exam-pane container so highlighter, eliminator, zoom, and instant-feedback reveal still work. (2) Added a per-render guard in the exam path: if the current question is missing text or has zero choices, the player now shows the standard 'This question can't be displayed' EmptyState with Back to setup and Skip to next buttons instead of a blank body. The route-level TestErrorBoundary still catches any render-time throw. (3) Per-section timer and question count: for multi-section full exams (SAT R&W + Math, ACT English/Math/Reading/Science) the header now reads '<Section name> · Question N of <section total>' instead of 'Question N of <lumped total>'; the timer displays the remaining time for the CURRENT section (computed from FULL_LENGTH_MINUTES[section] * timing multiplier - elapsed-in-section via segmentStart), so ACT English shows 75q/45min, ACT Math 60q/60min, SAT R&W 54q/64min, SAT Math 44q/70min — not the lumped 175q/204min. The 5-minute timer warning also now triggers per-section. The existing per-section auto-submit/lock at section expiry was already correct and is unchanged. (4) Verified by starting Full section and Full exam paths for ACT English and SAT Reading-and-Writing: question, passage, and all four answer choices render; Next / Navigator / Review & submit work; per-section header and timer show the right numbers. Scoring untouched.

  • Shipped

    Mobile fixes, stale-bundle auto-reload, LaTeX cleanup

    (1) Mobile responsiveness pass: added a @media (max-width: 760px) block in src/styles.css that collapses the in-test two-pane player to a single column (passage stacked above questions, divider hidden, left pane capped to 45vh with its own scroll), wraps the header tool row (Reference, Calculator, Flag, Pause, A-/A+, highlighters, eliminator, line reader, exit) onto multiple lines with smaller padding, and lets the footer (Back / Navigator / Review / Next / Review & submit) wrap to two rows so every button is reachable. Also added global html,body { max-width:100vw; overflow-x:hidden } at the same breakpoint to kill horizontal scroll across dashboard, tests list, games, and the onboarding tour. Verified at 390px wide: Quick practice (SAT Math) and Full section (SAT R&W) both render with the question text and all four answer choices fully visible without horizontal scroll, header tools wrap cleanly, footer buttons reachable. (2) Stale bundle / blank page: added a script in src/routes/__root.tsx that listens for window 'error' and 'unhandledrejection' events matching chunk-load failures (Failed to fetch dynamically imported module, ChunkLoadError, Loading chunk N failed, Importing a module script failed). First occurrence auto-reloads once (guarded by sessionStorage tp-chunk-reload); if the reload still fails, a fixed bottom banner appears with 'A new version is available' and a Reload button instead of leaving a white screen. The counter clears 4s after a successful load. (3) LaTeX artifact: src/components/MathText.tsx now runs a stripLatexNoise step before escaping/transform that removes literal '\(' '\)' '\[' '\]' delimiters and any bare backslash not followed by a letter, so data like '\7, 8, 8, 8, 29\' renders as '7, 8, 8, 8, 29' instead of showing the slashes. Scoring untouched.

  • Shipped

    Test player no longer blanks out on bad data

    Added a TestErrorBoundary (src/components/TestErrorBoundary.tsx) around the in-test player in tests_.$testId.tsx so a render-time crash (bad question shape, missing field, math typesetting blow-up) now shows a friendly 'This test hit a snag' card with a Try again button that resets the player back to the setup screen, instead of an unrecoverable white screen. RunTest also gained an early guard: if the started session ends up with zero questions (e.g. a section with no bank coverage slipped past upstream checks), it now renders a 'No questions to show' EmptyState with Back to setup and All tests buttons. The existing start-time paths already show a loading skeleton during AI generation, toast on AI failure, and fall back to a bank quiz when bank coverage exists; this change covers the residual case where the player itself was the failure point. Verified entry points by starting them: Full exam (SAT digital + ACT English) -> setup screen renders, full exam launches; Full section / Real test (SAT R&W, SAT Math, ACT Math, ACT Reading, ACT Science) -> launches; Quick practice across SAT/ACT and AP US History, AP Calculus AB -> launches; Practice by topic on AP US History -> launches; AI smart quiz and Endless AI on SAT Math -> launches with skeleton then questions, AI failures fall back to bank or surface a toast instead of blanking. No blank screens observed after the boundary was added. Scoring and question data untouched.

  • Shipped

    Auth gate on start actions, plus pre-test feedback and text-size lock

    (1) Signed-out visitors can still browse the catalog, marketing pages, and any test detail page, but every actual start action now requires an account. Added src/lib/require-auth.ts ensureSignedInOrPrompt(navigate, returnPath, what) which checks supabase.auth.getSession(), stashes the intended return path in localStorage tp:postLoginReturn, toasts a friendly nudge, and routes the user to /signup?redirect=<path>. Wired into tests_.$testId.tsx around every start handler: bank-mode onStart (full, full-exam, full-section, topic, smart, Bluebook practice), the AI startFresh and startSmart paths, and the SAT/ACT ExamSetupScreen onStart. Game and FRQ pages are gated at the route level with a new <RequireAuth> wrapper in src/components/RequireAuth.tsx, which subscribes to supabase.auth.onAuthStateChange, renders a 'Sign in to start <label>' card with Create account / Log in buttons that carry the current path through as ?redirect=, and otherwise renders the page. RequireAuth is now applied to /frq, /games/boss, /games/daily, /games/sprint, /games/survival, /games/time-attack, /games/multiplayer, and /games/$roomId. /practice already required a session — its redirect was upgraded to /signup with redirect= and the same localStorage stash so the flow matches. login.tsx now pops the redirect (URL ?redirect= first, then localStorage tp:postLoginReturn) and routes the user back there after email login OR after Google/Apple OAuth — the OAuth redirect_uri is rewritten to the saved path so the user lands on the start screen they came from instead of /dashboard. signup.tsx captures the same redirect on mount and stashes it for the post-confirm login. (2) Pre-test settings are now chosen on the start screen and locked once the test begins. Added SessionPrefs ({ feedback: 'instant' | 'end', fontScale: number }) persisted to localStorage tp:sessionPrefs, plus a new SessionPrefsCard inside ModePicker showing two radios for feedback ('Show correct/incorrect as I go' vs 'Reveal only at the end') and four buttons for text size (Small 0.9x / Normal 1x / Large 1.15x / Extra large 1.35x), with a 'Locked once you start' note. Every setStarted call now ships the current prefs into the Started state. RunTest reads state.prefs to derive showInstantFeedback (was hard-coded to true for non-exam practice — now honors the chosen mode for practice/topic/smart/fresh as well) and to seed the initial zoom (was always 1 — now starts at fontScale). The mid-test A- / A+ zoom buttons still adjust size, but there is no UI anywhere to flip the feedback mode mid-test (and there never was — confirmed by searching the runner). The SAT/ACT ExamSetupScreen now accepts an initialFeedback prop seeded from prefs so the proctored full-length flow defaults to the same choice, and its onStart syncs the picked feedback back into prefs before starting. Scoring logic and question data untouched.

  • Shipped

    Real guided tour with spotlight coachmarks; Pesty button opens the tutor

    Replaced the static 4-card welcome overlay with a proper spotlight walkthrough engine in src/components/Coachmarks.tsx. Each step locates a real DOM element by data-tour-id, dims the rest of the page with an SVG mask cut-out, outlines the target in plum, scrolls it into view, and renders a tooltip that auto-flips to the side with the most space (bottom/top/right/left). Tooltips show 'i / N' progress, have Back/Next/Skip controls, and the skip button works at any step. The engine listens to window 'tour:start' events so the tour is replayable. Two tours are configured: welcome-v1 (5 steps over Tests/Tutor/Dashboard nav) auto-runs once on first /dashboard visit, in-test-v1 (7 steps over Calculator, Reference, Eliminator, Flag, Navigator, Submit answer, score report intro) auto-runs once on first /tests/<id> visit. Persistence uses localStorage keys tp-tour-welcome-v1-seen and tp-tour-in-test-v1-seen so tours don't repeat. data-tour-id attributes added to: nav Tests/Tutor/Dashboard links (SiteChrome) and the in-test toolbar buttons Calc, Ref, Eliminator, Flag, Navigator, plus the Submit answer button (tests_.$testId.tsx). Added a 'Replay guided tour' item in the nav drawer that clears the seen flag and dispatches the tour:start event. PestyHelper changes: (1) the bottom-right floating mascot now navigates to /tutor on click instead of toggling a tip bubble, so it works as a true 'open the AI tutor' shortcut; (2) the auto-tip bubble and the floating button are both hidden entirely on /tests/<id> and /games/<roomId> so they can never cover the Submit button or answer choices; (3) the bubble is also suppressed while a guided tour is running (via a sessionStorage tp-tour-running flag) so the two overlays don't fight. Scoring and question data untouched.

  • Shipped

    Fixed answer checking and blank reveal in practice/tests

    Some bank items shipped with an empty `answer` field, so the comparison `picked === q.correct` always failed and the reveal rendered as 'the answer is .' with the letter missing. Added src/lib/answer-key.ts with resolveCorrectLetter(), which (1) accepts a direct A/B/C/D match (case-insensitive), (2) accepts an answer stored as the choice VALUE instead of the letter (normalized: NFKC, unified minus/dashes, stripped spaces/commas/trailing punctuation, lowercased), (3) parses the explanation for explicit call-outs like 'answer is X', 'Choice X', '(X) is correct', and finally (4) finds which choice VALUE appears in the explanation (last mention wins, so 'x = 2.5' resolves to choice A '2.5'). Wired into both normalization paths so client and server agree: src/data/tests.ts normalize() and src/lib/questions.bank.server.ts loadBank(). Tightened the reveal in src/routes/tests_.$testId.tsx to uppercase both sides before comparing and to render 'the answer is A (2.5)' (letter + value) instead of just a letter; falls back to 'see explanation below' only if no letter and no value could be resolved. Audit across the shipped MCQ banks: SAT 48 affected (math 39, reading_writing 9), ACT 0, AP 87 across 36 subject files (art_history 14, music_theory 13, physics_c 13, plus 2 each in most other AP files); 135 items total now resolve to the right letter without changing the intended answer data. Repro question 'If (x+2)/(x-1)=3' now marks 2.5 correct and the reveal reads 'the answer is A (2.5)'. Scoring weights and question prompts/choices/explanations are untouched.

  • Shipped

    Theme borders cleaned up; official TestPest logo wired everywhere

    (1) Dark themes (Dark, Ocean, Forest, Grape, System+dark) no longer paint chunky white borders. Introduced two new tokens in src/styles.css — --stroke (border color) and --stamp (offset shadow color) — defaulting to var(--ink) so the Playful brand keeps its chunky black border + offset shadow. In every dark theme block --stroke is retuned to var(--line-2) (a subtle theme-appropriate divider) and --stamp is set to transparent so the brand's 4px/6px offset shadows disappear in dark instead of becoming a white block. Substituted every 'solid var(--ink)' border declaration and every '0 var(--ink)' shadow color across src/styles.css and every inline-styled component/route file (cards, buttons, inputs, choices, banners, ribbons, ticker, edu cards, dividers, portal sidebar/topbar, roster tables, nav burger, nav drawer, footer ribbon, score-report card, calculator and reference panels, modals, attempt drawer, friend code card, invite card, live game UI, donor ticker, leaderboard rank pills, exam header/foot/timer/tool/pane). Sunset (light) keeps the chunky ink border by design. (2) Replaced the generic generated OG/social image and the placeholder favicon.ico with the official TestPest mark already uploaded earlier in this chat (the testpest-favicon-v2 monster face). Rendered the SVG to /public/favicon.ico (16/32/64), /public/apple-touch-icon.png (180), /public/icon-192.png, /public/icon-512.png and a 1200x630 /public/og-image.png. __root.tsx now serves the SVG as the primary <link rel='icon'> with the .ico as fallback, the apple-touch-icon points at the rendered PNG, og:image and twitter:image both point at /og-image.png with explicit 1200x630 dimensions, and the EducationalOrganization JSON-LD logo now references /testpest-logo.svg. manifest.webmanifest updated to use the SVG plus 192/512 PNG maskable icons and a #f5f0e8 brand background. SiteChrome already renders <TestPestMark /> in the header and footer. Scoring and question data are untouched.

  • Shipped

    Bluebook-style Desmos and reference panels in the test runner

    The in-test Calculator and Reference floating panels were restyled to match the College Board Bluebook layout. (1) FloatingPanel chrome switched from the brand stamp shadow to Bluebook neutrals: white card, 1px #c7c9cf border, 6px radius, soft 12px drop shadow, 38px header with a #f6f7f9 bar and a lucide X close button (no ASCII '✕'). System sans-serif (-apple-system, Segoe UI, Roboto, Helvetica Neue, Arial) throughout. (2) Per-panel default sizes now mirror Bluebook's right rail: Reference defaults to 360x620, Desmos defaults to 560x680, both right-anchored 16px from the viewport edge with a 84px top offset. Stored sizes still load from sessionStorage. (3) DesmosCalculator header is now a Bluebook-style segmented control (Graphing / Scientific / Basic) inside a #f1f3f6 pill with a white active pill, 0.8rem 600 active weight, and a thin #e5e7eb divider above the calculator surface. (4) SatReferenceSheet rewritten as a single-column Bluebook reference: 16/18px padding, 0.9rem/1.45 body, mono 0.95rem formulas, 84px figure column with inline-SVG diagrams for Circle, Rectangle, Triangle, Pythagorean, Special Right Triangle (30-60-90 and 45-45-90), Rectangular Solid, Cylinder, Sphere, Cone, Pyramid, and the three trailing constant statements (360 degrees, 2pi radians, 180 degree triangle sum). Source labeled as College Board Bluebook Digital SAT Math Reference Sheet. Scoring math and question data are untouched.

  • Shipped

    Design system tightening: tokens, focus states, premium polish

    Site-wide UI/UX polish without changing scoring, content, or brand personality. (1) Design tokens in src/styles.css: explicit 8px spacing scale (--space-1..10), explicit type scale (--text-xs..3xl with line-height tokens --lh-tight/snug/base/loose), refined transitions (added --t-smooth 200ms ease for non-bouncy UI motion), and a soft-elevation shadow family (--shadow-soft-sm/md/lg) alongside the existing brand 'stamp' shadows for premium surfaces. (2) Global focus ring: a single --ring token (3px paper + 2px plum) now applied via :focus-visible to every interactive element (links, buttons, inputs, [role=button], [tabindex]) — no more invisible keyboard focus. (3) Button system: .btn tightened to use the spacing+type tokens, gained explicit :focus-visible (stamp shadow + ring), proper [aria-disabled] parity with :disabled (opacity .5, pointer-events none, flattened shadow), and a .btn-block full-width modifier. Size modifiers (.btn-sm/lg/xl) now read from the type scale. (4) Card system: hover transition retimed to --t-smooth, the gimmicky -0.4deg hover rotate removed for a clean lift, focus-visible state added, and a new .card.elevated variant uses soft shadows + line border for premium surfaces (modals, score reports). (5) Inputs: new .input + .field/.field-label/.field-hint/.field-error primitives — 44px min height for touch targets, hover/focus/invalid/disabled states all wired to the ring tokens. (6) Layout rhythm: .stack / .stack-sm / .stack-lg / .cluster / .page-section utilities so vertical rhythm always lands on the 8px grid. Existing .btn/.card classes everywhere automatically inherit the tightened states — no page-level refactor needed. Question data, scoring math, and brand colors are untouched.

  • Shipped

    Polish pass: password reset, security hardening, verified resume on refresh

    Final polish ahead of launch. (1) Auth: added /forgot-password and /reset-password routes wired to supabase.auth.resetPasswordForEmail and updateUser; login page now links to Forgot password. Email-verification flow already routes through signUp() with emailRedirectTo on /onboarding (student) or /onboarding-guardian (educator); the new reset page accepts both the on-arrival PASSWORD_RECOVERY event and an existing recovery session, and rejects expired/invalid links with a clear message. (2) Timed test resume already persists answers, submitted set, marked-for-review set, remaining seconds, paused state, and adaptive module routing to public.exam_resumes via getExamResume / saveExamResume / clearExamResume (auth-scoped, RLS); a refresh during a timed section rehydrates the exact state without losing progress. (3) Supabase security pass: dropped the overly-broad 'Authenticated can read invites by code' SELECT on school_invites (lookup-by-code already runs through a server function with the admin client); dropped 'answers public read' on game_answers and restricted SELECT to room participants, with a new SECURITY DEFINER round_answers(room_id, question_index, guest_token) RPC so guests can still read live opponent answers in their own room; dropped the anonymous INSERT policy on frq_notify_signups (subscribeFRQNotify already inserts via the admin client server-side); dropped the self-insert INSERT policy on xp_ledger and switched claimMission to insert XP through the admin client so users can't self-award XP from the browser. The earlier 'always-true' UPDATE/INSERT warning is resolved by the frq_notify_signups drop. (4) Mobile/responsive: hub, dashboard, tests, in-test two-pane, and games hub already use clamp() and auto-fit grids that collapse to a single column under ~720px (two-pane reading switches to stacked panes); kept untouched in this pass. (5) Accessibility: every new form input has an associated label via htmlFor/id and autocomplete hints; the existing icon-only buttons in the test runner already carry aria-label / title. (6) Emojis: no emojis remain in product chrome (verified by ripgrep across src/ for the U+1F000-U+1FAFF range). Scoring and question data are untouched.

  • Shipped

    No-emoji UI pass: lucide icons, SVG mascot moods, real confetti, plain copy

    Stripped every emoji from product chrome and replaced them with proper iconography. (1) Pesty mascot moods (wave, happy, think, encourage, celebrate) now render as flat inline-SVG badges (hand, smile arc, thought cloud, heart, sparkle burst) anchored to the TestPestMark — no emoji overlay. (2) Correct-answer celebration is a real CSS particle confetti burst (src/components/Confetti.tsx, mounted globally in __root.tsx, triggered via window event 'tp:confetti' from the test runner) — respects prefers-reduced-motion. (3) Hub cards (/hub) use lucide icons (ClipboardList, Gamepad2, BarChart3, Bot, Trophy, Users, BrainCircuit, BookOpen, Tag, Gift, Heart) instead of pictographs. (4) Signup role picker uses lucide icons (GraduationCap, BookOpenCheck, School, Users). (5) Bulk emoji scrub across SiteChrome (notifications/toasts), FloatingPanel, InviteFriendsCard, LiveGameUI, ReferralAttribution, dashboard, donate, educators, friends, games_.$roomId, index, not-ready, onboarding, onboarding-guardian, pest, pricing, profile, rewards, school, setup, shop, shop_.$itemId, tests_.$testId, tutor, checkout.return — coin, party, medal, fire, target, brain, books, gift, robot, controller, hands, mortarboard, school, family, tools, sunrise, warning, lightning, and graduation-cap glyphs removed; text reads as plain English ('coins', '+25', etc.). (6) Copy pass: post-correct toast now reads 'Correct.' and the streak line 'N in a row.' instead of 'on fire!'; other peppy boilerplate trimmed in the same files. Emojis are still allowed where a user would type them (display name, nickname, tutor chat input). Question data and scoring math are untouched.

  • Shipped

    Real score reports for SAT, ACT, AP + progress chart + Review answers

    Finishing any full section or full-length exam now generates a real score report and saves it to the score_reports table (extended with kind, quiz_attempt_id, test_id, test_name, scaled_score, score_band, time_seconds, and source_label). SAT/PSAT reports show the 400-1600 total composed from the two section scores (Reading & Writing 200-800 and Math 200-800), a published +/-40 SEM range band, the per-topic skill breakdown, questions correct per adaptive module, and time used. ACT reports show all four section scores 1-36 plus the Composite as the rounded average and a +/-2 SEM band per the ACT technical manual. AP reports show a predicted 1-5 from a 50% MCQ / 50% FRQ composite on a 0-100 scale, with the MCQ percent, modeled FRQ percent (FRQ rubric used when present, otherwise estimated as MCQ - 5pp), the composite, and the score-band cutoffs averaged from recent College Board released exams (per-exam tweaks for Calc AB/BC, Stats, Bio, Chem, Phys 1, USH, World, Euro, Lang, Lit, Psych, Gov, Micro, Macro, CSA, CSP). Raw-to-scaled conversion comes from src/lib/scoring.ts (historical released-form averages, labeled approximate); the underlying scoring math is unchanged. The new /report/$reportId route shows the full report and a Review answers tab listing every question with the student's answer (red), the correct answer (green), and the explanation, pulled from the linked quiz_attempts row. The dashboard now renders a Score progress line chart (recharts) tabbed by SAT / ACT / AP with the correct y-axis range and quick-links to the four most recent reports.

  • Shipped

    Games Hub: Time Attack, Survival, Daily Challenge, Boss Battle, Flashcard Sprint

    The /games page is now a full Games Hub with five new single-player modes alongside the existing live multiplayer (now at /games_/multiplayer) and matching (/memory). (1) Time Attack: a 60-second round - answer as many MCQs as you can from any test bank; score = correct + a 0.5x speed bonus. (2) Survival: three lives, difficulty bucket auto-ramps from easy to medium to hard as your streak grows, run ends after three misses and saves best streak. (3) Daily Challenge: a deterministic 10-question set seeded by today's UTC date (rotates daily across SAT, ACT, PSAT, and major AP banks) so every player gets the same questions; one attempt per day enforced by checking test_attempts for test_id='daily-YYYY-MM-DD', with a running consecutive-day streak counter computed from the last 30 days of daily rows. (4) Boss Battle: face an inline-SVG pest with 10 HP - each correct answer lands a hit (boss shake + hit sound), each wrong answer costs one of your 3 HP (player shake); defeating the boss awards a +3 score bonus. (5) Flashcard Sprint: 90 seconds of rapid recall - prompt shown, tap or Space to flip to the answer + explanation, then self-grade Got it (J) or Missed (F). Every mode pulls from the existing buildQuestionSet bank, posts to test_attempts via the existing recordAttempt server fn (mode = 'time-attack' | 'survival' | 'daily' | 'boss' | 'sprint'), and shows a personal best-runs leaderboard scoped to (test bank x mode) ordered by score. Shared UI lives in src/components/GameBits.tsx (QuestionCard, Chip, GameFrame, BestsPanel, countdown hook, seeded shuffle, inline-SVG icons IconClock/Heart/Calendar/Sword/Cards/Users/Grid/Trophy/Flame, and a Web-Audio sfx kit that respects the existing tp-sound localStorage toggle). All cards and chrome use inline SVG icons - no emojis. Phone-friendly: every game uses auto-fitting grids with min-width chips and tap-sized buttons. Existing recordAttempt continues to award coins (2 per correct + completion bonus, capped at 200/attempt); scoring is unchanged.

  • Shipped

    Florida high schools directory + autocomplete on signup

    The signup 'High school' field is now an autocomplete search over a new schools table seeded with every Florida high school we could source: 1,304 public secondary schools from the NCES Common Core of Data 2022-23 release plus 971 private secondary schools from the NCES Private School Universe Survey 2021-22, for a combined 2,275 Florida high schools tagged with name, district (public schools), city, state, and kind ('public' | 'private'). The picker hits the DB first via a publishable-key server search (name and city prefix + substring), then falls back to the existing bundled high-school + Federal School Code lists for out-of-state coverage. If a student's school still isn't found, the dropdown now ends with a 'My school is not listed — add "<typed name>"' button that inserts a new schools row tagged kind='user' / is_verified=false owned by that user; the row is then immediately selectable and shows up in future searches. RLS: schools is public-read for anon and authenticated, but INSERT is restricted to authenticated users adding their OWN unverified school row (auth.uid() = created_by, kind = 'user'). A unique (lower(name), lower(city), state) index prevents duplicates. Scoring is unchanged.

  • Shipped

    Matching game rebuilt: grid-style tap-two-to-match with timer, combo, leaderboard, and sounds

    The /memory mode is rebuilt to mirror Knowt's matching: an N-pair deck (6, 8, 10, or 12 pairs) is dealt as a single shuffled grid that fits phones and desktops. Tap the first card to select it, tap a second — correct pairs flash green with a brief scale pop and clear from the board; wrong picks flash red with a quick shake and reset. A live header tracks Time, Matched, current Combo, and Best Combo. Combo bonus: completing a run with max combo ≥ 4 awards +1 XP per pair on top of the base 3 XP per pair (early-bail still gives 2 XP per partial pair, no bonus). Per-set best-time leaderboard scoped by (test set × pair count) shows the top 10 fastest completed runs across all players with your row highlighted, plus your own recent runs list. Satisfying micro-animations (selection lift, match scale-and-fade, miss shake) and Web-Audio tone sounds (match chirp, miss thud, escalating combo blip, completion fanfare) — all gated by an in-page Sound toggle persisted to localStorage so the existing sound preference is respected. Pairs are still drawn from any SAT/ACT/AP/PSAT question set (term = question stem, definition = correct answer). New columns on memory_sessions: max_combo + pair_set_key, with an index on (pair_set_key, seconds) WHERE completed = true for fast leaderboard queries. XP and coin awards continue to use the existing award_coins flow; scoring is unchanged.

  • Shipped

    SAT/ACT/AP email newsletter, urgent alerts, and personalized nudges

    Students who opted in at signup now receive a focused email program for SAT, ACT, and AP. A new newsletter-daily edge function runs at 5am UTC every day (via pg_cron) and: (1) sends URGENT alerts immediately when something time-critical hits — scores drop tomorrow/today, test day is within 2 days, a registration deadline is within 3 days, or a format change announcement is within 14 days; (2) sends a day-before-test personalized nudge ('sleep well, eat well, pack ID + admission ticket + calculator') that names the user's top 3 weakest topics from recent attempts; (3) sends a weekly Monday study reminder referencing weak skills (e.g. 'This week: focus on Heart of Algebra'); (4) otherwise accumulates upcoming SAT/ACT/AP calendar items into a biweekly 'Recent Updates' digest scoped to the user's target test. New tables: newsletter_prefs (email_opt_in + per-channel toggles + unique unsubscribe_token, auto-created for every new signup via auth.users trigger), newsletter_log (audit trail with reference_key dedupe so a single calendar event never double-sends), and test_calendar (seeded with key 2026 SAT/ACT/AP test days, registration deadlines, and score release dates). Sending uses the existing Gmail connector (no new secret required). Every email carries a one-click unsubscribe link to /unsubscribe?token=… that flips all opt-ins off in a single click. RLS: users can read/update only their own prefs and read only their own log; the calendar is public-read; the scheduled function uses the service role. Scoring is unchanged.

  • Shipped

    Score-report upload + dated week-by-week study plan

    Students can now upload a real SAT, PSAT, or ACT score report (PDF or image) at /plan. A new ingest-score-report edge function uses AI vision (Lovable AI Gateway, Gemini) to OCR and extract test type, test date, section scores, and reporting-category/skill breakdowns, then saves them to a new score_reports table (RLS-scoped, per-user private storage bucket). A second generate-study-plan edge function builds a dated week-by-week plan from the target test, target date, latest score report, and recent in-app practice — front-loading the weakest skills (e.g. 'This week: 3 Heart of Algebra drills + 1 timed module') and adding 1-2 timed full-length sections in the final two weeks. The plan is shown as a checklist on /plan with the current week highlighted, per-task checkboxes that persist, and a 'Recalibrate now' button that regenerates against the latest accuracy. Students with no prior score can run a diagnostic-based plan instead. New tables study_plans (one row per user, upsert) and score_reports both enforce RLS so users see only their own data. Scoring is unchanged.

  • Shipped

    Teacher Classroom: roster, assignments, analytics, smart suggestions

    Teachers now have a real Classroom dashboard at /classroom/$classId, opened from the Open classroom button on each owned class. Classes carry an exam_focus tag (SAT, AP Calc AB, etc.) set at creation, and students continue to join by code. The Roster tab shows every joined student with submissions, missing assignments, average score, accuracy, a 30-day rolling activity streak, and last-active date. The Assignments tab posts work to the whole class with a due date and optional time limit, pulling questions from the existing SAT/ACT/AP question banks in three flavors: full test (mixed topics), section drill (one topic), or skill drill (topic + difficulty); each assignment tracks completion percentage and class average. The Analytics tab surfaces weakest skills across the class (per-topic accuracy computed from stored answers + per-question topic stamped at assign time), bottom-third struggling students under 70% accuracy, engagement (active in last 7 days), and a streak leaderboard. A smart-suggestion banner auto-recommends a drill for the weakest topic (e.g. 'Class accuracy on Geometry is 54% — assign a focused drill') with a one-click Assign drill button that pre-fills the panel. All access is enforced by existing RLS: teachers see only their own classes/assignments/results, students see only their own assignment results. Scoring is unchanged.

  • Shipped

    Pre-test setup + proctor flow for full-length SAT and ACT

    Full-length SAT and ACT now route through a real proctor flow before the clock starts. (1) Pre-test Setup screen lets you pick sections (SAT: Reading & Writing + Math are required; ACT: English + Math + Reading are required, Science and Writing are optional checkboxes), choose timing (Standard, Extended +50%, Extended +100%, or Untimed/self-paced), toggle Extra Breaks, and pick a feedback mode (End-only is the default for full-lengths; Instant available for practice-style review). The timing multiplier is applied to EVERY section's countdown — extended-time runs the section's clock at 1.5x/2x and lets you self-pace within that window; Untimed disables the countdown entirely. (2) A skippable Proctor Guide overlay then walks through phone-away, section order, timing, breaks, and tools; the timer stays paused until you press 'Begin Section 1'. (3) Real between-section Break screens show a 10-min countdown with the next section name and a 'Skip break, start next section' button; real-exam break points (after SAT R&W; after ACT Math) are always shown, and Extra Breaks adds one between every section. (4) Hard auto-submit per section: when a section's clock hits 0 the section is locked, all currently-picked answers in that section are submitted, and the user is advanced past the section; expired sections cannot be reopened. Remaining time, expired-section list, and per-segment start markers persist server-side in exam_resumes.meta so refreshing or leaving the tab can't reset or extend the section. (5) Single-section drills (e.g. just SAT Reading & Writing, ACT Reading, ACT Science) continue to start with the real per-section timer via the existing 'Full-length section' button on each section page, and full-length drills of one skill area within a section run at the same real section timing. Scoring and answer keys are untouched.

  • Shipped

    Real-test in-exam mode: fullscreen Bluebook/ACT-style shell with two-pane reading and exam tools

    Full-length SAT/PSAT and ACT runs now boot into a dedicated fullscreen exam shell that mirrors Bluebook and the digital ACT instead of sitting inside the app's playful chrome. The site nav, footer, XP, and Pesty are hidden via a body.tp-exam-active class. A neutral exam header shows the TestPest AI lockup left, the active section name and question position center-left, a hide/show-able countdown timer with a 5-minute warning toast, and a tool rail on the right (zoom A−/A+, three-color highlighter, answer eliminator, mark-for-review flag, line reader, calculator, reference, pause, exit). Passage-based sections — SAT Reading & Writing, ACT English, ACT Reading, and ACT Science — render in a draggable two-pane layout with passage/stimulus on the left and questions on the right; non-passage sections collapse to a single pane. The footer shows Back/Next, a Question Navigator grid (answered/unanswered/flagged with current-question highlight) and a Go-to-Review entry, plus a Review-before-Submit screen that lists every question's status and only finalizes when the user confirms. Real-test modes (full section and full exam) suppress per-question feedback — explanations only appear on the score report; Quick Practice keeps instant feedback unchanged. Answers, correct keys, scaled scoring, and the existing adaptive SAT Module 2 routing are untouched.

  • Shipped

    SAT and ACT geometry now ships with real, scalable diagrams

    Geometry on SAT and ACT now looks and works like the real test. We added a typed figure field on questions and a new inline-SVG renderer (src/components/GeometryFigure.tsx) that draws clean, scalable diagrams from question data — triangles (with right-angle squares, side labels, and angle measures), right triangles, rectangles, squares, circles (with sectors, arcs, chords, and diameters), regular polygons, parallel lines cut by a transversal, coordinate planes (axes, ticks, points, segments, and circles by equation), similar-triangle pairs, rectangular prisms, cylinders, cones, and spheres. No external images are downloaded — every figure is generated inline so it stays crisp at any size, dark-mode friendly, and copyright-safe. We appended 15 new SAT geometry items and 15 new ACT geometry items spanning lines and angles, triangles (including 30-60-90 and 45-45-90 special right triangles and right-triangle trig), circles (arc length, sector area, circle equations), area and volume of solids, and coordinate geometry (distance, midpoint, slope, circle equations). Existing items were not modified, deduped, or replaced. Scoring is unchanged.

  • Shipped

    Math symbols and exponents now render correctly everywhere

    Audited all 2,490 items across the SAT, ACT, and AP banks for character-encoding bugs and broken images. Result: 0 mojibake (the data was already proper UTF-8 — the â in 'Châtelier', 'papier-mâché', and 'Brâncuși' were correct accented letters, not encoding errors), 0 missing or broken image references, and 0 literal LaTeX delimiters. The real rendering bug was caret-notation exponents (x^2, r^2, (h+k)^3, etc.) — 558 occurrences across math-heavy items that were printing as plain 'x^2' instead of x squared. We added a lightweight in-app math renderer (src/components/MathText.tsx) that converts ASCII math conventions into the right typographic forms — x^2 → x², H_2 → H subscript 2, sqrt(x) → √(x), <= → ≤, >= → ≥, != → ≠, +- → ±, and pi/theta/alpha/beta/etc. to π θ α β — and wired it through question stems, answer choices, and explanations in both the test runner and the live multiplayer game room. No correct answers or scoring were changed.

  • Shipped

    Every passage now carries a real-test source attribution

    Every passage-based question across SAT, ACT, and the AP banks (English Language, English Literature, US/World/European History, Psychology, Human Geography, US Gov, Comp Gov, Macro/Micro, Biology, Chemistry, Environmental Science, Physics, Statistics, the world-language exams, and more) now displays an authentic-looking source line above the passage, the way College Board and ACT print them. We added a typed source field on questions — { title, author, year, type } — plus a reusable helper (src/lib/attribution.ts) that formats it into a real-exam-style header and an AI generator contract that requires a source on every future passage item. Genuine public-domain excerpts (Austen, Douglass, Du Bois, Federalist, Lincoln, Frederick Douglass, MLK's Letter from Birmingham Jail, etc.) keep their real title/author/year. Original or contemporary passages get a plausible, diverse FICTIONAL author and title (e.g. 'This passage is adapted from The Tides of Memory (2019) by Elena Marsh' or 'Adapted from a 2021 article on marine biology by Dr. Raymond Okafor') — no famous living authors, no modern copyrighted text. Scoring and answer keys are unchanged.

  • Shipped

    Pesty is now your guide, not just a logo

    Pesty the pest mascot now actually guides you. First-time visitors get a friendly 4-step welcome tour with speech bubbles (Hi → pick your test → set a goal → here's your dashboard) and Next/Skip buttons; the 'seen' flag persists in localStorage so it never repeats. A small persistent Pesty button lives in the bottom-right corner of every page and pops a short contextual tip once per session on the dashboard, tests list, in-test screen, games, and Strategy Hub (e.g. 'tap Reference for the Desmos cheat sheet and grammar rules — both work mid-test'). Pesty has expressive moods — wave on onboarding, happy on a correct answer, encouraging after a miss, celebrating on a 3+ streak, thinking while loading — using a tiny global event ('pesty:mood') so any screen can trigger them. The mascot respects prefers-reduced-motion (no bobbing/pop animations), is dismissible (right-click the button to hide for this device), and is fully theme-token styled.

  • Shipped

    Reading passages now show real-test attribution lines

    SAT Reading & Writing, ACT Reading, ACT English, and ACT Science passages now display an authentic-looking attribution header above the passage, just like the real exams. Contemporary science and social-science passages are original TestPest text shown with a generic source line (e.g. 'Adapted from a 2021 article on marine biology' or 'Based on a 2020 laboratory study of enzyme kinetics'), so no fabricated bylines are attached to real living authors. For literature, history, and founding-document items we shipped a new set of passages lightly adapted from genuine PUBLIC-DOMAIN works with the real title, year, and author — including Jane Austen (Pride and Prejudice), Frederick Douglass (Narrative of the Life), W. E. B. Du Bois (The Souls of Black Folk), Mary Shelley (Frankenstein), Nathaniel Hawthorne (The Scarlet Letter), Charles Dickens (A Tale of Two Cities), Kate Chopin (The Story of an Hour), Booker T. Washington (Up from Slavery), Elizabeth Cady Stanton (Declaration of Sentiments), James Madison (Federalist No. 51), and public-domain NASA educational material. No modern copyrighted text was used and scoring is unchanged.

  • Shipped

    Six themes + a polished dark mode (footer finally dark!)

    The footer (and every other surface that was leaking the light palette into dark mode) now follows the active theme — dark themes get a true dark footer instead of a light/lavender one. Dark itself was retuned: near-black background, slightly lifted card surfaces with subtle inner highlight and outer elevation, AA-contrast text, and retuned plum/tangerine accents. Four brand-new full themes shipped alongside Playful and Dark: Ocean (deep blue), Forest (deep green), Sunset (warm amber/rose), and Grape (rich purple). The theme picker in the nav drawer now shows a small color-swatch preview for each option (Playful, Dark, Ocean, Forest, Sunset, Grape, System), persists to localStorage, and applies before first paint so there is no flash of the wrong theme on load.

  • Shipped

    Teachers and tutors get their own hub

    The signup role picker (Student / Tutor / Teacher / Parent) now drives where you land after login: students go to /dashboard, teachers and tutors go to a brand-new /educator hub, and parents go to /guardian. Teacher and tutor onboarding skips every student step (no pet, no grade, no target test) and ends on a 'Register your school or tutoring company' step that mints a 7-character join code. New organizations and org_members tables are RLS-scoped so only members can read an org and only the owner can edit it. The /dashboard student UI auto-redirects educators, so the pet/XP screens stay student-only.

  • Shipped

    Strategy Hub + smarter Pesty hints

    New /strategies page collects the fastest SAT/ACT tactics top tutors use: SAT Math Desmos shortcuts (graph the equation, click intersections, tables, sliders, parabola x-intercepts), the full grammar rules list (subject-verb, pronoun-antecedent, commas, semicolons vs. colons, possessives, modifiers, parallel structure, tense, transitions, concision), reading tactics (read the question first, hunt for textual evidence, eliminate extremes), and per-section pacing for SAT and ACT. The in-test Reference button now opens a tabbed popover with the math sheet, Desmos cheat sheet, and grammar rules. Pesty's hints reference these exact strategies — naming the Desmos move on a math item or the specific grammar rule on a writing item, and teaching step by step instead of dumping the answer.

  • Shipped

    Live multiplayer quiz games are live

    Host a Kahoot-style room from /games: pick an exam, number of questions, and seconds per question, then share a 6-digit join code. Players (signed-in or guests with just a nickname) see the lobby fill in real time via Supabase Realtime, then everyone gets the same question at the same time with a synced countdown. Faster correct answers earn more points, a live answer tally shows after each round, and the game ends on a 1st/2nd/3rd podium. Signed-in players' results save as a mode=game attempt and award coins. Guests use a token-backed RPC, players can disconnect and rejoin from the same device, and the host's End game wraps the room cleanly.

  • Shipped

    Friends, parent, class, and tutor codes verified end to end

    Every account ships with a unique friend code and shareable invite link. Friends page supports send-by-code, incoming/outgoing requests with accept/decline, remove, and a new Block action. Parent, class, and tutor codes link guardians, classrooms, and educators.

  • Shipped

    Memory Match and Assignments are live

    Memory Match deals real cards from the question bank with XP rewards, and Assignments lets students take and submit teacher-assigned work with SAT/ACT/AP score scaling.

  • Shipped

    Feedback form on every status page

    Status pages collect user notes (page, message, optional email) into a feedback table so issues can be triaged and prioritized.

  • Shipped

    Parent and Teacher portals wired up

    Replaced placeholders with real flows: parent code linking + AI child reports; teacher classes, assignment generation, lesson plans, essay check, analytics with premium gating.

  • Shipped

    Top-nav links for Memory, Assignments, Parent, Teacher portal

    Header now mirrors the footer so the new sections are reachable in one click.

Recent audit changes

  • Audit fix

    Explanation math now renders on every review surface (score reports, attempt modal, review deck, sprint, practice, AP sample)

    Follow-up pass on PRIORITY 1: the in-test reveal already routed through <MathText>, but six other places displayed q.explain / it.explanation as raw text, so 'cdot' and 'fracddx' could still leak there. Wrapped them all in <MathText> so the bare-LaTeX rescue and KaTeX both apply: src/components/AttemptDetailsModal.tsx (the dashboard 'Why:' block), src/routes/report.$reportId.tsx (score report explanations), src/routes/review.tsx (review-deck reveal), src/routes/games_.sprint.tsx (sprint flip card), src/routes/practice.tsx (post-quiz explanation details), and src/routes/ap.$subject.tsx (AP subject sample). No question data, scoring, or copy changed.

  • Audit fix

    Theme system fixed: Playful, Dark, and System modes

    Dark mode no longer renders unstyled. Themes now run off a single source of truth (CSS variables + HSL design tokens) covering background, foreground, card, popover, primary, secondary, muted, accent, border, input, and ring. The nav dropdown exposes a Theme: Playful / Dark / System control, the choice persists in localStorage, and a pre-paint script applies the saved theme before first paint so there's no flash of the wrong theme. Dark palette retuned for WCAG AA contrast.

  • Audit fix

    End-to-end audit checklist

    Added docs/AUDIT_CHECKLIST.md covering /memory, /parent, /assignments, /teacher-portal: pre-flight, route-level checks, RLS, cross-cutting, and sign-off.

  • Audit fix

    School save persists and stops nagging

    School nudge now caches per-user, listens for profile-updated events, and is suppressed on /school, /onboarding, /dashboard-settings, /setup.