Produced by /plan-design-review + /plan-eng-review on 2026-05-24. Shipped over two PRs:
- PR #12 (merged 2026-05-25) — the redesign + Eleventy build pipeline introduction
- PR #13 (in flight) — link styling, input padding, admin link → button, admin mobile header
Full planning artifacts (SUBSCRIBE_UX_DESIGN.md, test plan, qa scripts) live in the cal-landing repo and the operator’s ~/.gstack/projects/dghauri0-ihnyc-rc-cal-landing/. This doc is the durable summary that should outlive those.
Baseline (pre-PR)
| Metric | Grade | Notes |
|---|---|---|
| Design Score | D+ | 4/10 across 7 review dimensions |
| Information Architecture | C- | Picker OK, child pages wildly inconsistent in depth |
| Interaction States | D | ”Loading verification…” can sit stuck forever |
| User Journey / Arc | C- | Internal flow jumps to Cloudflare login with no signposting |
| AI Slop / Specificity | C+ | Festive winter background still on in May |
| Design System | D | No DESIGN.md; subscribe + admin diverged |
| Responsive / a11y | D | Dark mode broken on /subscribe-programs; pinch-zoom blocked (WCAG 1.4.4 fail); placeholder-as-label (WCAG 3.3.2 fail) |
| Decisions Documented | D+ | 0 captured pre-review |
Findings (49 decisions)
24 design-review decisions (D1-D24) + 17 eng-review decisions (E1-E17) + 8 follow-up TODOs.
Five carried critical user-visible impact: dark mode broken, pinch-zoom blocked, placeholder labels, stuck Turnstile button, no returning-user affirmation.
What shipped (PR #12 — 22 commits)
Critical bleeds fixed
| Decision | Effect |
|---|---|
| D13 — remove festive winter background | ~200 lines of decorative <style> deleted. Unbroke dark mode (the body { background: var(--bg-base) !important } rule was beating the dark-mode override). Killed perpetual snow/twinkle/bgDrift GPU animations. |
D20 — remove user-scalable=no | Pinch-zoom restored across all three subscribe pages. WCAG 1.4.4 fix. |
| D19 — visible labels + autocomplete + aria-describedby | Form a11y compliance. iOS autofill + password managers now wire up correctly. |
| D8 / E8 / E17 — Turnstile refactor | Button label always reads “Get my link” (was “Loading verification…” on cold load). Loading status is a separate <p> above the button. 5-retry cap with contact-only CTA after exhaustion. Polling loop replaced with event-driven Promise. |
D10 / E1 / E7 — API returning flag | New SELECT subscribers WHERE email=? before the upsert returns a returning: boolean in the response. Frontend renders ”✅ Welcome back. Fresh link sent to |
Architectural moves
| Decision | Effect |
|---|---|
| E10 / E10b — Introduce Eleventy SSG | src/ → dist/ via Nunjucks. Replaces the copy-paste drift that let festive bg land on Programs but not Internal. |
E12 — Shared renderError helper + error-block partial | Standard error UI (message + Retry + Contact link) used by every failure path. ES module with window.IhnycRc.errorBlock bridge for classic-script callers. |
E2 / D21 — openDialog/closeDialog helper + native <dialog> migration | Both modals (Internal picker, Google Calendar warning) now use <dialog>. Free focus trap + Escape close + ::backdrop. Helper adds backdrop-click-to-close + return-focus-to-trigger. Safari 15.4+ only (no polyfill per E9). |
| D11 / E3 — Remove successModal | Post-form-submit confirmation is inline #result; no redundant “check your email” modal. |
| D6 / D14 / D15 — Programs page layout collapse | Three stacked cards → one. Welcome philosophy paragraphs → <details> (“About this calendar”). Italic quote moved to muted footer line. Form now first thing on mobile viewport. |
| D7 / E4 — Deep-link redirect | /subscribe?calendar_id=programs redirects to /subscribe-programs with the param stripped. Removes the existing “noop query param” bug. |
| D12 — Cloudflare disclosure in Internal modal | One-line “You’ll see a Cloudflare login page first — that’s expected.” Eliminates the brand-jump WTF moment. |
| D17 / D18 — Picker rewrite | Two-line headline framing audience + access difference. “Subscribe →” verb. Lock glyph on Internal. Back-button renamed to “IHNYC RC home”. Programs card first (open path more common). |
| D22 — Admin link demoted | Was a prominent button on the picker; now a tiny footer link. Refined in PR #13 to a real styled button (still subtle). |
Tests
24 Vitest unit tests across 3 files (6 API + 9 dialog + 9 error-block) in happy-dom env. 4 bash + browse regression scripts under qa/ for the things Vitest can’t touch (dark mode end-to-end, no-JS fallback, Mixpanel firing, CF Access flow).
DESIGN.md
New file at the cal-landing repo root captures: the two visual worlds (public Ubuntu vs admin Inter, on purpose), color tokens, typography rules, form rules, modal rule (native <dialog> only), voice (“calm civic utility”), state coverage rule, file organization, and a “when you add a new page” checklist. Future drift is now harder.
What shipped (PR #13)
Post-merge feedback after PR #12:
- Footer links rendered as harsh bright-blue underlined. New
--accentCSS var (#2b67f6light /#93c5fddark) drives a globala { color: var(--accent); text-decoration: none }rule. Underline shows on hover/focus-visible (a11y signal preserved). Anchors styled as buttons opt out explicitly. Net -30 lines (deleted four per-page link overrides in subscribe-programs.html). - Form inputs squished. Padding 10 → 14, form gap 8 → 10, label margin compensated. Added
input:focus { outline: 2px solid var(--accent) }. - Admin “Admin” was a tiny underlined link. Promoted to
.admin-footer-btn(rounded surface-colored button with ⚙ glyph). - Admin header overflowed right edge on phone. New
@media (max-width: 768px)block inadmin.css: flex-wraps the header rows, hides brand wordmark + breadcrumb on small screens, allows the tab strip to scroll horizontally as a safety net, tightens paddings. <select>Programs Calendar triggered iOS system wheel for a one-option non-choice. Replaced with<input type="hidden" id="cal" value="programs">+ visible.cal-badgepill. JS contract (cal.valuein ~14 fetch sites) preserved; deadchangelistener removed.
Decisions held during triage
| Decision | Chosen | Alternative rejected |
|---|---|---|
| Scope split | One PR for the redesign + Eleventy + API change | Tier A bleeds first, Tier B redesign later |
| SSG choice (E10b) | Eleventy 11ty — Layer 1 boring, vanilla HTML output | Astro (overkill for ~5 pages); custom Node script (reinventing); web components (runtime cost) |
| Test framework (E15) | Vitest + happy-dom + bash/browse scripts | Playwright (real weight for ~5 pages) |
<dialog> polyfill (E9) | Ship without — RC members are on Safari 15.4+ in 2026 | Feature-detect + div-modal fallback |
| Turnstile retry cap (E8) | 5 retries then contact-only | 2 (too tight); unlimited (loop risk) |
| Re-issue path (D5) | Reuse the existing form + microcopy | Separate /resend page (new path to remember) |
| Logo + back-button on picker (D23) | Keep both, rename back to “IHNYC RC home” | Drop one |
Admin <select> (PR #13) | Hidden input + badge for the single-option case | Custom <details> dropdown (overkill until Internal admin lands) |
Public UI conventions (preserve when editing)
These are now load-bearing. Future edits to src/*.html should keep them.
- Forms have visible labels + autocomplete per-input. Never placeholder-as-label. Never drop
autocomplete="name"/autocomplete="email"— iOS autofill depends on them. - Modals are native
<dialog>. Open/close viawindow.IhnycRc.dialog.openDialog(el, triggerEl?)/closeDialog(el). Don’t write a<div class="modal hidden">; you’ll lose focus trap, Escape, and ::backdrop styling. - Errors use the standard error block.
window.IhnycRc.errorBlock.renderError(targetEl, { message, retry?, contact }). Include a retry callback when the failure is self-recoverable; the helper handles a contact link. - Turnstile loading is a separate status note, never the button label.
<p id="turnstile-status">above the button reads “Verifying you’re human…” → “Verified ✓” / failure copy. The submit button always says “Get my link”. - Page-specific
<style>blocks are for genuinely page-specific styling only. Anything reusable goes instyles.csswith a CSS variable. Never re-declare link colors in a page — the globala { color: var(--accent) }rule handles them. - No body background
!importantoverrides. This was the bug that broke dark mode in 2026-05; the rule is now incal-landing/DESIGN.md. - Seasonal / decorative backgrounds require a date gate AND
prefers-reduced-motion: reducerespect AND an opt-out. A perpetual GPU animation for May–November is not seasonal.
Admin conventions added (PR #13)
<select>is iOS-poisoned for narrow option lists. Anything 1–3 options on a touch-first surface should be a custom picker (<details>-based dropdown or button group).<select>is fine for 10+ options where the system wheel is actually faster than a custom UI.- Header chrome is mobile-fragile. Any addition to the admin header must verify at 375px. The current
@media (max-width: 768px)block insrc/admin/admin.csshides the brand wordmark and breadcrumb; new header items should either fit alongside the existing actions OR be opted into the wrap row.
Known deferred work
Tracked in SUBSCRIBE_UX_DESIGN.md TODOS section + this list:
- Font unification across public + admin. Currently Ubuntu (public) + Inter (admin) on purpose, but worth re-evaluating once the public side stabilizes.
- Seasonal background system done right. If festive treatments return, build a date-gated module (Dec 1 – Jan 7 active) with opt-out and
prefers-reduced-motionrespect. Not a!importantoverride in a page-specific<style>. - VoiceOver pass on iOS + macOS. First time the public pages get a real screen-reader audit. Capture findings in a new
A11Y_AUDIT.md. - Migrate admin pages to Eleventy. Public side is now on Eleventy; admin still pass-through copy. Doing so would let
head/header/footerpartials be shared across the whole site instead of duplicated three times in admin pages. - No-JS no-Turnstile fallback. Documented in
qa/no-js-fallback.sh: programs path returns 400 for no-JS clients because Turnstile is required and can’t be rendered server-side. Two fix paths: (a) a server-rendered alternative challenge, or (b) a non-Turnstile rate limit. Out of scope for the redesign PR. - Internal calendar admin. When it lands, replace the hidden-input + badge in
src/admin/index.htmlwith a real custom dropdown (<details>-based menu). Never re-introduce<select>per convention 8 above. media/*.mp3audio files. The commented<audio>block was removed in PR #12, but the three media binaries are still insrc/media/. Decide whether to remove them.
Related
- cal-landing service index
- ops — added a “Public UI conventions (post-2026-05-25 design review)” section pointing here
- admin design review (2026-05-22) — the prior pass; many of its conventions still apply