Case study · Design engineering

Queen's Dilemma

An ornate, art-heavy web app that holds up at every screen width and text size. I designed and built it so six friends could hand out a board game's characters fairly, and now any group can.

The Queen's Dilemma landing page: a parchment scroll on a walnut table with a crowned wax seal at the top, a blackletter title, the six character crests, three steps marked with numbered wax seals (create a court, rank the characters, reveal the roles), a 'Create a court' button, and 'Already invited to a court? Sign in to join'.
Role
Solo: product, interaction and visual design, frontend and backend
Timeline
October 15, 2026 to present
Status
Live and open to any group of 3 to 6. Its first game was six friends' game night.
Built with
Next.js, React, TypeScript, Tailwind container queries, Convex, Clerk, dnd-kit
Responsive by component
Container queries instead of screen breakpoints. At 640px the biography went from an 81px to a 318px column, no name splits mid-word, and 200% text gets a deliberate layout. A script sweep verified it.
A theme turned into a system
Every text colour pairing has its contrast recorded, and the game's wax seal marks every commitment, from sealing a ranking to breaking it open for your role.
Motion with a job
A loader that becomes the page instead of vanishing, and a reveal you break open yourself. Each has a reduced-motion path that keeps the moment.
Private tool to public product
A fairness rule anyone can check, then a one-night tool rebuilt for strangers, with tests going from 78 to 335.

Fair, and impossible to rig

My friends and I were about to play The Queen's Dilemma, where everyone plays a different character. We needed to hand out the six characters without anyone choosing first, and we weren't going to be in the same room. So everyone ranks the characters on their own phone, whenever suits them, and seals the ranking. The last seal assigns every role at once.

Taking turns rewards whoever goes first. Picking the lowest total ranking sounds fairer, but it will happily give one person their last choice as long as everyone else does well. So the server checks all 720 possible assignments and looks after the unhappiest player first, then the next, and so on.

Judge the two options by

Option A Chosen

  1. 6
  2. 2
  3. 1
  4. 1
  5. 1
  6. 1

Total 12

Option B Chosen

  1. 3
  2. 2
  3. 2
  4. 2
  5. 2
  6. 2

Total 13

By total, A wins, and someone gets their 6th, last choice.

Worst first, B wins. Its total is higher, but nobody does worse than their 3rd choice.

Each circle is the choice one player gets, from 1 (most wanted) to 6 (least wanted), listed worst first. Switch the rule to see which option wins.

Nobody can rig it, including me

I was one of the six players, so cheating had to be impossible for the person running it. There's no button to press: the last ranking to arrive runs the assignment in the same transaction. Ties are broken by a seed whose hash is saved before anyone ranks, and no query returns anyone else's ranking. Tests prove each of these.

What happened on the night

Four of us got our first choice, one their second and one their fourth. Two outcomes were equally fair, so the sealed seed chose between them. One friend was a little disappointed with their fourth. Because the rule protects the worst outcome first, that means no arrangement could have given everyone their third choice or better.

I first showed the seed and its hash on the result, then took them off: accurate, but too technical for a group of friends. The result now explains the rule in one sentence.

The 'How the court decided' section of a result: a sentence explaining the rule, then how many players got each choice: four got their 1st, one their 2nd and one their 4th. Below is a folded 'The figures behind it' and a note that rankings stay private.
The rule in one sentence, on the result screen. A test court set up with the same numbers as our game night.

One card at every width

This is where I spent the most time. The ranking card is the heart of the app, and I made it work everywhere in a day of small changes, each checked by measurement.

A card holds a lot: the rank seal, the crest over the portrait, a drop-cap name and title, a quote, a biography, and three controls (drag, Up and Down). Its layout comes from the game's character scroll, which is a desktop composition. On narrower screens it broke names in half. At tablet widths it crushed the biography into an 81px column. At 200% text it pushed the name off the screen.

Compare the first card at

Before

Before at 320px: the rank seal with Up and Down stacked beneath it on the left, the crest and portrait, and the name broken into 'ili' and 'an' beside a red drop cap, with the title on two lines.

The name splits into “ili” and “an” beside the art, and the title wraps.

Now

After at 320px: the rank seal, Up, Down and the drag grip on one line, then the crest and portrait beside the whole name 'Jilian' and 'The Silk Weaver', then the quote and 'About Jilian'.

The controls move onto the seal's line, so the art and name get the full width. The name is whole.

Before

Before at 390px: the rank seal with Up and Down stacked beneath it, and the art beside the name and title.

Fine at this width, with the controls in a rail down the left.

Now

After at 390px: the same layout as at 320px, with the controls on the seal's line and the quote across the card.

The same arrangement as on narrower phones, so phones share one layout, and the quote gets the full width.

Before

Before at 390px with 200% text: the controls and text have doubled but the portrait hasn't. The drop cap is pushed to the card's right edge, the name has disappeared off the screen and the title is cut off.

The rail and the text double but the art doesn't, so the name is pushed off the screen entirely.

Now

After at 390px with 200% text: the rank seal and controls on one line, the portrait centred above, then the large drop-cap name 'Jilian', the title, the quote and 'About Jilian', all inside the card.

The card reads its own width, so large text gets the same layout as a narrow phone. Nothing got smaller.

Before

Before at 640px: the desktop layout in a narrow card, with the biography squeezed into a tall column beside the portrait.

The desktop layout switched on at a 640px screen, but the card had only 386px. The biography is crushed beside the portrait.

Now

After at 640px: the art beside the name and title, the biography running across the card underneath, then the quote, with the controls in a column on the right.

A tablet layout: art beside the name, biography across the card beneath.

Before

Before at 782px: the desktop layout, with the biography in a narrow column beside the portrait.

Still the desktop layout in too little room.

Now

After at 782px: the tablet layout, with the biography and quote sharing one measure under the art and name.

The tablet layout, with the biography and quote sharing one measure.

Before

Before at 1280px: the desktop composition, with the biography beside the portrait and small type.

Desktop, where the original composition worked.

Now

After at 1280px: the same desktop composition with larger type, the quote under a gilt ornament, and the controls in a column on the right.

The desktop composition is kept. The controls moved right, and the type and quote were refined later.

The same card from the build just before this work and from today's app, cropped to the card at each screen width. On a wide screen they're shown at actual size.

Why breakpoints couldn't fix it

The desktop layout switched on at a 640px screen, but at that width the card's content column was only 250 to 400px. Large text made it worse: the controls are sized in rem, so they double at 200% text, but the art is sized in pixels, so it doesn't. No rule based on the screen could know how much room the name actually had.

Let the card read its own width

I made the card and its content column into two named containers, and every layout decision reads the width it actually has. That turns large text into the same problem as a narrow phone, with the same deliberate answer. My rules were: nothing shrinks, names never break, and the desktop composition stays.

I also moved the controls. The first card kept Up, Down and the drag grip in a rail on the left, under the rank. Most people hold a phone in their right hand, so they now sit in a column on the right, under the thumb. Up and Down also scroll the page by exactly as far as the card moved, so the button you pressed stays where it was: six taps in one spot take a character from 6th to 1st.

<article className="@container/entry">
  <div className="grid
    [grid-template-areas:'seal_ctl'_'body_body'_'more_more']
    @min-[max(190px+2.75rem,46px+9.5rem)]/entry:[grid-template-areas:'seal_body_ctl'_'seal_more_ctl']">
    …
    <div className="@container/content [grid-area:body]">
      <div className="grid [grid-template-areas:'art'_'name']
        @min-[15rem]/content:[grid-template-areas:'art_name']
        @min-[28.5rem]/content:[grid-template-areas:'art_name'_'art_desc']">
Simplified from the app's CharacterCard. Thresholds mix pixels and rem, like max(190px + 2.75rem, 46px + 9.5rem): the pixel part is the art and seal, the rem part is the text and controls. So a threshold moves with text size by exactly as much as the parts that grow.
How the card arranges itself, by the room it has (widths at 100% text)
Room Controls Art and name Biography and quote
Under ~305px, or large text Seal and Up/Down on one line; the grip laid over the card's edge Portrait centred above the name Quote across the card
~305 to 420px Up, Down and grip on the seal's line Art and name side by side, centred as one group Quote across the card
~424 to 640px A column on the right, under the thumb Side by side Quote across the card
~640 to 916px Column on the right Phone-size portrait beside the name Biography and quote across the card, one 52ch measure
~916px and up Column on the right The scroll's own composition: large portrait, drop cap spanning name and title Biography beside the portrait
Biography column at 640px
81 → 318px
Tallest card at 640px
831 → 289px
Names split mid-word
6 → 0

Measured, not eyeballed

I tested the layout with measurements instead of three breakpoints. Short headless Chrome scripts rendered the real ranking screen and checked:

  • Names: every name renders on one line, at 320, 375, 390 and 430px, at 100% and 200% text.
  • Collisions: every 4 to 8px from 232 to 1000px, at five text sizes, no crest, portrait, name or quote overlaps the seal or the controls.
  • Controls and measure: every control stays inside its card and at least 40px, and lines are counted in rendered characters, with edges measured from the letters, not the boxes.

The sweep caught what my eyes missed. In one 32px band the crest hung over the rank seal, and at one 4px step the laid-over grip covered the name. Looking caught what the numbers missed, like a control squeezed until it clipped.

Saying no with data

Some ideas, including some of mine, were built and measured and then dropped:

  • Switching to desktop sooner. Lines would drop to about 32 characters, under the range I'd set.
  • Centring the text block in the spare space. It trimmed the right-hand gap, but the text then lined up with nothing: neither the seal nor the crest.
  • Centring the controls in the spare space. It measured perfectly, and side by side it looked worse.

A theme with rules

My first version used the game's art but not its layout: gold blackletter on a navy background, with parchment cards floating on top. The character names were hard to read, and the cards ignored how the game's own character scroll is laid out.

The next day I rebuilt every screen on that scroll. Parchment became the page, the crest moved over the portrait, a wax seal took the rank, and the name got a drop cap. I chose a display face that keeps the blackletter mood but reads, and supplied the scroll, seal and table art myself instead of generating any.

Day 1

The first ranking screen: a gold blackletter 'Rank the Court' heading on dark navy, then boxed parchment cards, each with a large rank number, a drag handle, a portrait, the name in fraktur, a small crest badge, 'Show details', and Up and Down buttons.

Day 2

The redesigned ranking screen on a parchment scroll with a wax seal at the top: 'Rank the court', each character with a red wax rank seal, Up and Down buttons, the crest overlapping the portrait, a red drop-cap initial, the title, and the character's quote between rules. A red 'Seal my ranking' button is fixed at the bottom.
The same screen a day apart. Both use the game's art; only the second is built from the scroll it comes from.

One seal for every commitment

A theme like this falls apart quickly if every screen improvises, so I gave the game's wax seal one job: it marks commitment. You press it to seal your ranking, watch your friends' seals appear as they seal theirs, break yours to change your mind, and break it once more to read your role. Wax red follows the same rule. It fills only the seals and the one button on each screen that commits something.

  • Rank

    Raised numerals in the game's typeface give each character its place. Sized in rem, they grow with the text.

  • Sealed

    A player's initial, sunk into the wax like their own stamp, once their ranking is sealed.

  • Not sealed yet

    The outline of a seal around an empty crown. It's drawn rather than faked in wax, and never lettered, so it can't pass for sealed.

Drawn from the app's seal art, typeface and crown. The seal is hidden from screen readers, so every use comes with text that says what it means.

A seal big enough to read

The confirmation for sealing first had a small seal beside its title: safe, but the initial was too faint to read. I hung it from the sheet's top edge instead, half outside, the way wax closes a letter. A much larger seal cost only about 10px of height. The sheet scrolls on small phones, and a scrolling box clips anything hanging outside it, so I split the dialog into two layers: one that holds the seal, and a sheet inside it that scrolls.

First try

A 'Seal your ranking?' sheet on a phone with a small wax seal beside the title, the ranking listed 1 to 6, a note that you can change it until everyone has sealed, and 'Seal it' and 'Go back' buttons.

Now

The same sheet as shipped: a large wax seal engraved with the player's initial, R, hangs from the middle of the sheet's top edge, with the title centred beneath it and the ranking listed 1 to 6 above 'Seal it' and 'Go back'.
Measured at four screen sizes from 320 × 568 up: the seal is centred within 1px and “Seal it” stays on screen.

One tap shouldn't stall six people

Changing your ranking deletes your sealed one, so nothing private lingers. It also means one stray tap holds up the reveal for everyone. The first version unsealed instantly. Now breaking your seal asks first, says what it costs everyone else, and starts on “Keep it sealed”. The waiting room shows your sealed order, so nobody breaks a seal just to check.

Day 1

The first waiting room: 'Your ranking is sealed', '4 of 6 votes submitted', six players with crowned seals or dashed rings, and a small button already reading 'Unsealing…'.

Now

Today's waiting room, dimmed behind a 'Break your seal?' sheet with the player's engraved seal hung from its top edge. It explains that the court can't be revealed until you seal again, so everyone else will be waiting on you, above 'Break my seal' and 'Keep it sealed' buttons.

Tokens with the contrast written down

The rules live in a design document I keep in step with the code. Every colour pairing used for text has its contrast recorded. Old gilt doesn't have enough contrast on parchment to be text, so instead of a ratio it gets a rule: rules and frames only.

  • Parchment #ecdfc3 The page itself
  • Iron-gall ink #29231e Text, 12:1 on parchment
  • Faded ink #55473a Secondary text, 6.3:1
  • Wax red #9d2734 Commitment: seals, the Seal button
  • Old gilt #a47c36 Rules and frames only, never text
  • Walnut #241912 The table under the scroll

Motion with a job

The app has very little motion, and each piece is there to orient or to confirm. That restraint is what lets the few moments land.

The loading scroll on a phone: it unrolls under “Loading…” and a short tale, rolls back up, and when the court is ready, the ranking page unrolls on from the open scroll. Recorded from the running app.

A loader that becomes the page

The first loader was a breathing wax seal and “Summoning the court…”, and it felt flat. Now a scroll unrolls under an illuminated “Loading…” with a short tale from the court, then rolls back up and goes again. When the page is ready, the scroll doesn't vanish. The page unrolls on from wherever the scroll had got to, while the loader's words fade where they stood. A skeleton screen was never an option: the loader shows before the app knows which screen comes next.

  • Never a blink. A load under 250ms shows no loader at all. Once it appears, it stays at least 700ms, so it never flashes for a moment and vanishes.
  • Roll and paper never part. My first version revealed the sheet with a clip on the main thread while the roll moved on the compositor, and under load they drifted apart. Now the window lifts with the roll while the sheet drops by the same amount: all transforms.
  • Quiet for everyone else. Screen readers hear only “Loading…”. With reduced motion, the scroll rests open with its tale.

The reveal you break open

Voting ran over a few days, so most players met their result alone, on a phone, after an email. The first version played its animation as the page loaded, before anyone was looking. Now the player presses the seal, and it cracks where they pressed it.

The reveal on a phone: the court's name, 'The Thornwood Table', over a walnut table, then 'The court has spoken. Every role has been assigned. Yours is sealed inside.', a large wax seal engraved with the player's initial, and 'Break the seal' beneath it.

You press it

After breaking the seal, a scroll unrolls with the character: Lyrie's crest and portrait, a drop-cap name, 'The Veil Piercer', a badge reading 'Your #1 choice. No one in the court got worse than their 4th choice.', her quote and her biography.

Your role unrolls

  • No empty frame. The portrait, crest and scroll load while the letter is still sealed, at the exact sizes the reveal asks for. On a throttled connection, all three arrived before the press.
  • Reduced motion keeps the moment. Breaking the seal is a press, not an animation, so it stays. Only the motion goes.
  • Focus follows the change. If the reveal arrives while you're in the waiting room, focus moves to the new heading, so a screen reader isn't left on a button that no longer exists.

Opening it to strangers

The first version was built for one evening: one court, six friends in a config file, fixes from the command line. That was the right call for a one-time event. Opening it to any group of 3 to 6 meant undoing almost every shortcut.

  • Keep the friends' court exactly as it was

    Their game was already decided. I moved it into the new multi-court model in place, and a test checks that every row except two new fields is identical, so nobody had to rank again and their result still replays.

  • Invite by link, not by email

    A public site that emails anyone you name is a spam relay. Hosts get a link and a ready-to-paste message instead. The only email the site sends goes to people who have already signed in.

  • Removing a player never reveals the court

    When a court stalls, the host can remove someone, but that mustn't trigger the reveal as a side effect. Removal is undoable, the host is nudged first, and the reveal becomes a deliberate button. The host controls when, never what.

  • Make the privacy page true, then keep it true

    The code deleted courts after 30 days, but the email service kept every recipient's address. A daily cleanup made the promise true end to end, and the page reads its numbers from the same constants as the deletion job.

I shipped it in seven phases, each written test-first and checked by an independent review. Accessibility audits ran alongside: axe-core on every public page, reflow at 320px, enlarged text spacing, keyboard focus and Windows high-contrast mode, reaching out to the 404 page and link previews in group chats.

Automated tests
78 → 335
Players per court
6 → 3 to 6

Working with AI

I built this with Claude Code, using skill-based workflows for planning, implementation, design review and code review. I wrote the build spec myself: the fairness rule with a worked example, the privacy model, the reveal's rules and the acceptance criteria. Most of my time went into design decisions and reviewing what came back.

  • Side by side, then my call. I had alternatives built next to each other and turned down the ones that didn't work, including three replacement fonts for the drop caps.
  • Reviews get checked too. When a design review misread how the reveal works, I pushed back, checked it against the backend code, and dropped half of its suggested fix.

The pattern I noticed most: the first build of a request tended to come out safe, optimized for every constraint. A short “what if” from me, like bigger and in a better place, pushed it to the expressive version, and then I made that version meet the same constraints.

What I learned

Text size is a layout input, not a zoom level. Once the card read its own width, a phone with large text and a narrow phone became the same problem with the same answer, and I stopped designing for screens and started designing for the room a component actually gets.

Measuring a layout finds different bugs from looking at it. The scripts caught collisions in bands I'd never have stopped at by hand, and screenshots caught problems no metric was checking. I needed both.

A theme only holds up when it has rules. The seal works because it means the same thing everywhere, and because I left it out of the places it didn't belong.

Try it

It's live at questionablequeens.quest. We're still playing, so it keeps growing with our games, starting with session notes for each game night.

Interested in this work? Email me at ilya.m.polyakov@gmail.com or go back to the home page.

Queen's Dilemma is an unofficial fan tool for The Queen's Dilemma by Hjalmar Hach and Lorenzo Silva (Horrible Guild), and isn't affiliated with or endorsed by the publisher. Character art and text come from the game's character scroll. Screenshots show a test court with invented players.