Back to projects
PulzAid · Occupational health · 2023–2026

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

Product strategyDesign systemsReact / TypeScriptWorkflow designAI guardrailsOffline field UX
PulzAid
One shared care record
pulzaid.com
PulzAid marketing site with the trigonometric wave hero
Explain & provePublic website
pulzaid.app / oshs / workbench / case-2843
Carlos MendezAwaiting Review
Send reportCompile documents
Case#2843EmployerBuildWell Inc.JobsiteOakland, CAAssignedMaya R. · EMT
Work packets2 done · 1 in review
Basic Demographic
Complete
Telehealth Triagemulti
Complete
Field First Aidmulti
In review
Additional Services
Draft
X-ray Imaging Requestmulti
TD/Tdap Vaccine Release
Telephonic Follow-Up
+ Add packet
Field First Aid

Maya R. · submitted 10:58 AM · ready for review

#2 of 2
Service time
10:42 AM
Technician
Maya R.
Assessment
Left wrist strain · mild swelling · ice + compression applied
Disposition
Returned to modified duty
Employee signature captured
Request correctionMark complete
Attachments

2 internal · 1 customer-visible

Internal notes

“Confirm PO # before invoicing”

Change requests

Employee phone corrected

Reports

Service record · incident report

Aftercare

Wound care · EN → sent by SMS

Employer request
OSHS field care
Customer status
Scope
Design + code · one person
Span
2023–2026 · two rebuilds
Surfaces
11 site templates · 37 app routes
Operations
7 packet types · 9 derived stages
The design problem

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.

01

Understand

Employers, brokers, and carriers learn what the care model is — and try the triage themselves.

pulzaid.com
02

Request

A guided intake captures the incident once, with drafts that survive interruptions.

pulzaid.app
03

Operate

OSHS teams coordinate triage, dispatch, field care, and paperwork from one Workbench.

OSHS Workbench
04

Follow through

Customers watch four honest milestones; workers get aftercare in their language.

Case page · aftercare
01pulzaid.com

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.

pulzaid.com · 2023–2025
A borrowed SaaS shell: new logo and headline, template everything else
Before · recreated from the 2023 codebaseA borrowed SaaS shell: new logo and headline, template everything else
pulzaid.com · live
Built, not bought: a wave drawn from trigonometry, Lexend display, glass surfaces
After · Nov 2025 rebuild, live todayBuilt, not bought: a wave drawn from trigonometry, Lexend display, glass surfaces
Exhibit A · what “template debt” looks like

“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.

The old site's template testimonials, still branded TaxPal
Recreated for this page from the first 2023 codebase. Four of the six quotes name “TaxPal”.
2023 · template
1 page

One scrolling homepage. Every audience, one generic pitch.

11 templates, 4 intents
Explainintent
  • Injury care solutions
  • Onsite & nearsite care
  • Medical surveillance

3 service pages, anchor-linked from a mega-menu

Convinceintent
  • Partner with us — TPAs · Captives · Brokers · Agents · Carriers
  • Contact

5 insurance audiences, each with its own economics

Proveintent
  • Intelligent triage
  • Triage playground (live AI demo)

Visitors can try the classifier before a sales call

Careintent
  • 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.

The public demo · Mar 2026

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.

phase 1
Input
Free-text injury description
phase 2
Loading
Staged status · ring + scanline art
phase 3
Result
Type · severity · next step · reasons
catch — any phase
ErrorA designed dead-end-free state: what happened, and a one-tap retry

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”

pulzaid.com / triage-playground
The result state: recommended action leads, alternatives stay visible, reasoning is expandable
Live component · sample scenarioThe result state: recommended action leads, alternatives stay visible, reasoning is expandable
Jul 2026 · the site joins the care loop

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.

Field care wraps up
Technician texts an aftercare guide from the Workbench
Worker opens pulzaid.com/aftercare — 6 topics, EN·ES·中文, no login
pulzaid.com / aftercare
Language first, topic second — the reading order of someone who was just hurt
Live todayLanguage first, topic second — the reading order of someone who was just hurt
02pulzaid.app · employer experience

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.

pulzaid.app / request · 2024
Everything at once: SSN block, facility checklist, equipment logistics — all before care starts
Before · production, 2024Everything at once: SSN block, facility checklist, equipment logistics — all before care starts
pulzaid.app / request · live
Three steps with one job each — demographics, symptoms, review — with the step count visible the whole way
After · Jan 2026 wizard, live todayThree steps with one job each — demographics, symptoms, review — with the step count visible the whole way

Where every field went

978-line component · 28 inputs → 3 steps
Step 1 · Demographics
  • Employee identity & contact
  • Jobsite company + location
  • Approving manager
  • Org-specific fields (PO #, store #)
  • HIPAA acknowledgment gate
Step 2 · Symptoms
  • Emergency check first
  • Symptom onset + description
  • Photo / file attachments
Moved out or cut
  • 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.

Every change

form.watch() streams each keystroke of the wizard into a serializer — no save button, no “are you sure”.

On this device

Values + current step land in injuryRequestV2_facts / _step. Attachments are deliberately stripped — medical files never sit in localStorage.

On return

Reload, tab crash, dropped call — the wizard rehydrates to the same step with the same values. Submit (or an explicit Clear) wipes both keys.

pulzaid.app / detail / case
Four milestones on top, a readable timeline underneath, the AI recommendation at the bottom
Live product · demo case dataFour milestones on top, a readable timeline underneath, the AI recommendation at the bottom
The second half of the loop

“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.

Triage Recommendation
Suggested next step
Imaging Service
Detected injury type
Wrist — suspected sprain / fracture
Imaging requestTelemed requestRead-only guidance · humans take the action

~40 operational events → 4 stable milestones

EventType enum → CaseStatus enum
case_opened
intake_form_finished
01
Case requested
physician_assigned
xray_order_sent
doctor_report_pending_signature
+ two dozen more during care
02
In progress
doctor_report_available
03
Report ready
case_closed
04
Completed

The 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.

03OSHS operations · 2026

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.

Before · mapped in the April workflow review

One injury, three copies of the truth

Reconciled by hand before billing
Caspio
01
Phone intake
FastField
02
Telehealth form
FastField
03
Field first-aid form
DocuWare
04
Manual final filing
×Names and phone numbers drifted between tools
×Telehealth and first-aid forms could not sync
×Processing compared PDFs line by line
The pivotal model decision

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.

Rejected model
A fixed workflow

One prescribed pipeline. Breaks the moment care branches, repeats, or runs in parallel.

Shipped model
A modular Workbench

Seven addable, assignable work packets — three of them repeatable — around one stable case header.

On-Site Health & Safety
PulzAid × OSHS
Case Workbench · recreated for this page, synthetic case data
pulzaid.app / oshs / workbench / case-2843
Carlos MendezAwaiting Review
Send reportCompile documents
Case#2843EmployerBuildWell Inc.JobsiteOakland, CAAssignedMaya R. · EMT
Work packets2 done · 1 in review
Basic Demographic
Complete
Telehealth Triagemulti
Complete
Field First Aidmulti
In review
Additional Services
Draft
X-ray Imaging Requestmulti
TD/Tdap Vaccine Release
Telephonic Follow-Up
+ Add packet
Field First Aid

Maya R. · submitted 10:58 AM · ready for review

#2 of 2
Service time
10:42 AM
Technician
Maya R.
Assessment
Left wrist strain · mild swelling · ice + compression applied
Disposition
Returned to modified duty
Employee signature captured
Request correctionMark complete
Attachments

2 internal · 1 customer-visible

Internal notes

“Confirm PO # before invoicing”

Change requests

Employee phone corrected

Reports

Service record · incident report

Aftercare

Wound care · EN → sent by SMS

1

One stable case header

Identity, employer, jobsite, assignee, and the stage badge stay pinned above every packet.

2

Packets, not pages

Each unit of work has a lifecycle — draft, ready for review, needs correction, complete — and repeatable types can multiply.

3

Coordination beside the form

Notes, change requests, attachments, reports, and aftercare SMS live in a rail — visible, never entangled with clinical fields.

4

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 rollup
Inside — the operations queue
NewNeeds More InfoNeeds AssignmentIn ProgressAwaiting ReviewFollow-up NeededCall AttemptedCase CompleteClosed

The 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?”

Customer case page
Case requested
In progress
Report ready
Completed

Four milestones that only move forward. Internal churn — corrections, reassignments, a second field visit — never shows up as backwards motion.

Shared case link (no login)
New
In progress
Closed

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 2026
Never trust onLine

navigator.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.

Durable queue

Failed submits persist in IndexedDB keyed by a client-op id — the idempotency key that makes replays safe to repeat.

Ordered drain

On reconnect the queue replays in creation order. 401 / 403 / 408 / 429 are kept and retried; poison requests are dropped, not looped.

Honest UI

Drafts mirror to disk 500 ms after each change. The packet shows “saved on this device” — never a fake “submitted”.

Cached, not faked

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.

04The design system

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 repos
pulzaid.com — expressive
mainGreen
#064D52
brand anchor · themeColor
pulza
#084D52
dark teal variant
pulzb
#A9D6D9
light accent
primaryGreen
#048672
action green
stone-100
#F5F5F4
warm neutral ground
pulzaid.app — operational
mainGreen
#084D52shared
text · nav · focus
primaryGreen
#048672shared
primary action fill
secondGreen
#1ABDBD
hover state
thirdGreen
#267980
supporting teal
highlightGreen
#D1FAE5
success · progress bg
lightGray
#F7F7F7
app shell

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 swap
Works for you
Lexend · hero display only — wide, confident, rare
Occupational Health Toolbox
Sora · titles & cards — .text-title 32→44px, .text-subtitle 22→28px
Please fill out the form to submit your injury request; it should take approximately 2 minutes.
Inter · everything that works — 20px marketing body, 14px app labels

Primitives, alive

hover & focus me
Report ReadyAwaiting Review
fadeIn 220ms ease-outfadeInUp 0.6s +16pxshimmer 1.5s ∞radius 4xl = 2rem
From the design doc, verbatim

“PulzAid should feel calm, clinical, fast, and trustworthy.”

Each word had to cash out in an enforceable rule — a feeling you can lint for:

CalmOne accent hue; motion only when state actually changes.
ClinicalColor only ever means state — never decoration.
FastEvery page leads with its next action.
TrustworthyEvery state says what’s actually true.

Voice is a design token too

Proceed with request submission
Submit request
Something went wrong
Please go back and make sure all required fields are complete.
Status: awaiting_review
In review — we’ll update this page automatically.

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.

Triage playground loading state: counter-rotating rings around a pulsing core with staged status lines
Waiting, designed

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 → today
2021
Consumer app era

Poppins, dark cyan + grass green, a friendly bandage mark. Designed for workers’ phones.

Poppins
2023
Template era

New wordmark and teal on a borrowed shell — speed over identity, and it showed.

Template default
2025–26
The system

Lexend / Sora / Inter, the wave drawn from trigonometry, glass surfaces, tokenized state color.

Lexend · Sora · Inter
Live specimen · re-implemented for this page

Anatomy of the brand wave

canvas · rAF · trigonometry
y = A(t) · sin(kx + φᵢ)

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.

hsla(180 ± 15°)

Color is computed, not picked: the hue drifts between emerald and teal per line, per frame — the brand palette expressed as a function.

3 periods / 1440 px

Wavelength is locked to a fixed reference width, so the composition holds from a phone to a 5K display instead of stretching.

Peak-weighted strokes

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.

Density adapts

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.

No asset, just math

~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.

Closing

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.

Reflection

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.