One care record.
Every pair of hands.
I led design and frontend across PulzAid’s public site, employer experience, and OSHS field operations — three years of decisions, shown with the evidence.
Design + frontend leadProduct design · design system · React/TypeScript
Three connected surfacespulzaid.com · pulzaid.app · OSHS Workbench
Shipped with the teamClinical, operations, product, and engineering

Maya R. · submitted 10:58 AM · ready for review
2 internal · 1 customer-visible
“Confirm PO # before invoicing”
Employee phone corrected
Service record · incident report
Wound care · EN → sent by SMS
The product was not a page. It was a chain of trust.
PulzAid sells employers and their insurers a faster, cheaper path through workplace injuries — AI triage, telemedicine, imaging, and field first aid coordinated as one service. So each injury travels through an employer on the phone, a remote clinician, a field technician in a hard hat, a processing team, and the customer paying for it all. Each needs a different level of detail — but every one of them has to believe they are looking at the same case.
The rule I held for three years: one record, many role-specific views, no break in continuity.
Understand
Employers, brokers, and carriers learn what the care model is — and try the triage themselves.
Request
A guided intake captures the incident once, with drafts that survive interruptions.
Operate
OSHS teams coordinate triage, dispatch, field care, and paperwork from one Workbench.
Follow through
Customers watch four honest milestones; workers get aftercare in their language.
The site was selling a company that no longer existed.
The 2023 site was a two-week template stand-up by the founding team — the right call for a seed-stage company, and it stayed live while the business outgrew it. The 2025 rebuild I led started from information architecture, not from a moodboard.


“Loved by businesses worldwide.”
The first cut shipped with the template’s TaxPal testimonials, pricing table, FAQs, and © TaxPal footer still live. A hurried April 2023 scrub swapped the strings but kept the shell — invented placeholder testimonials on template avatars ran in production for the next two and a half years. Credible at a glance; hollow the moment a broker or carrier read past the hero. That gap between looking real and being real was the design problem.

One scrolling homepage. Every audience, one generic pitch.
- Injury care solutions
- Onsite & nearsite care
- Medical surveillance
3 service pages, anchor-linked from a mega-menu
- Partner with us — TPAs · Captives · Brokers · Agents · Carriers
- Contact
5 insurance audiences, each with its own economics
- Intelligent triage
- Triage playground (live AI demo)
Visitors can try the classifier before a sales call
- Aftercare center — 6 topics × 3 languages
The site becomes part of care delivery, not just marketing
The decision: organize by visitor intent, not by org chart. 11 page templates in four intent groups — so a broker, an employer, and an injured worker each find their own path within one click of the header.
Let visitors try the AI — after designing its limits.
The triage playground puts PulzAid’s core capability one textarea away from any skeptical buyer: describe an injury, watch it classify severity and recommend a next step. The design work is everything that keeps a public medical demo from becoming advice.
5 requests / 24 h per visitor — a cookie-enforced cap on both client and server
Input hard-capped at 2,000 characters; nothing stored, nothing linked to a person
Conservative tie-breaking baked into the prompt: prefer imaging, choose the higher severity
A visible disclaimer and a designed error state — “demo”, never “diagnosis”

Marketing pages don’t end a care journey. This one does.
The last mile of a field visit belongs to the worker, not the buyer — so the marketing site had to carry it. Six care topics, three languages, co-branded with On-Site Health & Safety.

Twenty-eight questions, asked next to an injured coworker.
That was the 2024 request surface. Someone reporting an injury is having a bad day — the interface must not add “start over” to it. The redesign triaged the fields themselves, then kept the relationship alive after submit.


Where every field went
978-line component · 28 inputs → 3 steps- Employee identity & contact
- Jobsite company + location
- Approving manager
- Org-specific fields (PO #, store #)
- HIPAA acknowledgment gate
- Emergency check first
- Symptom onset + description
- Photo / file attachments
- SSN block → removed from intake entirely
- Exam-space checklist → moved to the imaging request flow, where it belongs
- Body-part selectors → cut from intake; they survive only in the OSHS X-ray request, where a technician actually needs them
The test for every field: does the person on the phone need it right now to start care? If not, it moved to the flow that actually consumes it — or left the product. Step 3 is a review screen, because trust needs a look-before-you-send moment.
form.watch() streams each keystroke of the wizard into a serializer — no save button, no “are you sure”.
Values + current step land in injuryRequestV2_facts / _step. Attachments are deliberately stripped — medical files never sit in localStorage.
Reload, tab crash, dropped call — the wizard rehydrates to the same step with the same values. Submit (or an explicit Clear) wipes both keys.

“What happens next?” needed its own interface.
After submit, the operational machine produces about forty event types. The customer page translates them into four milestones that only move forward — and the AI’s triage output is framed as a recommendation card with human actions beside it, never as an automatic decision.
~40 operational events → 4 stable milestones
EventType enum → CaseStatus enumThe decision: customers never see the machine. Every internal event still lands in a readable timeline, but the promise at the top of the page is four words long — and it only ever moves forward. The page polls every 30 s while a case is active and stops when it becomes terminal.
Three copies of the truth, one Workbench.
When PulzAid partnered with On-Site Health & Safety, the workflow review surfaced a deeper problem than any single screen: the same incident was being re-typed across phone intake, telehealth, field first aid, and final filing — then reconciled by hand.
“I’ve been waiting for a new amazing form, Brendan, since 2018.”
— OSHS’s processing lead to PulzAid’s founder, in the April 2026 workflow review. The scoping decision came out of that meeting: don’t build every feature — solve paperwork continuity first, from intake through dispatch to the field technician.
One injury, three copies of the truth
Workflow → Workbench
The first concept treated care as a fixed sequence. Field reality isn’t: a case might need a second first-aid visit, an x-ray order, a tetanus release, or a follow-up call — in any order, in parallel. The product had to model work that accumulates, not steps that advance.
One prescribed pipeline. Breaks the moment care branches, repeats, or runs in parallel.
Seven addable, assignable work packets — three of them repeatable — around one stable case header.

Maya R. · submitted 10:58 AM · ready for review
2 internal · 1 customer-visible
“Confirm PO # before invoicing”
Employee phone corrected
Service record · incident report
Wound care · EN → sent by SMS
One stable case header
Identity, employer, jobsite, assignee, and the stage badge stay pinned above every packet.
Packets, not pages
Each unit of work has a lifecycle — draft, ready for review, needs correction, complete — and repeatable types can multiply.
Coordination beside the form
Notes, change requests, attachments, reports, and aftercare SMS live in a rail — visible, never entangled with clinical fields.
Review is a first-class state
Nothing reaches a customer unreviewed. “Ready for review” drives the queue, and corrections loop back without erasing work.
One record, three vocabularies of state
derivedStage · precedence rollupThe badge isn’t stored — it’s derived from work-packet content by a precedence rule (closed > needs_more_info > needs_assignment > field_in_progress > awaiting_review > …), so a case with parallel, repeatable packets still rolls up to exactly one honest answer to “where is this case?”
Four milestones that only move forward. Internal churn — corrections, reassignments, a second field visit — never shows up as backwards motion.
An employer following a link gets three words and a timeline — nothing they can’t act on.
The less someone can act on detail, the less detail the interface owes them.
Offline is a requirement, not an edge case
IndexedDB queue + service worker · shipped Aug 2026navigator.onLine lies on captive portals. Every write just tries the real network and falls back on failure — the interactive add-form call is capped at 4 s so the button never freezes.
Failed submits persist in IndexedDB keyed by a client-op id — the idempotency key that makes replays safe to repeat.
On reconnect the queue replays in creation order. 401 / 403 / 408 / 429 are kept and retried; poison requests are dropped, not looped.
Drafts mirror to disk 500 ms after each change. The packet shows “saved on this device” — never a fake “submitted”.
A service worker caches the app shell on first visit and pre-caches the six body-part diagrams the X-ray form renders lazily — so a form opened in a basement still has its images. Reads are cached; writes only ever go through the queue.
One system, three very different rooms.
A marketing site, a customer app, and a field operations tool share one design system. Its structure is tokens and primitives; its feeling is a deliberate emotional register; its craft is the part no table specifies — what a loading state should do to someone waiting on a medical answer, and why the hero wave is trigonometry instead of a video file.
Two palettes, one anchor
tailwind.config · both reposneutrals
mainGreen and primaryGreen are shared with the site — same anchors, different jobs.
The rule that holds both: green is the anchor; saturated color is reserved for state. When a status pill turns amber or red, it is the only loud thing on the screen — so a full case queue has at most a few loud things in it.
A voice per altitude
next/font · display swapPrimitives, alive
hover & focus me“PulzAid should feel calm, clinical, fast, and trustworthy.”
Each word had to cash out in an enforceable rule — a feeling you can lint for:
Voice is a design token too
Direct, supportive, operational. Internal vocabulary — enum names, packet ids, API errors — never crosses into a customer-facing surface; the stage-translation layer exists precisely so it doesn’t have to.

The triage loading state is 210 lines of hand-built SVG/CSS: counter-rotating rings (12s / 8s reverse), a pulsing core, and staged status lines that tell someone waiting on a medical answer what is happening. Zero libraries.
Three brands, one company
2021 style guide → todayPoppins, dark cyan + grass green, a friendly bandage mark. Designed for workers’ phones.
New wordmark and teal on a borrowed shell — speed over identity, and it showed.
Lexend / Sora / Inter, the wave drawn from trigonometry, glass surfaces, tokenized state color.
Anatomy of the brand wave
Every line is one sine term. Phase φ blends a shared “breathing” motion (70%) with a per-line cascade (30%), so the field moves together without moving in lockstep.
Color is computed, not picked: the hue drifts between emerald and teal per line, per frame — the brand palette expressed as a function.
Wavelength is locked to a fixed reference width, so the composition holds from a phone to a 5K display instead of stretching.
Line width (0.6–2.8 px) and opacity follow the crest. Where adjacent peaks align, lines widen and fade up to 90% — the moiré is designed, not accidental.
Production tunes 18–42 lines to viewport height, 12–15 on mobile; this specimen is pinned at 16. Retina rendering is capped at 2× DPR to protect battery.
~480 lines of canvas code on requestAnimationFrame, looping at t mod 8π — no video file, no Lottie JSON, no image request. The only bytes are the code.
What’s live, and what I’d still change.
Everything above is running in production — the site, the wizard, the Workbench, the offline queue. The honest gap: we shipped the request wizard without instrumenting time-to-submit, so “approximately 2 minutes” is still a design contract, not a measured median. That’s the first thing I’d wire up next.
The screens were never the hard part. The hard part was deciding what stays synchronized, what each person should never have to see — and holding those decisions all the way into production code.
Owning both design and frontend meant no decision got lost in a handoff — the stage vocabulary and the offline contract shipped as specified, because the spec and the PR were the same artifact.