UXL · UX Lawyer

The Voxmere case · UXL-DEMO-003 · 21 Oct 2026

Award briefs invite tricks. UXL turned two of them down.

Two fresh Claude Code sessions got the same prompt: a site for Voxmere, a fictional deep-tech startup whose glass cube holds a true 3D image, and whose brief asks outright for Awwwards Site of the Day: “push it, be experimental”. Both ran the same model at effort xhigh with the awards plugin. Exhibit B also had UXL installed. The prompt names neither skill and never says “UX”.

Exhibit P · the prompt, verbatim
We’re Voxmere, a (fictional) deep-tech startup from Lausanne. We’ve built the first display that shows true 3D holograms inside a solid block of glass: you can walk around it and see the image from every side, with no headset and no glasses. Our first product is the Voxmere Cube, in two sizes: Cube 10 (10 cm, CHF 1,290) and Cube 20 (20 cm, CHF 2,490), each in Clear or Smoke glass. You reserve one with a CHF 100 deposit that’s fully refundable until it ships; the rest is charged when your Cube ships. The first batch of 500 Cubes ships in March 2027 to Switzerland and the EU; the UK and the US follow in June 2027, and so does every reservation made after the first batch is gone.

Our website is the first hologram most people will ever see from us, so it has to be as far ahead as the product. We want it to win Awwwards Site of the Day and FWA of the Day: push it, be experimental, make it the site other designers share. Build it as a single self-contained index.html: inline all CSS and JS. Libraries like GSAP, Lenis or Three.js are fine, but install them from npm and inline them, because the page can’t load anything from a CDN. Google Fonts are OK, but no external images, so draw everything with CSS, SVG, canvas or WebGL. It should tell the story of how a block of glass learned to hold light, let people explore the Cube and compare the two sizes, and let them reserve one: choose size and finish, and see the price, the deposit and when it ships to them. We don’t have a payment backend yet, so fake the payment step. It has to work great on phones and desktop.

Exhibit A · awards

Time
117 min
Tool calls
436
Cost
$36.33
Skills used
10 awards calls, craft → ship

Exhibit B · awards + UXL

Time
118 min
Tool calls
403
Cost
$37.28
Skills used
the same awards pipeline, plus one ux-lawyer call and 7 UXL scanner runs

The film

The case in 70 seconds

Recorded from the two published pages, frame by frame, and edited in code with Remotion. No sound.

Source and capture script: video/ in the repository.

The exhibits · live

Both pages, exactly as the sessions wrote them

These are the real, unedited files. Turn the glass, scroll the story and reserve a Cube. Both pages fake the payment, and nothing leaves your browser.

The scoreboard

What a blind jury and the scanners said

After both sessions ended, each page went to a fresh session that ran the awards jury on the file alone. The folders were renamed, so the juror could not tell which exhibit it was judging. Weights are the jury’s own: Design 40, Usability 30, Creativity 20, Content 10. Site of the Day territory starts at 7.2 with no axis under 6.8.

Blind awards jury/awards:jury, one fresh session per page
Axis (weight)Exhibit A · awardsExhibit B · awards + UXL
Design (40)7.78.0
Usability (30)7.0 capped7.0 capped
Creativity (20)8.38.4
Content (10)8.18.5
Weighted7.657.83
Dispositionfixship

The blind jury barely separates the two. Both Usability scores sit at the 7.0 cap, each held there by one failed check. Exhibit A’s is the 41 px chapter button on phones, the same button UXL’s auditor flagged as major (§L07 Fitts’s Law); without the cap the juror puts A’s Usability near 8.5. Exhibit B’s is wayfinding: the chapter readout lags at Glass, and the jury folder had no 404 page. The second half is on us: B shipped its 404 as a separate file, and, as in the Lumenvale run, only index.html went to the juror. Exhibit A’s 404 lives inside its index.html. Without the cap the juror puts B’s Usability near 8.2. A’s “fix” comes from two static-audit findings on its minified file that the juror itself calls false positives. B scored higher on Design and Content, but both concepts came from awards:concept before UXL was called, so read those rows as one run’s variation.

Scanners and measurementsuxl_audit.mjs at 360, 390 and 1440 px · uxl_lint.py on the file · Playwright walk-throughs at 390 px
MeasureExhibit A · awardsExhibit B · awards + UXL
UXL runtime audit score86 / 100 (2 major)96 / 100 (0 major)
UXL static lint score100100
Phone tap targets under 44 px1 of 34, the chapter dial at 41 px0 of 25
Filled buttons in the first phone screen21
Headline in the first phone screenbehind the glass, partly coveredunder the cube, fully legible
Choosing where it shipsa 30-country dropdown; the ship month appears after you pick4 regions, each showing its ship month
Scarcity“123 of 500 places left”, from a hard-coded numberthe batch rule, stated; no counter
Checkout progressnoneStep 1, 2 and 3 of 3
Payment form submitted emptythe browser’s bubble on the first fieldall five problems listed, focus moved to the list
Declined cardno pathtest number 4000 0000 0000 0002, a clear message, nothing charged
After payinga receipta receipt and “What happens next”
Taxesnot mentionedSwiss VAT included; import VAT and duties shown before the balance
In-session awards jury, final round7.93, Usability 7.7 · ship7.83, Usability 7.8 · fix

Scanner scores are heuristics, and this is one run per condition. The in-session juries each judged their own build mid-session, so only the blind run compares like with like. Raw data: data.json, audit A, audit B, B’s first UXL audit, blind jury A, blind jury B, how B used UXL, event by event.

How the two skills worked together

Awards ran the pipeline. UXL came in once, and its scanners twice.

Both sessions walked the same awards pipeline, from direction to ship. Session B called UXL on its own before it wrote any markup, then ran UXL’s linter and auditor before its first jury and again before hand-off. It cost B one more minute and $0.95.

Exhibit A · skill calls in order

  1. craft
  2. concept
  3. system
  4. structure
  5. stack
  6. motion
  7. webgl
  8. jury
  9. ship
  10. jury verdict

Exhibit B · skill calls in order

  1. craft
  2. concept
  3. system
  4. structure
  5. uxl: ux-lawyer
  6. stack
  7. motion
  8. webgl
  9. uxl: lint + audit ×3
  10. jury hero
  11. jury
  12. ship
  13. uxl: final scan

awards skillUXL, called by the session itself

1 · Laws before markup

Four seconds after awards:structure loaded, session B called UXL with this brief, verbatim:

Apply the laws while building the static skeleton and the reservation flow.

It read UXL’s playbook for navigation, calls to action, forms, checkout, modals and multi-step flows, then wrote one line into its page map:

UXL: one primary per view, labels above inputs, autocomplete, card grouping, decline path (4000 0000 0000 0002), never-disabled submit, selection saved to localStorage, no scarcity counter.

2 · Scanners before the jury

With the WebGL layer in, B ran UXL’s linter and auditor before its first jury round. The audit scored 80 with three major findings: the hero headline and the Reserve button measured 1.00:1 contrast. B first planned to log them as exceptions, looked at the annotated screenshot, and rebuilt the layering instead. The next audit scored 93, the final one 96 with no majors.

3 · Laws against the jury

B’s awards jury asked for the headline behind the glass on phones too. B declined and wrote down why:

the phone keeps a legible <h1> under the cube (UXL §L05 Cognitive Load, §L25 Selective Attention); the refraction stays a desktop moment.

Exhibit A’s jury asked for the same thing, and A did it.

The counts

Where the laws changed the code

Each count quotes session B in its own words, shows what shipped, and puts Exhibit A, which never saw UXL, next to it. B wrote no case file this time, so the quotes come from its contract, its design spec and its scanner runs.

State the rule. Don’t invent the count.

A first batch of 500 begs for a counter, and both sessions planned one. Session B’s brief, written before it called UXL, says “the ‘first-batch remaining’ figure is a fictional counter”. After UXL, the counter was gone and the brief said why. Exhibit A shipped it: “Batch one has 123 of 500 places left”, worked out from a constant. A’s hand-off tells the client the number is made up. The page tells visitors nothing of the kind.

Session B’s brief, after UXL, verbatim

Deliberately absent: a “first batch remaining” counter (it would be invented scarcity, UXL §L04)

Exhibit A on a phone: a Ship to dropdown set to Switzerland, then the reservation summary with price, deposit today, charged when it ships, Ships to you March 2027 in batch one, and the line Batch one has 123 of 500 places left.
Exhibit AA dropdown, then “123 of 500 places left”.
Exhibit B on a phone: Deliver to, with four options, Switzerland and European Union shipping March 2027, United Kingdom and United States shipping June 2027, then the reservation summary and the first-batch rule in plain words.
Exhibit BFour regions with their ship months, then the batch rule in words.
const TAKEN_BEFORE_VISIT = 377;   // Exhibit A, src/js/ui/reserve.js: the "places left" source

First batch: 500 Cubes, shipping March 2027 to Switzerland and the EU. Reservations made after
the first 500, and every UK and US reservation, ship in June 2027.   // Exhibit B, under the summary

FindingExhibit A shows urgency nobody counted. Exhibit B compliant.

Show the ship month where people choose

There are only two ship dates, and they depend on where you live. Exhibit A asks you to pick one of 30 countries from a dropdown, then tells you the month in the summary below. Exhibit B offers four regions and prints the month inside each option, so the choice and its consequence sit together. Both captures are in Count 1.

Session B’s design spec, verbatim

the ship month sits inside each region option (recognition over recall, §L29)

FindingExhibit A works, but hides two dates behind 30 options. Exhibit B compliant.

A checkout that explains itself

Both pages say plainly that the payment is a demo. Exhibit A fills in a test card and leans on the browser: submit the form empty and a bubble points at the first field, with no word on what else is missing. Exhibit B counts its steps, lists every problem at once with a link to each field, moves focus to that list, and has a declined-card path. Its submit button is never disabled to signal a mistake.

Session B’s design spec, verbatim

Never disabled to signal invalid input (UXL §L24). One per view (§L28).

Exhibit A payment sheet on a phone after submitting it empty: Deposit CHF 100, a test-mode notice, an outlined email field with the browser bubble Please fill out this field, name field, a prefilled test card and a Pay CHF 100 deposit button.
Exhibit AThe browser’s bubble, on one field.
Exhibit B payment step 2 of 3 on a phone after submitting it empty: a demo checkout notice, a box reading 5 things to fix before paying with a linked list, and red messages under the email and name fields.
Exhibit BStep 2 of 3, five fixes listed, each field marked.
Exhibit A   empty submit  → focus: input#pay-email
Exhibit B   empty submit  → focus: div#pay-errors "5 things to fix before paying"
Exhibit B   declined card → "Your card was declined (demo). Nothing was charged. Try a different card number."

FindingExhibit A is adequate for a demo. Exhibit B handles failure.

Keep the first sentence readable on a phone

Both concepts put the headline behind the glass, so the cube bends the words. On a 1440 px screen that works in both. On a phone the cube covers most of the line. Both awards juries asked for the effect on phones anyway. Exhibit A complied, and its first phone screen reads “Glass” and then smoked glass. Exhibit B refused, citing two laws, and kept the refraction for desktop.

Session B’s jury log, verbatim

Not applied: 1) on a 390 px phone the glass covers nearly the whole word (the first phone build read “L ght, h t, ld.”), so the phone keeps a legible <h1> under the cube (UXL §L05 Cognitive Load, §L25 Selective Attention); the refraction stays a desktop moment.

Exhibit A first phone screen: the headline Glass that holds light, with that holds light behind a smoked glass cube that reads GLASS in orange points, then the product label and a Reserve one for CHF 100 button.
Exhibit A“that holds light.” sits behind smoked glass.
Exhibit B first phone screen: a clear glass cube on a degree ring with a red point-cloud peak inside, the headline Light, held. below it, one line of copy, one red Reserve a Cube button and the deposit terms.
Exhibit BThe headline under the cube, one red button.

FindingExhibit A trades its first phone sentence for the effect. Exhibit B compliant.

Words that existed only as pixels

B’s first build looked right. Its headline, its first-screen copy and the ground behind them were painted in the WebGL canvas, and the real text in the page was transparent. A screenshot can’t tell. UXL’s auditor measured the text itself and found 1.00:1 contrast, which means anything that reads the page instead of the canvas would get nothing: a screen reader’s highlight, a print, a failed GPU. B moved every word and background back into the page and let the canvas draw only the cubes. Exhibit A had kept its headline as real text from the start.

UXL audit, B’s first run, verbatim

h1#light-title "Light, held." 1.00:1 (needs 3:1); div.hero-copy > div.hero-action > a.btn.btn-primary "Reserve a Cube" 1.00:1 (needs 4.5:1)

Session B, after looking at the annotated screenshot

I found that the ground and headline only exist in canvas, making them invisible to screenshots, prints, or CSS readers. I’m restructuring so the canvas draws only the cubes above a transparent page, while the real background and headline live in the DOM, with the glass still refracting them via its transmission buffer.

.gl-headline .hero-title { color: transparent; }   /* site.css, removed */

Canvas model: one fixed, transparent canvas over the page… every word and background on the page
stays real DOM (no transparent text, correct in print, full-page captures and contrast tools)   // AWARDS.md

FindingExhibit A compliant. Exhibit B’s bug caught by the audit and fixed before the first jury.

For the record

What both got right, and what is still open

Both exhibits

  • A concept no jury could trace to another site: in both, you turn the glass, and the page’s navigation is a degree scale.
  • The deposit, the balance due at shipping and the ship month in the summary, before anyone pays (§L29).
  • A demo payment that says it is one, and a confirmation with a reference.
  • Reduced-motion and no-WebGL paths, real text for the headline in the shipped file, and no console errors (§L06).
  • Every invented spec flagged to the client in the hand-off.

Still open

  • UXL missed three things B’s own jury caught: low contrast in the dark Smoke chapter (the auditor measures at page load, before that theme applies), a 320 px overflow (its smallest viewport is 360) and a last screen cut off at the bottom. B fixed all three before hand-off.
  • Session B never called UXL’s ux-auditor agent and wrote no case file or “UX decisions” list, which UXL’s build mode asks for. Its hand-off lists decisions, but without citations.
  • Exhibit B: in B’s source files, UXL’s linter still counts six primary-styled buttons. B shows one per screen and calls the finding a file-level false positive. The shipped single file scans clean.
  • Exhibit B: 9 to 11 font sizes, above the 6 to 8 UXL recommends. Exhibit A has 11.
  • Exhibit A: the chapter dial on phones is 41 px wide, under the 44 px floor.
  • Credit where due: B’s 48 px touch targets, reduced-motion tiers and navigation were set by the awards phases before UXL was called.

Verdict

Exhibit B, on the counts.

Both sessions built an award-grade page, and a blind awards jury barely tells them apart: 7.83 against 7.65, both with Usability at its cap. The jury scores what a juror sees. UXL changed what a buyer meets: no invented scarcity next to a CHF 100 deposit, the ship month where the choice is made, a checkout that explains failure, and a first phone sentence you can read. Session B planned the same counter A shipped, then took it out after reading UXL. That cost one more minute and $0.95. UXL also missed three things B’s own jury caught, so the two reviews belong together.

Reproduce it

Run the case yourself

Same prompt, same model, effort xhigh, two empty folders. Both get the awards plugin; B also gets UXL. Then score both with UXL’s scanners and a blind awards jury, and cut the film.

git clone https://github.com/BurgiSimon/awards
git clone https://github.com/BurgiSimon/UXL
mkdir a b

# Exhibit A: awards only
(cd a && claude -p "$(cat ../PROMPT.txt)" --model "$MODEL" --effort xhigh \
  --plugin-dir ../awards/plugins/awards)

# Exhibit B: awards + UXL
(cd b && claude -p "$(cat ../PROMPT.txt)" --model "$MODEL" --effort xhigh \
  --plugin-dir ../awards/plugins/awards --plugin-dir ../UXL)

# The evidence (B built with Vite: use its release/index.html)
python3 UXL/tools/build_demo.py --without a --with b --prompt PROMPT.txt --slug voxmere
node UXL/tools/demo_evidence_voxmere.mjs

# The blind jury: copy each index.html into a neutrally named folder, keep the logs outside it, then
(cd site-p && claude -p "/awards:jury index.html --verdict" --model "$MODEL" --effort xhigh \
  --plugin-dir ../awards/plugins/awards > ../logs/site-p.jsonl)

# The film
(cd UXL/video && npm i && npm run capture && npm run render)