EQ eqctrl

Work · Ethan Ouimet · Portland, OR

Work I've designed and shipped.

I design products and build the systems and agent loops underneath them. My career started at IDEO CoLab out of college in 2017. Open to full-time and contract roles.

Role
Product design, engineering, art direction
When
2026, public beta
Problem
One jar for phone, tablet and desktop.
Outcome
Player notes become shipped fixes.
Built with
TypeScript, Canvas, Node, SQLite, Claude Code
Live
catchalou.eqctrl.io

CatchaLou

A live puzzle game I designed and built, from the layout system to the loop that turns player notes into shipped fixes.

fig. 1.1Desktop, a practice run, as it is live today.
  1. One jar, every screen. Layout is measured from the visible glass, so the jar is the same shape on phone, tablet and desktop. Everything else moves around it.
  2. Thumbs low, eyes high. Aim anywhere, tap to drop. Anything you tap sits in the thumb zone, anything you read sits up top.
  3. Tokens the canvas can read. Color, spacing, radius and type live in CSS and TypeScript, so the canvas HUD and the HTML menus share values.
  4. Every capsule reads without color. Each size has a locked hue and its own motif on Louie.
  5. Figma follows the code. A plugin rebuilds a token-bound mock of every screen from the same values.
  6. The cast. A drop and merge game starring Louie, our cat and the mascot of my wife Emily's streaming community.
style.css against tokens.tsNine rows match exactly. Display size differs on purpose.STYLE.CSSTOKENS.TSROLEAccent--broth-deep#e07a3f · L4COLOR.broth#e07a3f · L27MATCHCard--card#fffaf2 · L9COLOR.card#fffaf2 · L21MATCHCream--cream#fff6e9 · L8COLOR.cream#fff6e9 · L22MATCHCanvas ink--ink-canvas#3a2c20 · L12COLOR.ink#3a2c20 · L23MATCHHairline--linergba(58,44,32,.10) · L30COLOR.linergba(58,44,32,.10) · L26MATCHAlert wash--bad#e05050 · L19COLOR.bad#e05050 · L30MATCHSpacing--s-1 to --s-54 8 12 16 24 · L36SPACE4 8 12 16 24 · L8MATCHRadius--r-sm to --r-pill10 14 20 999 · L51RADIUS10 14 20 999 · L11MATCHType, six sizes--t-h1 to --t-caption30 22 16 14 12 10 · L54-55TYPE30 22 16 14 12 10 · L15MATCHDisplay size--t-display52 · L54TYPE.display34 · L15DIFFERS style.css against tokens.tsNine rows match. Display size differs on purpose.AccentMATCHcss --broth-deep #e07a3f L4ts COLOR.broth #e07a3f L27CardMATCHcss --card #fffaf2 L9ts COLOR.card #fffaf2 L21CreamMATCHcss --cream #fff6e9 L8ts COLOR.cream #fff6e9 L22Canvas inkMATCHcss --ink-canvas #3a2c20 L12ts COLOR.ink #3a2c20 L23HairlineMATCHcss --line rgba(58,44,32,.10) L30ts COLOR.line rgba(58,44,32,.10) L26Alert washMATCHcss --bad #e05050 L19ts COLOR.bad #e05050 L30SpacingMATCHcss --s-1 to --s-5 4 8 12 16 24 L36ts SPACE 4 8 12 16 24 L8RadiusMATCHcss --r-sm to --r-pill 10 14 20 999 L51ts RADIUS 10 14 20 999 L11Type, six sizesMATCHcss --t-h1 to --t-caption 30 22 16 14 12 10 L54-55ts TYPE 30 22 16 14 12 10 L15Display sizeDIFFERScss --t-display 52 L54ts TYPE.display 34 L15
fig. 1.2Tokens, both files.
CatchaLou home on a phone: compact logo, tabs for Daily, Modes, Battle, Ranks and Awards, a Daily Drop card with a Play button, and a live rank strip.
fig. 1.3Home. One card, five tabs.
CatchaLou modes tab on a phone: Speed, Shapes and Practice chips, a one or three minute toggle, and a Play button.
fig. 1.4Modes. Length on a toggle.
fig. 1.5Play. Taps live at the bottom.
The previous CatchaLou home: a large logo, one big Play tile with a length dial, Practice and Speed tiles, then Battle and Leaderboard.
beforeHome in September.
The current CatchaLou home with the smaller lockup, tabbed card and rank strip.
afterHome in October. Smaller lockup, one tabbed card, a live rank strip.

1.AFrom a player's note to a shipped fix

1 · PLAYER 2 · SERVER 3 · AGENT 4 · ME 5 · BOT Sends a note Files an issue Opens a PR Review, release Replies in thread in game or Discord,device attached on GitHub, or linksto an open one own account, branch,tests, pull request I merge and cutthe release tag when logged, andagain when shipped BACK TO THE PLAYER'S THREAD, EVERY TIME a person decides automated 1 · PLAYER 2 · SERVER 3 · AGENT 4 · ME 5 · BOT Sends a note Files an issue Opens a PR Review, release Replies in thread In game or Discord, device attached. On GitHub, or linked to an open one. Own account, branch, tests, pull request. I merge and cut the release tag. When logged, and again when shipped. a person decides automated
fig. 1.6The feedback loop. Agents do the middle, people hold the gates.
  1. One door in, one door out. A note arrives from the game or Discord and the answer lands in the same thread, when it is logged and when the fix ships.
  2. The agent never touches main. Own user, no secrets, a capped API key, a daily run limit and an off switch. I approve every reply before a player sees it.
  3. Only a tag I cut reaches players. Main deploys itself to staging, where a second agent can test in a headless browser and report with screenshots.
Role
Brand, go-to-market, benchmarks, content
When
2026, through its acquihire
Problem
Turn a hippo mark into a working system.
Outcome
Brand kit, design system, benchmarks, campaign.
Company
HipAI, an enterprise AI context layer
Tools
Figma, Linear, Claude, Python

HipAI

Brand system, benchmark program and go-to-market for a context layer that gives AI agents one graph of a company's sources.

The top of the HipAI benchmark one-pager: the hippo lockup, the headline Better answers. Lower cost. Flat at scale., and a three-step strip from any source to one context graph to any agent.
fig. 2.1The benchmark one-pager, top section. An HTML twin, then generated into Figma by plugin.
LinkedIn article cover on navy: Most retrieval scales expensively, HipAI context doesn't, with a chart of tokens spent against corpus size and the hippo on the flat line.
fig. 2.2Article cover for the benchmark campaign.
fig. 2.3The graph visualization spec, built as a living demo.
  1. Codified the mark, then put it everywhere. The hippo existed. I wrote the brand kit around it (canonical purple, navy, Funnel Sans, the naming rule), and built the Figma design system from a machine-readable brief: tokens, a WCAG matrix, 21 components and the plugin scripts that generate them.
  2. Instrumented benchmarks, not estimates. The harness that measured the graph against RAG and agentic doc search on real corpora, per query, in tokens and dollars, and the rule that every question runs through every retrieval surface before a story is chosen.
  3. A ledger for every published number. Each public figure carries a primary source, a second measurement and a sanity check before it ships.
  4. Built the demo company. A synthetic enterprise corpus of about 1,100 files and three million tokens, across agency and client layers, so demos and benchmarks ran on something shaped like a customer.
  5. Wrote the campaign. Threads and LinkedIn articles in a cost-led register, a blog post for gethip.ai, social profile copy, a content calendar, and the one-pager that went to investors.
  6. Drew the product's graph. A design spec for the graph visualization, handed to engineering, and mascot scenes for loading, error and 404 states.
HipAI brand sheet: the hippo lockup in purple and navy on white, reversed on navy, and the icon alone on white and on purple.
fig. 2.4Lockups and icon across the three brand fields.
fig. 2.5The context flywheel, for the GTM deck.
HipAI component sheet on a light grey ground: buttons in five variants, three sizes and four states; status, category and count pills; divider and accent bar; stat cards; cards; pull quotes; logo lockups cropped from the brand sheet; heroes; section headers; carousel slides; and a strip naming ten components not yet built.
fig. 2.621 components: eleven built, ten named. Re-rendered from the plugin's specs.
FOREGROUND ON BACKGROUND, WCAG 2.1 CONTRAST AA 4.5:1 and up, normal textLG 3:1 and up, large text and UIFAIL under 3:1 LIGHT MODE bg #F5F7F9 card #FFFFFF Purple/50 #F5F1FD Purple/100 #EAE3FD fill #8B5CF6 fill #6F39EF panel #152442 panel #0F192F Neutral/Ink #0F192F Neutral/Ink on #F5F7F9 bg: 16.29:1, passes AA for normal textAa16.29AA Neutral/Ink on #FFFFFF card: 17.49:1, passes AA for normal textAa17.49AA Neutral/Ink on Purple/50: 15.73:1, passes AA for normal textAa15.73AA Neutral/Ink on Purple/100: 14.08:1, passes AA for normal textAa14.08AA Neutral/Ink on #8B5CF6 fill: 4.13:1, large text and UI onlyAa4.13LG Neutral/Ink on #6F39EF fill: 2.92:1, failsAa2.92FAIL Neutral/Ink on #152442 panel: 1.14:1, failsAa1.14FAIL Neutral/Ink on #0F192F panel: same color, not applicable Neutral/Heading #152442 Neutral/Heading on #F5F7F9 bg: 14.35:1, passes AA for normal textAa14.35AA Neutral/Heading on #FFFFFF card: 15.41:1, passes AA for normal textAa15.41AA Neutral/Heading on Purple/50: 13.85:1, passes AA for normal textAa13.85AA Neutral/Heading on Purple/100: 12.40:1, passes AA for normal textAa12.40AA Neutral/Heading on #8B5CF6 fill: 3.64:1, large text and UI onlyAa3.64LG Neutral/Heading on #6F39EF fill: 2.57:1, failsAa2.57FAIL Neutral/Heading on #152442 panel: same color, not applicable Neutral/Heading on #0F192F panel: 1.14:1, failsAa1.14FAIL Neutral/Ink-2 #2C477D Neutral/Ink-2 on #F5F7F9 bg: 8.49:1, passes AA for normal textAa8.49AA Neutral/Ink-2 on #FFFFFF card: 9.11:1, passes AA for normal textAa9.11AA Neutral/Ink-2 on Purple/50: 8.19:1, passes AA for normal textAa8.19AA Neutral/Ink-2 on Purple/100: 7.34:1, passes AA for normal textAa7.34AA Neutral/Ink-2 on #8B5CF6 fill: 2.15:1, failsAa2.15FAIL Neutral/Ink-2 on #6F39EF fill: 1.52:1, failsAa1.52FAIL Neutral/Ink-2 on #152442 panel: 1.69:1, failsAa1.69FAIL Neutral/Ink-2 on #0F192F panel: 1.92:1, failsAa1.92FAIL Neutral/Ink-3 #708BC2 Neutral/Ink-3 on #F5F7F9 bg: 3.17:1, large text and UI onlyAa3.17LG Neutral/Ink-3 on #FFFFFF card: 3.41:1, large text and UI onlyAa3.41LG Neutral/Ink-3 on Purple/50: 3.06:1, large text and UI onlyAa3.06LG Neutral/Ink-3 on Purple/100: 2.74:1, failsAa2.74FAIL Neutral/Ink-3 on #8B5CF6 fill: 1.24:1, failsAa1.24FAIL Neutral/Ink-3 on #6F39EF fill: 1.76:1, failsAa1.76FAIL Neutral/Ink-3 on #152442 panel: 4.52:1, passes AA for normal textAa4.52AA Neutral/Ink-3 on #0F192F panel: 5.13:1, passes AA for normal textAa5.13AA Brand/Text #541BDA Brand/Text on #F5F7F9 bg: 7.68:1, passes AA for normal textAa7.68AA Brand/Text on #FFFFFF card: 8.25:1, passes AA for normal textAa8.25AA Brand/Text on Purple/50: 7.42:1, passes AA for normal textAa7.42AA Brand/Text on Purple/100: 6.64:1, passes AA for normal textAa6.64AA Brand/Text on #8B5CF6 fill: 1.95:1, failsAa1.95FAIL Brand/Text on #6F39EF fill: 1.38:1, failsAa1.38FAIL Brand/Text on #152442 panel: 1.87:1, failsAa1.87FAIL Brand/Text on #0F192F panel: 2.12:1, failsAa2.12FAIL Brand/Primary-Hover #6F39EF Brand/Primary-Hover on #F5F7F9 bg: 5.58:1, passes AA for normal textAa5.58AA Brand/Primary-Hover on #FFFFFF card: 5.99:1, passes AA for normal textAa5.99AA Brand/Primary-Hover on Purple/50: 5.38:1, passes AA for normal textAa5.38AA Brand/Primary-Hover on Purple/100: 4.82:1, passes AA for normal textAa4.82AA Brand/Primary-Hover on #8B5CF6 fill: 1.41:1, failsAa1.41FAIL Brand/Primary-Hover on #6F39EF fill: same color, not applicable Brand/Primary-Hover on #152442 panel: 2.57:1, failsAa2.57FAIL Brand/Primary-Hover on #0F192F panel: 2.92:1, failsAa2.92FAIL Brand/Primary #8B5CF6 Brand/Primary on #F5F7F9 bg: 3.94:1, large text and UI onlyAa3.94LG Brand/Primary on #FFFFFF card: 4.23:1, large text and UI onlyAa4.23LG Brand/Primary on Purple/50: 3.81:1, large text and UI onlyAa3.81LG Brand/Primary on Purple/100: 3.41:1, large text and UI onlyAa3.41LG Brand/Primary on #8B5CF6 fill: same color, not applicable Brand/Primary on #6F39EF fill: 1.41:1, failsAa1.41FAIL Brand/Primary on #152442 panel: 3.64:1, large text and UI onlyAa3.64LG Brand/Primary on #0F192F panel: 4.13:1, large text and UI onlyAa4.13LG Status/Success #1DAF5A Status/Success on #F5F7F9 bg: 2.67:1, failsAa2.67FAIL Status/Success on #FFFFFF card: 2.86:1, failsAa2.86FAIL Status/Success on Purple/50: 2.58:1, failsAa2.58FAIL Status/Success on Purple/100: 2.31:1, failsAa2.31FAIL Status/Success on #8B5CF6 fill: 1.48:1, failsAa1.48FAIL Status/Success on #6F39EF fill: 2.09:1, failsAa2.09FAIL Status/Success on #152442 panel: 5.38:1, passes AA for normal textAa5.38AA Status/Success on #0F192F panel: 6.11:1, passes AA for normal textAa6.11AA Status/Warning #F49D25 Status/Warning on #F5F7F9 bg: 2.02:1, failsAa2.02FAIL Status/Warning on #FFFFFF card: 2.17:1, failsAa2.17FAIL Status/Warning on Purple/50: 1.95:1, failsAa1.95FAIL Status/Warning on Purple/100: 1.74:1, failsAa1.74FAIL Status/Warning on #8B5CF6 fill: 1.96:1, failsAa1.96FAIL Status/Warning on #6F39EF fill: 2.76:1, failsAa2.76FAIL Status/Warning on #152442 panel: 7.11:1, passes AA for normal textAa7.11AA Status/Warning on #0F192F panel: 8.08:1, passes AA for normal textAa8.08AA Status/Danger #EE2B2B Status/Danger on #F5F7F9 bg: 3.90:1, large text and UI onlyAa3.90LG Status/Danger on #FFFFFF card: 4.19:1, large text and UI onlyAa4.19LG Status/Danger on Purple/50: 3.76:1, large text and UI onlyAa3.76LG Status/Danger on Purple/100: 3.37:1, large text and UI onlyAa3.37LG Status/Danger on #8B5CF6 fill: 1.01:1, failsAa1.01FAIL Status/Danger on #6F39EF fill: 1.43:1, failsAa1.43FAIL Status/Danger on #152442 panel: 3.68:1, large text and UI onlyAa3.68LG Status/Danger on #0F192F panel: 4.18:1, large text and UI onlyAa4.18LG Status/Info #308CE8 Status/Info on #F5F7F9 bg: 3.24:1, large text and UI onlyAa3.24LG Status/Info on #FFFFFF card: 3.48:1, large text and UI onlyAa3.48LG Status/Info on Purple/50: 3.13:1, large text and UI onlyAa3.13LG Status/Info on Purple/100: 2.80:1, failsAa2.80FAIL Status/Info on #8B5CF6 fill: 1.22:1, failsAa1.22FAIL Status/Info on #6F39EF fill: 1.72:1, failsAa1.72FAIL Status/Info on #152442 panel: 4.43:1, large text and UI onlyAa4.43LG Status/Info on #0F192F panel: 5.03:1, passes AA for normal textAa5.03AA White #FFFFFF White on #F5F7F9 bg: 1.07:1, failsAa1.07FAIL White on #FFFFFF card: same color, not applicable White on Purple/50: 1.11:1, failsAa1.11FAIL White on Purple/100: 1.24:1, failsAa1.24FAIL White on #8B5CF6 fill: 4.23:1, large text and UI onlyAa4.23LG White on #6F39EF fill: 5.99:1, passes AA for normal textAa5.99AA White on #152442 panel: 15.41:1, passes AA for normal textAa15.41AA White on #0F192F panel: 17.49:1, passes AA for normal textAa17.49AA DARK MODE bg #090F1B card #152442 border-surf #1F335C fill #976EF7 Neutral/Heading #FFFFFF Neutral/Heading on #090F1B bg: 19.16:1, passes AA for normal textAa19.16AA Neutral/Heading on #152442 card: 15.41:1, passes AA for normal textAa15.41AA Neutral/Heading on #1F335C border-surf: 12.45:1, passes AA for normal textAa12.45AA Neutral/Heading on #976EF7 fill: 3.57:1, large text and UI onlyAa3.57LG Neutral/Ink #F8FAFC Neutral/Ink on #090F1B bg: 18.32:1, passes AA for normal textAa18.32AA Neutral/Ink on #152442 card: 14.73:1, passes AA for normal textAa14.73AA Neutral/Ink on #1F335C border-surf: 11.90:1, passes AA for normal textAa11.90AA Neutral/Ink on #976EF7 fill: 3.41:1, large text and UI onlyAa3.41LG Neutral/Ink-2 #CBD5E1 Neutral/Ink-2 on #090F1B bg: 12.91:1, passes AA for normal textAa12.91AA Neutral/Ink-2 on #152442 card: 10.38:1, passes AA for normal textAa10.38AA Neutral/Ink-2 on #1F335C border-surf: 8.39:1, passes AA for normal textAa8.39AA Neutral/Ink-2 on #976EF7 fill: 2.40:1, failsAa2.40FAIL Neutral/Ink-3 #94A3B8 Neutral/Ink-3 on #090F1B bg: 7.47:1, passes AA for normal textAa7.47AA Neutral/Ink-3 on #152442 card: 6.01:1, passes AA for normal textAa6.01AA Neutral/Ink-3 on #1F335C border-surf: 4.86:1, passes AA for normal textAa4.86AA Neutral/Ink-3 on #976EF7 fill: 1.39:1, failsAa1.39FAIL Brand/Text #B394F9 Brand/Text on #090F1B bg: 7.78:1, passes AA for normal textAa7.78AA Brand/Text on #152442 card: 6.25:1, passes AA for normal textAa6.25AA Brand/Text on #1F335C border-surf: 5.05:1, passes AA for normal textAa5.05AA Brand/Text on #976EF7 fill: 1.45:1, failsAa1.45FAIL Brand/Primary #976EF7 Brand/Primary on #090F1B bg: 5.37:1, passes AA for normal textAa5.37AA Brand/Primary on #152442 card: 4.32:1, large text and UI onlyAa4.32LG Brand/Primary on #1F335C border-surf: 3.49:1, large text and UI onlyAa3.49LG Brand/Primary on #976EF7 fill: same color, not applicable Status/Success #34D399 Status/Success on #090F1B bg: 9.97:1, passes AA for normal textAa9.97AA Status/Success on #152442 card: 8.02:1, passes AA for normal textAa8.02AA Status/Success on #1F335C border-surf: 6.48:1, passes AA for normal textAa6.48AA Status/Success on #976EF7 fill: 1.85:1, failsAa1.85FAIL Status/Warning #FBBF24 Status/Warning on #090F1B bg: 11.48:1, passes AA for normal textAa11.48AA Status/Warning on #152442 card: 9.23:1, passes AA for normal textAa9.23AA Status/Warning on #1F335C border-surf: 7.46:1, passes AA for normal textAa7.46AA Status/Warning on #976EF7 fill: 2.14:1, failsAa2.14FAIL Status/Danger #F87171 Status/Danger on #090F1B bg: 6.93:1, passes AA for normal textAa6.93AA Status/Danger on #152442 card: 5.57:1, passes AA for normal textAa5.57AA Status/Danger on #1F335C border-surf: 4.50:1, passes AA for normal textAa4.50AA Status/Danger on #976EF7 fill: 1.29:1, failsAa1.29FAIL Status/Info #60A5FA Status/Info on #090F1B bg: 7.54:1, passes AA for normal textAa7.54AA Status/Info on #152442 card: 6.06:1, passes AA for normal textAa6.06AA Status/Info on #1F335C border-surf: 4.90:1, passes AA for normal textAa4.90AA Status/Info on #976EF7 fill: 1.40:1, failsAa1.40FAIL Neutral/On-Primary #0F172A Neutral/On-Primary on #090F1B bg: 1.07:1, failsAa1.07FAIL Neutral/On-Primary on #152442 card: 1.16:1, failsAa1.16FAIL Neutral/On-Primary on #1F335C border-surf: 1.43:1, failsAa1.43FAIL Neutral/On-Primary on #976EF7 fill: 5.01:1, passes AA for normal textAa5.01AA CONTRAST, FOREGROUND ON BACKGROUND AA 4.5:1+LG 3:1+ large, UIFAIL under 3 LIGHT MODE #F5F7F9 #FFFFFF #F5F1FD #EAE3FD #8B5CF6 #6F39EF #152442 #0F192F Ink Neutral/Ink on #F5F7F9 bg: 16.29:1, passes AA for normal text16.3 Neutral/Ink on #FFFFFF card: 17.49:1, passes AA for normal text17.5 Neutral/Ink on Purple/50: 15.73:1, passes AA for normal text15.7 Neutral/Ink on Purple/100: 14.08:1, passes AA for normal text14.1 Neutral/Ink on #8B5CF6 fill: 4.13:1, large text and UI only4.1 Neutral/Ink on #6F39EF fill: 2.92:1, fails2.9 Neutral/Ink on #152442 panel: 1.14:1, fails1.1 Neutral/Ink on #0F192F panel: same color, not applicable Heading Neutral/Heading on #F5F7F9 bg: 14.35:1, passes AA for normal text14.3 Neutral/Heading on #FFFFFF card: 15.41:1, passes AA for normal text15.4 Neutral/Heading on Purple/50: 13.85:1, passes AA for normal text13.8 Neutral/Heading on Purple/100: 12.40:1, passes AA for normal text12.4 Neutral/Heading on #8B5CF6 fill: 3.64:1, large text and UI only3.6 Neutral/Heading on #6F39EF fill: 2.57:1, fails2.6 Neutral/Heading on #152442 panel: same color, not applicable Neutral/Heading on #0F192F panel: 1.14:1, fails1.1 Ink-2 Neutral/Ink-2 on #F5F7F9 bg: 8.49:1, passes AA for normal text8.5 Neutral/Ink-2 on #FFFFFF card: 9.11:1, passes AA for normal text9.1 Neutral/Ink-2 on Purple/50: 8.19:1, passes AA for normal text8.2 Neutral/Ink-2 on Purple/100: 7.34:1, passes AA for normal text7.3 Neutral/Ink-2 on #8B5CF6 fill: 2.15:1, fails2.1 Neutral/Ink-2 on #6F39EF fill: 1.52:1, fails1.5 Neutral/Ink-2 on #152442 panel: 1.69:1, fails1.7 Neutral/Ink-2 on #0F192F panel: 1.92:1, fails1.9 Ink-3 Neutral/Ink-3 on #F5F7F9 bg: 3.17:1, large text and UI only3.2 Neutral/Ink-3 on #FFFFFF card: 3.41:1, large text and UI only3.4 Neutral/Ink-3 on Purple/50: 3.06:1, large text and UI only3.1 Neutral/Ink-3 on Purple/100: 2.74:1, fails2.7 Neutral/Ink-3 on #8B5CF6 fill: 1.24:1, fails1.2 Neutral/Ink-3 on #6F39EF fill: 1.76:1, fails1.8 Neutral/Ink-3 on #152442 panel: 4.52:1, passes AA for normal text4.5 Neutral/Ink-3 on #0F192F panel: 5.13:1, passes AA for normal text5.1 Text Brand/Text on #F5F7F9 bg: 7.68:1, passes AA for normal text7.7 Brand/Text on #FFFFFF card: 8.25:1, passes AA for normal text8.2 Brand/Text on Purple/50: 7.42:1, passes AA for normal text7.4 Brand/Text on Purple/100: 6.64:1, passes AA for normal text6.6 Brand/Text on #8B5CF6 fill: 1.95:1, fails1.9 Brand/Text on #6F39EF fill: 1.38:1, fails1.4 Brand/Text on #152442 panel: 1.87:1, fails1.9 Brand/Text on #0F192F panel: 2.12:1, fails2.1 Pri. hover Brand/Primary-Hover on #F5F7F9 bg: 5.58:1, passes AA for normal text5.6 Brand/Primary-Hover on #FFFFFF card: 5.99:1, passes AA for normal text6.0 Brand/Primary-Hover on Purple/50: 5.38:1, passes AA for normal text5.4 Brand/Primary-Hover on Purple/100: 4.82:1, passes AA for normal text4.8 Brand/Primary-Hover on #8B5CF6 fill: 1.41:1, fails1.4 Brand/Primary-Hover on #6F39EF fill: same color, not applicable Brand/Primary-Hover on #152442 panel: 2.57:1, fails2.6 Brand/Primary-Hover on #0F192F panel: 2.92:1, fails2.9 Primary Brand/Primary on #F5F7F9 bg: 3.94:1, large text and UI only3.9 Brand/Primary on #FFFFFF card: 4.23:1, large text and UI only4.2 Brand/Primary on Purple/50: 3.81:1, large text and UI only3.8 Brand/Primary on Purple/100: 3.41:1, large text and UI only3.4 Brand/Primary on #8B5CF6 fill: same color, not applicable Brand/Primary on #6F39EF fill: 1.41:1, fails1.4 Brand/Primary on #152442 panel: 3.64:1, large text and UI only3.6 Brand/Primary on #0F192F panel: 4.13:1, large text and UI only4.1 Success Status/Success on #F5F7F9 bg: 2.67:1, fails2.7 Status/Success on #FFFFFF card: 2.86:1, fails2.9 Status/Success on Purple/50: 2.58:1, fails2.6 Status/Success on Purple/100: 2.31:1, fails2.3 Status/Success on #8B5CF6 fill: 1.48:1, fails1.5 Status/Success on #6F39EF fill: 2.09:1, fails2.1 Status/Success on #152442 panel: 5.38:1, passes AA for normal text5.4 Status/Success on #0F192F panel: 6.11:1, passes AA for normal text6.1 Warning Status/Warning on #F5F7F9 bg: 2.02:1, fails2.0 Status/Warning on #FFFFFF card: 2.17:1, fails2.2 Status/Warning on Purple/50: 1.95:1, fails1.9 Status/Warning on Purple/100: 1.74:1, fails1.7 Status/Warning on #8B5CF6 fill: 1.96:1, fails2.0 Status/Warning on #6F39EF fill: 2.76:1, fails2.8 Status/Warning on #152442 panel: 7.11:1, passes AA for normal text7.1 Status/Warning on #0F192F panel: 8.08:1, passes AA for normal text8.1 Danger Status/Danger on #F5F7F9 bg: 3.90:1, large text and UI only3.9 Status/Danger on #FFFFFF card: 4.19:1, large text and UI only4.2 Status/Danger on Purple/50: 3.76:1, large text and UI only3.8 Status/Danger on Purple/100: 3.37:1, large text and UI only3.4 Status/Danger on #8B5CF6 fill: 1.01:1, fails1.0 Status/Danger on #6F39EF fill: 1.43:1, fails1.4 Status/Danger on #152442 panel: 3.68:1, large text and UI only3.7 Status/Danger on #0F192F panel: 4.18:1, large text and UI only4.2 Info Status/Info on #F5F7F9 bg: 3.24:1, large text and UI only3.2 Status/Info on #FFFFFF card: 3.48:1, large text and UI only3.5 Status/Info on Purple/50: 3.13:1, large text and UI only3.1 Status/Info on Purple/100: 2.80:1, fails2.8 Status/Info on #8B5CF6 fill: 1.22:1, fails1.2 Status/Info on #6F39EF fill: 1.72:1, fails1.7 Status/Info on #152442 panel: 4.43:1, large text and UI only4.4 Status/Info on #0F192F panel: 5.03:1, passes AA for normal text5.0 White White on #F5F7F9 bg: 1.07:1, fails1.1 White on #FFFFFF card: same color, not applicable White on Purple/50: 1.11:1, fails1.1 White on Purple/100: 1.24:1, fails1.2 White on #8B5CF6 fill: 4.23:1, large text and UI only4.2 White on #6F39EF fill: 5.99:1, passes AA for normal text6.0 White on #152442 panel: 15.41:1, passes AA for normal text15.4 White on #0F192F panel: 17.49:1, passes AA for normal text17.5 DARK MODE #090F1B #152442 #1F335C #976EF7 Heading Neutral/Heading on #090F1B bg: 19.16:1, passes AA for normal text19.2 Neutral/Heading on #152442 card: 15.41:1, passes AA for normal text15.4 Neutral/Heading on #1F335C border-surf: 12.45:1, passes AA for normal text12.4 Neutral/Heading on #976EF7 fill: 3.57:1, large text and UI only3.6 Ink Neutral/Ink on #090F1B bg: 18.32:1, passes AA for normal text18.3 Neutral/Ink on #152442 card: 14.73:1, passes AA for normal text14.7 Neutral/Ink on #1F335C border-surf: 11.90:1, passes AA for normal text11.9 Neutral/Ink on #976EF7 fill: 3.41:1, large text and UI only3.4 Ink-2 Neutral/Ink-2 on #090F1B bg: 12.91:1, passes AA for normal text12.9 Neutral/Ink-2 on #152442 card: 10.38:1, passes AA for normal text10.4 Neutral/Ink-2 on #1F335C border-surf: 8.39:1, passes AA for normal text8.4 Neutral/Ink-2 on #976EF7 fill: 2.40:1, fails2.4 Ink-3 Neutral/Ink-3 on #090F1B bg: 7.47:1, passes AA for normal text7.5 Neutral/Ink-3 on #152442 card: 6.01:1, passes AA for normal text6.0 Neutral/Ink-3 on #1F335C border-surf: 4.86:1, passes AA for normal text4.9 Neutral/Ink-3 on #976EF7 fill: 1.39:1, fails1.4 Text Brand/Text on #090F1B bg: 7.78:1, passes AA for normal text7.8 Brand/Text on #152442 card: 6.25:1, passes AA for normal text6.2 Brand/Text on #1F335C border-surf: 5.05:1, passes AA for normal text5.0 Brand/Text on #976EF7 fill: 1.45:1, fails1.4 Primary Brand/Primary on #090F1B bg: 5.37:1, passes AA for normal text5.4 Brand/Primary on #152442 card: 4.32:1, large text and UI only4.3 Brand/Primary on #1F335C border-surf: 3.49:1, large text and UI only3.5 Brand/Primary on #976EF7 fill: same color, not applicable Success Status/Success on #090F1B bg: 9.97:1, passes AA for normal text10.0 Status/Success on #152442 card: 8.02:1, passes AA for normal text8.0 Status/Success on #1F335C border-surf: 6.48:1, passes AA for normal text6.5 Status/Success on #976EF7 fill: 1.85:1, fails1.9 Warning Status/Warning on #090F1B bg: 11.48:1, passes AA for normal text11.5 Status/Warning on #152442 card: 9.23:1, passes AA for normal text9.2 Status/Warning on #1F335C border-surf: 7.46:1, passes AA for normal text7.5 Status/Warning on #976EF7 fill: 2.14:1, fails2.1 Danger Status/Danger on #090F1B bg: 6.93:1, passes AA for normal text6.9 Status/Danger on #152442 card: 5.57:1, passes AA for normal text5.6 Status/Danger on #1F335C border-surf: 4.50:1, passes AA for normal text4.5 Status/Danger on #976EF7 fill: 1.29:1, fails1.3 Info Status/Info on #090F1B bg: 7.54:1, passes AA for normal text7.5 Status/Info on #152442 card: 6.06:1, passes AA for normal text6.1 Status/Info on #1F335C border-surf: 4.90:1, passes AA for normal text4.9 Status/Info on #976EF7 fill: 1.40:1, fails1.4 On-Primary Neutral/On-Primary on #090F1B bg: 1.07:1, fails1.1 Neutral/On-Primary on #152442 card: 1.16:1, fails1.2 Neutral/On-Primary on #1F335C border-surf: 1.43:1, fails1.4 Neutral/On-Primary on #976EF7 fill: 5.01:1, passes AA for normal text5.0
fig. 2.7Palette contrast pairs, light and dark.
fig. 2.8Loading.
fig. 2.9Error.
fig. 2.10404.
Role
Product designer
When
2020 to 2022
Problem
Fixed stands cannot adjust to each artist.
Outcome
Granted patents on the stand and its mechanism.
Company
Xencelabs, by Hanvon Ugee
Patents
US12173837B2, USD992936S1, USD1025996S1, USD1026898S1
More
ethanouimet.com

Xencelabs

My core challenge was a pro tier drawing display with a stand engineered for professional artist needs other products do not meet.

The Xencelabs pen display on a stand, showing an orange rose, with the quick keys remote and pen case beside it.
fig. 3.1Pen display, quick keys remote and pen case.
  1. Adjustable where fixed stands are not. The mechanism slides and rotates under force, then holds.
  2. Sole named inventor. A granted US patent for the support mechanism and design patents for the display stand, an adjustable stand and the drawing tablet, assigned to Hanvon Ugee.
  3. Sketch to mass production. Sketches and scale models, engineering with the ME, EE and driver teams, then the factory.
  4. iF Design Award 2024. The Pen Display 16, credited with Volker Huebner of dingfest | design.
CAD exploded view: a pen display above its mount plate, with the stand base below and dashed lines showing how the parts join.
fig. 3.2Stand, exploded.
A white 1:1 scale model of a display stand on a desk, propping up a large black display from behind.
fig. 3.31:1 model of the display stand.
CAD render of a pen display on a folding stand, shown lying low and standing upright, with the quick keys remote at its edge.
fig. 3.4Folding stand, two positions.
Pages of pen and pencil planning sketches for a pen display: layout options, button placements, port and cable notes.
fig. 3.5Planning sketches.
Role
Product Marketing Lead
When
2025 to 2026
Company
Yupp, a consumer AI evaluation platform
Recap
yupp.ai

Yupp Cash Out

Go-to-market for the feature that paid people real money for rating AI answers.

  1. The problem was trust. An app that says it will pay you sounds like a scam. The story was fairness: you help make AI better and get paid for your part. The story never explained the rewards math.
  2. What I owned. Positioning, launch content, social and community, and the AI-assisted content workflows that let a small contractor team keep up.
  3. What moved. Registered users went from 250K to over 1M in the three months of the push, from 200+ countries.
  4. After. Yupp wound down in April 2026, and I'm grateful to the community that showed up for it.
The top of Yupp's recap page: The Yupp Story, June 2024 to April 2026, then By the Numbers with 1M+ users registered, 90K community members, 200+ countries, 900+ AI models, 37M prompts and 30M+ preference datapoints.
fig. 4.1Yupp's own recap, with the numbers. Click to open.
Role
Product designer, fellow
When
Summer 2017
With
Ford, Rob Sieg, Gloria Chua
More
ethanouimet.com

IDEO CoLab

Blokmotiv: an identity for every airbag part, so a counterfeit cannot get into a car.

The Blokmotiv demo table: a monitor showing a verified installation, printed stakeholder dashboards, white 3D printed airbag modules and a cardboard steering wheel with a scanner.
fig. 5.1The demo station.
A hand placing a white airbag module onto a scanner set into a cardboard steering wheel.
fig. 5.2The wheel checks a part before it is accepted.
  1. The problem. Counterfeit airbags are built from real parts taken from different cars, and can be worse than no airbag at all.
  2. The system. Every part gets an identity, the assembled airbag gets one more, and every step lands on a ledger that manufacturers, mechanics and owners can read.
  3. The demo was physical. Scan three parts, assemble, install, and watch the wheel accept a real airbag and reject a fake.
Role
Product design, architecture, brand
When
2026, two instances live
Problem
Single-purpose bots; AI text can misfire live.
Outcome
One shared core, safety gates on.
Surfaces
Discord, Twitch, YouTube
Name
Cohost is a working title

Cohost

One AI co-host per streamer, in place of a stack of single-purpose bots.

COHOST CORE · ONE CODEBASE Capture, mirror Recap after Operator chat Feed, roles events, chat, donations from the record, not live remembers the community donor roles, charity feed GATES, ON BY DEFAULT model text to operator channels only · known names or refuse · no mass pings · spend cap · off switch DEPLOYED PER STREAMER, OWN NAME AND AVATAR LouieBot HuzBot instance 1 · Emily's community the original, where the rules were proven instance 2 · a month-long charity streamathon Twitch first, mirrored to YouTube and Discord DISCORD TWITCH TWITCH YOUTUBE DISCORD EVENTS IN, POSTS OUT. ON EVENT DAY THE AI ONLY LISTENS. COHOST CORE · ONE CODEBASE Capture, mirror Recap after Operator chat Feed, roles events, chat, donations from the record, not live remembers the community donor roles, charity feed GATES, ON BY DEFAULT operator-only AI text · known names · no mass pings PER STREAMER, OWN NAME LouieBot HuzBot instance 1 Emily's community, where the rules were proven instance 2 a charity streamathon, Twitch first, mirrored to YouTube and Discord DISCORD TWITCH TWITCH YOUTUBE DISCORD EVENTS IN, POSTS OUT. ON EVENT DAY THE AI ONLY LISTENS.
fig. 6.1How Cohost is laid out. LouieBot came first; HuzBot is the second streamer on the same core.
  1. No model writes on event day. The bot captures and mirrors. The AI works from the record afterward, when a mistake costs nothing.
  2. Safety ships as defaults. A streamer can change the tone. The gates stay on.
  3. Discoverable, not configurable. Most streamers never open a dashboard after week one, so the product shows what can be tuned and sets its own defaults.
  4. Setup is a conversation. Intake chat, invite, a shadow launch, then live, with the streamer's own name and avatar on the bot.
A Discord operator channel with sample data. An operator named Ada asks Cohost who was most active in chat and whether the new emote was used. Cohost answers with fictional viewer names and emote counts, then a green high confidence line. A second question about TikTok gets a plain admission that there are no TikTok numbers, with a red low confidence line.
fig. 6.2Operator chat reply. Sample data.
A Discord card with a blue left bar titled Stream recap: 2h 14m live, with sample data. Fields show viewers, chat lines, most active chatters, top emotes, new followers, new subs, resub shout-outs, gifted subs and bits, all with fictional names.
fig. 6.3After-stream recap card. Sample data.
Mark sheet ten: twelve app-tile marks of a headset with a boom mic, each with a small-size pair, including a gear ring and a status row.
fig. 6.4Mark sheet 10 of 10. The name is still open, so the mark works without one: a headset in a gear ring, readable at 28 px.
Role
Design, build, writing
When
2026, ongoing
Built with
Hand-written HTML, Canvas, Remotion
Live
eqctrl.io

eqctrl.io and Clawdius

This site: a landing page that grows, a block-sprite crab, and free guides and tools.

fig. 7.1Home on load. The background is a space-colonization canvas that grows fresh every time.
  1. No framework, one file per page. Hand-written HTML with a light and dark theme, built with Claude as a pair.
  2. Clawdius. A block-sprite crab with his own design system: poses, reaction stickers, a 404 scene and a four-season set, animated with Remotion.
  3. Karpathy+. A free persistent-memory template for Claude, documented and forkable on GitHub.
  4. Guides that do work. The Claude guide is the on-ramp I hand every new client. The agents page shows a small fleet with a human gate on everything that ships.
The Clawdius character page: Meet Clawdius, a pixel crab in rust orange, with a row of pose variants below.
fig. 7.2The character page.
The Karpathy+ page: persistent memory for your AI, with a step diagram and a GitHub button.
fig. 7.3Karpathy+, the memory template.
Role
Custom Fabrication Assistant
When
2017 to 2018
Company
Satis&fy
More
Samsung, Nike, Dyson

Satis&fy: Samsung, Nike, Dyson

Fabrication for Samsung's Galaxy S8 launch, Nike's House of Go and Dyson's first North American boutiques.

A row of tall infinity-mirror columns, each framing a phone in nested glowing outlines that appear to recede forever.
fig. 8.1Samsung, lit. Photo: Dorothy Hong, Jordan Kleinman.
The installation on the event floor, with magenta light above and the mirrored columns running back into the room.
fig. 8.2Samsung, floor. Same credit.
  1. The role. Supporting the fabrication manager: requests, research, sourcing, pricing, vendors, prototyping, digital fabrication, finish carpentry.
  2. The builds. Samsung infinity mirrors and layered rear-projection screens, Nike's pop-up, Dyson boutiques.
A visitor walking a lit path toward a magenta and blue screen wall, with illustrated figures on the side walls: the Nike House of Go pop-up.
fig. 8.3Nike, Chicago. Photo: Tim Jarosz.
A Dyson boutique at night: a dark storefront with a large screen above the entrance and product displays inside.
fig. 8.4Dyson boutique.

Contact

Building something?

Let me know if I can help support the next thing you're designing. I'm open to full-time and contract roles, and happy to walk through any of these.

entropyeq@gmail.com LinkedIn Industrial design portfolio