UXL · UX Lawyer

The Lumenvale case · UXL-DEMO-002 · 20 Oct 2026

Awards sets the ceiling. UXL raises the floor.

Two fresh Claude Code sessions got the same prompt: an award-worthy site for Lumenvale, a fictional dark-sky observatory in the Swiss Alps. Both ran the same model at effort xhigh with the awards plugin, a pipeline of skills built to win Site of the Day. Exhibit B also had UXL installed. The prompt names neither skill and never says “UX”.

Exhibit P · the prompt, verbatim
We’re Lumenvale, a (fictional) dark-sky observatory high in the Swiss Alps. We run guided stargazing nights for small groups of 12: CHF 48 for adults, CHF 24 for kids under 16, with an optional private telescope session for CHF 30. Build our website 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 be award-worthy, the kind of site that wins Site of the Day, and tell the story of a night at Lumenvale. It needs our upcoming nights with the moon phase and seats left, and a way to book tickets for a night. It has to work great on phones and desktop.

Exhibit A · awards

Time
88 min
Tool calls
407
Cost
$29.95
Skills used
10 awards calls, craft → ship

Exhibit B · awards + UXL

Time
105 min
Tool calls
434
Cost
$39.24
Skills used
the same 10 awards calls, plus ux-lawyer and the ux-auditor agent

The exhibits · live

Both pages, exactly as the sessions wrote them

These are the real, unedited files. Each one computes its calendar from today’s date, so the nights are live. Scroll inside a frame and book a night. 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.67.9
Usability (30)6.0 capped8.1
Creativity (20)7.88.5
Content (10)8.38.5
Weighted7.238.14
Dispositionfixship

Exhibit A clears 7.2 overall, but its Usability of 6.0 sits under the 6.8 floor, so the jury sent it back to fix. One bug set that cap: with full motion on, the skip link leaves focus on itself. Exhibit B passed all eight usability walk steps. B also scored higher on Design and Creativity, but both concepts came from awards:concept before UXL was ever called, so read those two rows as one run’s variation, not as UXL’s doing.

Scanners and measurementsuxl_audit.mjs at 360, 390 and 1440 px · uxl_lint.py on the file · Playwright probes
MeasureExhibit A · awardsExhibit B · awards + UXL
UXL runtime audit score90 / 10096 / 100
UXL static lint score9494
Filled Book buttons in the nights list16 of 161 of 27, on the recommended night
Filled buttons in the first phone screen21
Body text on phones15 px17 px
Booking says when you paynoyes, at both steps and on the ticket
Confirmation tells youmeeting stop and timeaddress, bus stop, parking, what to bring, the cloud plan, a calendar file
Skip link with full motion onfocus stays on the linkfocus moves to the content
In-session awards jury, final round7.42, Usability 7.07.81, Usability 7.8

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, blind jury A, blind jury B, UXL case file.

How the two skills worked together

Awards ran the pipeline. UXL was called in twice.

Both sessions walked the same awards pipeline, from direction to ship. Session B added two UXL steps on its own: the skill before it built anything, and the auditor before it shipped.

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. jury hero
  10. uxl: ux-auditor
  11. jury
  12. ship + case file

awards skillUXL, called by the session itself

1 · Laws before code

Right after awards:structure laid out the chapters, session B called UXL with this brief, verbatim:

Apply the laws to the booking flow and navigation.

It then read UXL’s specs for navigation, calls to action, forms, modals and multi-step flows, and wrote phone layouts and accessibility notes into its page map. The idea of opening on the next dark night came from awards:structure, before UXL was called.

2 · Two reviewers, two questions

The awards jury asks what a Site of the Day juror sees: it scores captures of every chapter on Design, Usability, Creativity and Content. The UXL auditor asks what a visitor runs into: it booked seats in a real browser at 390, 375 and 1440 px and measured. Its case file came back with 0 critical, 5 major and 7 minor findings, each citing a law.

3 · One fix list

The awards:ship skill takes its fixes from the jury report. Session B handed it the UXL case file as well, verbatim:

Also fold in the UXL auditor's case file (0 critical, 5 major: M1 mobile two filled Book buttons via CSS specificity main.css:108-109; M2 calendar rows lack a 'Dark sky' tag …

Ship fixed all five majors in one pass, next to the jury’s three craft fixes. The counts below follow each one from the case file into the code.

The counts

From case file to code, law by law

Each count quotes the UXL auditor’s finding on Exhibit B, shows the line that shipped, and puts Exhibit A, which had no auditor, next to it. B’s own awards jury caught the first one too. The other four came from UXL alone.

One loud Book at a time

Emphasis only works when it is rare. Exhibit A paints every Book button in the list the same torch red, so 16 equal buttons compete and none leads. Exhibit B fills only the night it recommends and outlines the rest. Its auditor also found a CSS bug that put two filled Book buttons on a phone screen at once.

UXL case file, verbatim

On phones, two filled orange "Book" buttons compete through most of the story, and they lead to different places. This is a CSS specificity bug.

Exhibit A nights list on a phone: one night after another, each with its moon, star count, seats and a filled red Book button.
Exhibit AEvery row ends in the same filled button.
Exhibit B nights list on a phone: nights with outlined Book buttons, a sold-out night, and one filled Book button on the recommended dark-sky night.
Exhibit BOutlined Book buttons; the one filled button sits on the recommended night.
/* main.css: the filled header Book is desktop-only now */
@media (min-width: 768px) {
  .bar.is-solid:not(.is-calm) .bar__book { background: var(--accent); color: var(--on-accent); border-color: transparent; }
}

FindingExhibit A dilutes its call to action. Exhibit B compliant after the fix.

Say when people pay, where they commit

Neither page takes payment. Exhibit A never says so: its button reads “Hold 2 seats · CHF 96”, and its cloud policy offers “a refund”, which implies money already changed hands. Exhibit B states the terms in the calendar intro, under the total on both booking steps, and on the ticket.

UXL case file, verbatim

"Reserve 2 seats · CHF 96" next to a big total looks like a charge. Hesitant users drop out right before the one fact that would reassure them.

Exhibit A booking sheet on a phone: adults and children steppers, a private session checkbox, name field, and a footer with Total CHF 96 and a Hold 2 seats, CHF 96 button.
Exhibit ATotal and “Hold 2 seats”, with no word on payment.
Exhibit B booking step 1 of 2 on a phone: night selector, adults and children steppers, a private session that is taken this night, and a footer reading Total CHF 96, 2 adults, pay on the night, with a Continue button.
Exhibit B“2 adults · pay on the night” under the total, from step 1.
totalNote.textContent = `${parts.length ? parts.join(' · ') : 'No seats yet'} · pay on the night`;   // booking.js

<p class="booking__pay">No payment now. You pay at the valley station by card or TWINT; cancel free until 24 hours before.</p>

FindingExhibit A leaves the price looking like a charge. Exhibit B compliant.

End on everything a guest needs

The confirmation is the screen people keep. Exhibit A gives a reference, the bus stop and the meeting time. Exhibit B’s ticket adds the address, parking, what to bring, how a cloud cancellation will reach you and what you will pay, plus a calendar file with the location in it.

UXL case file, verbatim

Task 3 ("know when and where to meet") is only half served at the step people screenshot and keep.

Exhibit A confirmation on a phone: Your seats are marked, a reference, meet at the Alp Lumen Postbus stop at 18:55, a cloud refund note and a demo booking notice.
Exhibit AReference, stop and time, plus an honest demo notice.
Exhibit B ticket on a phone: You're on the plate, with rows for night, meet time, where, what to bring, if cloudy, seats, private session, moon and total paid at the valley station.
Exhibit BA ticket with where, what to bring and the cloud plan.
['Where', 'Lumenvale valley station, Talweg 1, Val Lumen · PostBus stop “Val Lumen, Talstation”, free parking'],
['Bring', 'Warm layers and a hat (often below 0 °C); we lend jackets, blankets and red torches'],
['If cloudy', b.phone ? 'We email and text you by 14:00 on the day' : 'We email you by 14:00 on the day'],

FindingExhibit A is adequate. Exhibit B ends on a complete ticket.

Call a dark night “dark” where people choose

A dark sky is the reason to pick one night over another. Before the fix, Exhibit B’s filter, hero and booking used three different words for it, and the rows used none. It now tags dark rows “Dark sky”, dims the moon when it stays below the horizon, and defines the term once in the legend. Exhibit A reached a good answer on its own route: each row gives an expected star count.

UXL case file, verbatim

"Dark", the attribute task 1 depends on, is hard to read in the calendar rows.

${n.dark ? '<span class="night__tag">Dark sky</span>' : ''}   // schedule.js, every calendar row

.moon.is-down { opacity: 0.45; }   /* main.css: a moon below the horizon reads as absent */

FindingBoth compliant at hand-off, by different routes.

Keyboard users keep their place

In Exhibit B, finishing a booking re-rendered the calendar and deleted the button that had opened the dialog, so keyboard focus fell back to the top of a 9,000 px page. The fix sends focus to the booked night. Exhibit A handles this case: its close handler falls back to the night’s Book button.

UXL case file, verbatim

Keyboard and screen-reader users lose their place on a page about 9,000px long, at the end of the main task.

const back = trigger && document.contains(trigger) ? trigger : night && (document.querySelector(`#${night.id} .night__book`) || document.getElementById(`${night.id}-t`));   // booking.js

FindingExhibit A compliant. Exhibit B’s bug caught and fixed before hand-off.

For the record

What both got right, and what is still open

Both exhibits

  • A real sky: moon phases, moonrise and twilight computed for the site, every night listed with its moon and seats.
  • A committed visual world that the jury could not trace to a source site.
  • Bottom-sheet booking on phones, a live total and specific error messages (§L29).
  • Reduced-motion tiers, a no-WebGL fallback and no layout shift (§L06).
  • No fake urgency or pre-ticked extras (§L04).

Still open

  • Exhibit A: the skip link fails with full motion on, which capped its blind Usability score at 6.
  • Exhibit B: the ticket says “We’ve sent the details to” your email, but nothing is sent. Its hand-off flags this; a real backend has to make it true.
  • Exhibit B: the blind jury’s three craft fixes. The scrolled header lets type ghost through, the moon scale stops two thirds along its track, and the dawn headline is cut under the bar on a 1440 × 900 screen.
  • Exhibit B: 11 to 12 font sizes, above the 6 to 8 UXL recommends.
  • Both: the fixes made after the last in-session jury round were not juried again in-session. The blind run above judged the final files.
  • Exhibit B cost 31 % more and took 19 % longer: $39.24 and 105 min against $29.95 and 88 min.

Verdict

Exhibit B.

Both sessions built an award-grade page: a committed world, a real sky, motion with reduced tiers. The awards skills set that ceiling in both. Exhibit B is the one the blind jury would ship, at 8.14 against 7.23, and the one where the person booking knows what they will pay, when, and where to go. UXL added two steps and 31 % to the bill. Its auditor found five major issues before hand-off, and four of them were ones the craft review had not flagged.

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.

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
python3 UXL/tools/build_demo.py --without a --with b --prompt PROMPT.txt --slug lumenvale
node UXL/tools/demo_evidence_lumenvale.mjs

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