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)

MetricGradeNotes
Design ScoreD+4/10 across 7 review dimensions
Information ArchitectureC-Picker OK, child pages wildly inconsistent in depth
Interaction StatesD”Loading verification…” can sit stuck forever
User Journey / ArcC-Internal flow jumps to Cloudflare login with no signposting
AI Slop / SpecificityC+Festive winter background still on in May
Design SystemDNo DESIGN.md; subscribe + admin diverged
Responsive / a11yDDark mode broken on /subscribe-programs; pinch-zoom blocked (WCAG 1.4.4 fail); placeholder-as-label (WCAG 3.3.2 fail)
Decisions DocumentedD+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

DecisionEffect
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=noPinch-zoom restored across all three subscribe pages. WCAG 1.4.4 fix.
D19 — visible labels + autocomplete + aria-describedbyForm a11y compliance. iOS autofill + password managers now wire up correctly.
D8 / E8 / E17 — Turnstile refactorButton 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 flagNew SELECT subscribers WHERE email=? before the upsert returns a returning: boolean in the response. Frontend renders ”✅ Welcome back. Fresh link sent to ” for known emails. Graceful degrade on D1 read failure.

Architectural moves

DecisionEffect
E10 / E10b — Introduce Eleventy SSGsrc/dist/ via Nunjucks. Replaces the copy-paste drift that let festive bg land on Programs but not Internal.
E12 — Shared renderError helper + error-block partialStandard 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> migrationBoth 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 successModalPost-form-submit confirmation is inline #result; no redundant “check your email” modal.
D6 / D14 / D15 — Programs page layout collapseThree 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 modalOne-line “You’ll see a Cloudflare login page first — that’s expected.” Eliminates the brand-jump WTF moment.
D17 / D18 — Picker rewriteTwo-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 demotedWas 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:

  1. Footer links rendered as harsh bright-blue underlined. New --accent CSS var (#2b67f6 light / #93c5fd dark) drives a global a { 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).
  2. Form inputs squished. Padding 10 → 14, form gap 8 → 10, label margin compensated. Added input:focus { outline: 2px solid var(--accent) }.
  3. Admin “Admin” was a tiny underlined link. Promoted to .admin-footer-btn (rounded surface-colored button with ⚙ glyph).
  4. Admin header overflowed right edge on phone. New @media (max-width: 768px) block in admin.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.
  5. <select> Programs Calendar triggered iOS system wheel for a one-option non-choice. Replaced with <input type="hidden" id="cal" value="programs"> + visible .cal-badge pill. JS contract (cal.value in ~14 fetch sites) preserved; dead change listener removed.

Decisions held during triage

DecisionChosenAlternative rejected
Scope splitOne PR for the redesign + Eleventy + API changeTier A bleeds first, Tier B redesign later
SSG choice (E10b)Eleventy 11ty — Layer 1 boring, vanilla HTML outputAstro (overkill for ~5 pages); custom Node script (reinventing); web components (runtime cost)
Test framework (E15)Vitest + happy-dom + bash/browse scriptsPlaywright (real weight for ~5 pages)
<dialog> polyfill (E9)Ship without — RC members are on Safari 15.4+ in 2026Feature-detect + div-modal fallback
Turnstile retry cap (E8)5 retries then contact-only2 (too tight); unlimited (loop risk)
Re-issue path (D5)Reuse the existing form + microcopySeparate /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 caseCustom <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.

  1. Forms have visible labels + autocomplete per-input. Never placeholder-as-label. Never drop autocomplete="name"/autocomplete="email" — iOS autofill depends on them.
  2. Modals are native <dialog>. Open/close via window.IhnycRc.dialog.openDialog(el, triggerEl?) / closeDialog(el). Don’t write a <div class="modal hidden">; you’ll lose focus trap, Escape, and ::backdrop styling.
  3. 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.
  4. 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”.
  5. Page-specific <style> blocks are for genuinely page-specific styling only. Anything reusable goes in styles.css with a CSS variable. Never re-declare link colors in a page — the global a { color: var(--accent) } rule handles them.
  6. No body background !important overrides. This was the bug that broke dark mode in 2026-05; the rule is now in cal-landing/DESIGN.md.
  7. Seasonal / decorative backgrounds require a date gate AND prefers-reduced-motion: reduce respect AND an opt-out. A perpetual GPU animation for May–November is not seasonal.

Admin conventions added (PR #13)

  1. <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.
  2. Header chrome is mobile-fragile. Any addition to the admin header must verify at 375px. The current @media (max-width: 768px) block in src/admin/admin.css hides 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:

  1. Font unification across public + admin. Currently Ubuntu (public) + Inter (admin) on purpose, but worth re-evaluating once the public side stabilizes.
  2. 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-motion respect. Not a !important override in a page-specific <style>.
  3. VoiceOver pass on iOS + macOS. First time the public pages get a real screen-reader audit. Capture findings in a new A11Y_AUDIT.md.
  4. Migrate admin pages to Eleventy. Public side is now on Eleventy; admin still pass-through copy. Doing so would let head/header/footer partials be shared across the whole site instead of duplicated three times in admin pages.
  5. 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.
  6. Internal calendar admin. When it lands, replace the hidden-input + badge in src/admin/index.html with a real custom dropdown (<details>-based menu). Never re-introduce <select> per convention 8 above.
  7. media/*.mp3 audio files. The commented <audio> block was removed in PR #12, but the three media binaries are still in src/media/. Decide whether to remove them.