diff --git a/README.md b/README.md
index 222a1649..a8522c02 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,11 @@ An uncertain saved-kit recovery keeps its exact retry details in memory and prev
Use **Pass your seat to another player** for a voluntary one-time transfer. The private offer lasts 24 hours; acceptance preserves progress and revokes the old owner’s sessions and recovery kits. The recipient uses **Accept a seat handover** on the home page, then creates their own recovery kit. Keep the tab open until acceptance is confirmed; exact private retry details survive refresh in that tab. See [seat handover](docs/SEAT_HANDOVER.md) for cancellation, retry behavior and current limits.
+The table header offers optional **Sound effects**, with mute, volume and a test
+sample. Short cues mark phase changes and automatic action notices. Sound starts
+off; preferences stay in your browser. See [table sounds](docs/TABLE_SOUNDS.md)
+for activation, background behavior and prototype limits.
+
## Verify changes
Use the [development guide](docs/DEVELOPMENT.md) for architecture, focused tests
diff --git a/app/globals.css b/app/globals.css
index 24b34d63..831e8dd5 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -105,6 +105,24 @@ a:focus-visible {
align-items: center;
margin-left: 28px;
}
+.table-sounds { position: relative; max-width: 100%; font-size: 14px; }
+.table-sounds summary { display: flex; align-items: center; gap: 8px; }
+.table-sounds summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
+.table-sounds-panel {
+ position: absolute; right: 0; top: calc(100% + 8px); z-index: 45;
+ display: grid; gap: 12px; width: min(280px, calc(100vw - 48px));
+ padding: 16px; border: 1px solid var(--border); border-radius: 8px;
+ background: #141814; color: #f5ecda; box-shadow: 0 8px 28px #0008;
+ font-size: 14px; line-height: 1.5;
+}
+.table-sounds-panel p { margin: 0; }
+.live-table .masthead { flex-wrap: wrap; gap: 12px; }
+.live-table .masthead-right { flex-wrap: wrap; }
+@media (max-width: 950px) {
+ .live-table .masthead-right { position: relative; }
+ .table-sounds { position: static; }
+ .table-sounds-panel { left: 0; right: auto; }
+}
.status-dot {
width: 6px;
height: 6px;
diff --git a/components/game-table.tsx b/components/game-table.tsx
index 136101b2..b05a8851 100644
--- a/components/game-table.tsx
+++ b/components/game-table.tsx
@@ -72,6 +72,7 @@ import {
} from './richese-allied-no-field';
import { presenceByLocation } from '@/game/force-presence';
import { AutomaticActionNotice } from './automatic-action-notice';
+import { TableSounds } from './table-sounds';
import { EcazSpice } from './ecaz-spice';
import { VictoryProgress } from './victory-progress';
import { tableActionOwner } from '@/game/table-turn';
@@ -902,6 +903,12 @@ export function GameTable({
+
Math.max(last, entry.seq), -1),
+ automaticSequence: automaticEvents.reduce((last, entry) => Math.max(last, entry.seq), -1),
+ }} />
{g.status === 'lobby' ? 'Gathering players' : `Turn ${g.turn} / 10`}