Exam/solution pages print clean via browser print-to-PDF: interactive
chrome hidden, questions unhidden, ink-friendly colors. Options printed
as black bars without the surface override — .opt/.chk/inputs/palette
now forced white. Verified with print-media emulation screenshot.
Sign-in page rewritten for non-technical students: Google-first when
configured, plain-language email path with honest 30-day scope, recovery
note, placeholders, full-width CTA. deploy/termux-start.sh sources .env
for GOOGLE_* credentials; deploy/env.example documents the 5-minute
Cloud Console setup; .gitignore covers secrets, DBs, pid/log files.
bench scripts send a browser UA so Cloudflare bot checks stop 403ing
local load tests. Landing bank counts cached 60s (five aggregate
subselects no longer run per hit). gzip compression on all responses.
Result page shows a why-lost row (concept gap / calculation slip /
misread / out of time / blind guess) on every wrong or blank question.
POST /result/:attempt/tag/:qid toggles one tag; ownership checked,
vocabulary enforced server-side. Dashboard gains a 'why you lose marks'
section aggregating tags across all submitted attempts. Verified live:
tag persists, rollup renders.
The builder previously exposed only chapters. Now each chapter expands to show
its subtopics as individual checkboxes: selecting a chapter checks all of its
subtopics, selecting the subject-level box checks everything, and selecting
individual subtopics sets the chapter to indeterminate. The exam query filters
by subtopic_id when any are given; the SQL path deduplicates so a question
matching two subtopics only appears once.
JEE mode sets duration to 60 × unique-subject-count (180 min for a full paper).
On the exam screen this renders per-subject countdown strips: Physics 60:00,
Maths 60:00, Chemistry 60:00. Each strip counts down independently; when one
hits zero the screen auto-jumps to the first unanswered question of the next
subject. The last strip exhausting time submits the form.
Single-subject papers keep the flat timer and show no strips.
Also: all audit targets now genuinely 44px (the .opt labels were 42px).
The screen had no mobile state at all: the clock scrolled away mid-paper, the
only navigation was save-and-next so a candidate could not go back, the palette
sat below the question where it pushed submit off-screen, and the nav row lost
three of its four links under 700px.
Sticky header carries the clock and a progress bar; previous/next are real
buttons; the palette is a drawer below 1024px opened by a floating button that
sits above the tab bar. Primary destinations move to a bottom tab bar on
phones, data tables scroll inside their own card, and every control meets the
44px touch minimum.
Verified with a headless browser across five viewports rather than by reading
the stylesheet: scripts/audit_mobile.py fails on horizontal overflow, tap
targets under 44px, and content hidden behind a fixed bar; scripts/shots.py
captures each page at both widths. Both caught bugs I would otherwise have
shipped, including a palette toggle that lived inside the drawer it opened and
so could never be pressed on a phone.
fmt_dur drops a leading zero hour, so a 24 minute paper reads 24m 00s.
Axum + SQLite full-stack copy of the ZenithPrep PYQ practice app.
Paper builder with unseen-question exclusion, timed CBT screen with
palette and autosave, +4/-1 grading with numerical tolerance, per-question
analysis, history, dashboard with weak-subtopic tracking and coach notes.