Why a stream timer counts to a moment instead of counting down, and why the bell is booked before it is needed
The obvious way to build a countdown is a variable holding the seconds left and an interval that subtracts one from it. That implementation is wrong in a way that only shows up in the exact situation a stream timer lives in. Browsers throttle timers in tabs you are not looking at: a hidden tab is held to roughly one wake per second, and after about five minutes in the background Chrome applies intensive throttling and drops it to one wake per minute. A timer built on subtraction believes it has counted one second every time it wakes, so a five minute break spent behind OBS finishes reporting minutes that never elapsed. The fix is to store the absolute instant the timer reaches zero and subtract the current clock from it, which is what core/timer/countdown does. Every read derives the remainder from that instant, so a throttled tab changes only how smoothly the digits move and never what they say. Pausing is the same idea inverted: the end instant is discarded, the remainder is frozen, and resuming computes a fresh end instant from whatever the clock says now, so a pause of ten seconds and a pause of ten minutes both give back exactly the time that was left.
The bell has the same problem and cannot use the same solution, because something has to be awake at zero to make a noise. A setTimeout scheduled for the end of the break is throttled exactly like the tick, so it fires late or, in a tab that has been backgrounded long enough, not until the tab is looked at again. The way out is that the Web Audio clock is not the page clock. An AudioContext runs on the audio thread with its own timeline, and every node can be told to start at a specific time on that timeline. So the strike is not fired at zero, it is booked at the moment you press start: eight oscillators are created immediately with start times several minutes in the future, and the audio thread sounds them on schedule whatever the page is doing. Any change to the countdown invalidates that booking, so pausing, resetting or adding a minute tears the scheduled nodes down and lays a fresh set, which is why the scheduling lives in one effect rather than being spread across the handlers.
There is a second reason a browser timer ends in silence, and it is a permission rather than a schedule. An AudioContext created outside a user gesture is born suspended, and every note scheduled on a suspended context is discarded without an error. The context is therefore created and resumed inside the click handler that starts the timer, not lazily when the bell is first needed, because by then the gesture is long over. The Test bell button exists for the same reason it is useful for checking levels: pressing it is itself a gesture, so it grants the permission and demonstrates the sound in one action. Choosing which output device the bell plays to uses AudioContext.setSinkId, which only Chromium implements, so the tool declares it as a preferred capability rather than a required one and says plainly what happens elsewhere. Device names are a separate honesty problem: the browser withholds them until the user has granted a media permission at least once, so unlabelled outputs are numbered rather than shown as blanks.
The display being a second window is a product decision with an architectural consequence. The point of the tool is that the streamer keeps editing while the audience keeps watching, and the editing must not be captured, so the controls and the display cannot be the same surface. They are the same route, distinguished by a query parameter, and the shell drops the site header and footer for the display so a window capture gets nothing but the clock. State crosses between them on a BroadcastChannel, and the control page sends its whole state on every change rather than sending events, which makes the display a pure function of what it was last told and removes any question of the two drifting out of order. A display that opens midway through a break announces itself and is sent the current state in reply. Everything arriving over that channel is validated before it is rendered, because it comes from another window rather than from this code: the phase has to be one of four known values, the clock fields have to be finite numbers, and the settings are run through the same normaliser that repairs whatever was loaded from storage.
The message field is where emoji stop being decoration and become a correctness problem. Capping a headline with slice would cut by UTF-16 code unit, and almost nothing interesting is one code unit: a flag is two regional indicator symbols, a skin toned wave is a base emoji plus a modifier, and a family is four people joined by zero width joiners. Cutting inside any of those leaves a stray joiner or half a flag on screen at forty points high. The cap is therefore applied with Intl.Segmenter in grapheme mode, which knows where the user perceived characters actually end, and falls back to iterating code points where Segmenter is missing, since that is still far better than code units. The same normaliser rejects a background that is not a six digit hex colour, clamps volume and text size into range, and coerces anything of the wrong type back to its default, which matters because the settings are read from localStorage and localStorage is editable by anyone sitting at the machine.