diff --git a/README.md b/README.md
index fc95a9d..1c69659 100644
--- a/README.md
+++ b/README.md
@@ -89,10 +89,10 @@ The Summary page and the sheet tabs in the lobbies show the character as **A4 pa
The **Lobbies** tab groups a campaign. Whoever creates a lobby is its DM and gets an 8-character join code. Players join with the code and pick one of their saved characters (they can switch it later or leave).
-The lobby page is built for desktop: the main area on the left, the dice tray at the top right, the chat as a column at the right edge of the window and a dice hotbar at the bottom centre of the main area.
+The lobby page is built for desktop: the main area on the left, the chat as a column at the right edge of the window and a dice hotbar at the bottom centre of the main area.
- **Chat** (right column): live chat for everyone, plus **whispers** to a single person (pick them in the dropdown next to the message field). Whispers are only visible to sender and recipient, the DM does not see other people's whispers. The same feed shows the **event log** (joins, leaves, character switches, renames, new codes); it can be hidden with a checkbox. The chat polls the server every few seconds, keeps the last 1000 entries per lobby and allows 500 characters per message and 30 messages per minute.
-- **Dice** (bottom centre): d4, d6, d8, d10, d12, d20 and d100, with a count (1 to 10 dice). A click rolls on the server (so nobody can cheat), the dice are thrown into a tray at the top right, next to the chat, for everybody in the lobby (CSS 3D: the d6 is a real cube, the other dice are faceted slabs; the animation always lands on the server's result) and the result is posted in the chat. Rolls follow the chat's recipient dropdown: with a person selected it is a private roll only that person (and you) can see. **The DM's rolls are hidden from the players** (the hotbar says "Hidden (only you)"); the DM can still pick a player as recipient to show them a roll.
+- **Dice** (bottom centre): d4, d6, d8, d10, d12, d20 and d100, with a count (1 to 10 dice). A click rolls on the server (so nobody can cheat), the dice tumble into a tray above the hotbar for everybody in the lobby and the result is posted in the chat. Rolls follow the chat's recipient dropdown: with a person selected it is a private roll only that person (and you) can see. **The DM's rolls are hidden from the players** (the hotbar says "Hidden (only you)"); the DM can still pick a player as recipient to show them a roll.
- **Party** (tab): member cards (name, concept, class, profession). The DM also finds the join code, rename, remove player and delete lobby here.
- **One sheet tab per character** (next to Party): the DM gets a tab for every character in the lobby (read-only, always the current saved version, refreshed every 15 s); a player gets "My character". Hover or tap a value to see how it is made up. "Open in the Character tab" jumps to the full editor (for the DM it is a read-only look, "Save" would create the DM's own copy).
diff --git a/frontend/src/DiceTray.jsx b/frontend/src/DiceTray.jsx
deleted file mode 100644
index 79d887a..0000000
--- a/frontend/src/DiceTray.jsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { useMemo } from 'react'
-
-// Dice tray with CSS 3D "throws". The result always comes from the server; the animation only has to land on it:
-// d6 is a real cube, the other dice are faceted slabs (stacked layers) that tumble in perspective.
-const rand = (a, b) => a + Math.random() * (b - a)
-const reduced = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
-const CELL = 72, COLS = 5, SIZE = 58
-
-// Outline and inner facet lines of each die seen face-on (viewBox 0 0 100 100)
-const FACE = {
- 4: { poly: '50,6 96,88 4,88', lines: ['50,62 50,6', '50,62 96,88', '50,62 4,88'], ty: 70 },
- 8: { poly: '50,4 96,50 50,96 4,50', lines: ['50,4 50,96', '4,50 96,50'], ty: 55 },
- 10: { poly: '50,3 97,40 50,97 3,40', lines: ['50,3 50,97', '3,40 50,62', '97,40 50,62'], ty: 56 },
- 12: { poly: '50,4 96,38 78,94 22,94 4,38', lines: ['50,4 50,30', '96,38 72,46', '78,94 64,72', '22,94 36,72', '4,38 28,46', '50,30 72,46 64,72 36,72 28,46 50,30'], ty: 62 },
- 20: { poly: '50,3 93,27 93,73 50,97 7,73 7,27', lines: ['50,28 74,70 26,70 50,28', '50,28 50,3', '50,28 7,27', '50,28 93,27', '74,70 93,73', '74,70 50,97', '26,70 50,97', '26,70 7,73'], ty: 56 },
- 100: { circle: true, lines: [], ty: 56 },
-}
-
-function Slab({ sides, value }) {
- const f = FACE[sides]
- const depth = 6
- const svg = (i) => (
-
- )
- return <>{Array.from({ length: depth }, (_, i) => svg(depth - 1 - i))}>
-}
-
-// rotation that brings face v of the cube to the front
-const CUBE = { 1: [0, 0], 2: [0, -90], 3: [-90, 0], 4: [90, 0], 5: [0, 90], 6: [0, 180] }
-const CUBE_FACES = [['front', 1], ['right', 2], ['top', 3], ['bottom', 4], ['left', 5], ['back', 6]]
-
-function Die({ sides, value, i, col, row, rolled }) {
- const v = useMemo(() => {
- const cube = sides === 6
- const [fx, fy] = cube ? CUBE[value] : [0, 0]
- const fz = cube ? 0 : rand(-14, 14)
- const turn = () => (Math.random() < .5 ? -1 : 1) * Math.round(rand(2, 4)) * 360
- return {
- '--dx': `${rand(120, 230)}px`, '--dy': `${-rand(130, 200)}px`, '--delay': `${i * 90}ms`, '--dur': `${rand(1.05, 1.3).toFixed(2)}s`,
- '--rx0': `${fx + turn()}deg`, '--ry0': `${fy + turn()}deg`, '--rz0': `${fz + turn()}deg`,
- '--rx1': `${fx}deg`, '--ry1': `${fy}deg`, '--rz1': `${fz}deg`,
- left: col * CELL + rand(-5, 5) + 8, top: row * CELL + rand(-3, 5) + 6,
- }
- }, [sides, value, i, col, row])
- const { left, top, ...vars } = v
- return (
-