உள்ளடக்கத்திற்குச் செல்லவும்
KAMOUniverse

மாற்று பதிவேடு

என்ன புதிது.

அது கப்பல்கள் போல், தளத்தில் ஒவ்வொரு மேம்பாடு.

  1. · kamo-internal

    Open at the front of a new session, stay put inside one

    Two rules that only make sense together, and the panel did the opposite of both. A member arriving in a NEW browser tab — a fresh sign-in, a second tab opened for something else — now gets the panel at the front: the Directory (the first tab), the first sub-tab of every strip inside it, and an empty search. For the rest of that session the panel is theirs: closing it, and reloading the page, both put them back exactly where they were. Before, closing it reset the tab, the search box and the Directory filter every time, so it was a place you could never come back to — close it to read the row you just found and you reopened at the top of a list you had already filtered your way out of. Meanwhile the Chats bucket was saved as a per-MEMBER preference, so it came back weeks later on any device: a member who spent Friday afternoon in Support signed in on Monday to a panel still showing Support, with no way to ask for the front. One half never remembered and the other never forgot. Where the member is now lives in one per-tab store beside the pin, for the same reason and with the same scope **************** sign-out needs no code, app/logout/page.tsx already clears it. Every strip reads its own order for "the first one" rather than a default written beside it — DEFAULT_TAB is gone, the landing tab is TAB_ORDER[0], and a remembered sub-tab that no longer exists (a deleted market, a revoked Sales Scripts right) falls back to the front and is forgotten, so a reload does not bounce the member back to a tab that is not there. The maximized chat window's switcher column is deliberately untouched and keeps its saved member preference; BUCKET_MEMORY says which instance is which, keyed off the prop that already tells them apart. Verified: 7392 tests pass (593 files) with the build guards ahead of them, tsc clean. The new panel and store tests were mutation-checked — making the store forget everything turns 19 of them red — because a test that asserts "lands on the first tab" passes just as well against a panel that remembers nothing at all.

  2. · kamo-marketing

    Give the film a watch page, because Google will not index it from the homepage

    Search Console reports the one video on this site as "video is not on a watch page". That is not a bug in the hero — it is Google saying the homepage is about the product, the film is an aperture beside the copy, and there is no page it can index the video FROM. The remedy Google asks for is a page whose main content is the video, and a sitemap that names that page. The homepage reel is untouched. /watch, and what makes it a watch page rather than a page with a video: - The <video> ships its src and poster IN THE SERVER HTML — the MP4, not the HLS master, because a crawler fetching a master playlist gets a text file listing three more text files. `preload="none"` is what makes declaring the file honest: nothing loads until someone presses play. The adaptive ladder is a client-side upgrade with autoStartLoad off, so a visitor who never plays still costs nothing. - VideoObject markup with contentUrl, both real poster frames as thumbnails, the measured duration, and eleven Clip key moments whose `?t=` addresses genuinely start the film there. Verified with a listener installed before the page's own scripts: ?t=51, ?t=34 and ?t=12 begin playing at exactly those positions. - A <video:video> block on the /watch entry in sitemap.xml, which names the watch page in its own <loc> rather than leaving Google to infer it. - The player is the page: it takes as much of the first screen as a 16/9 box can without pushing its own controls off the bottom, and the row fills the full width at every size — chapters beside it from 900px, and what the film shows as a third column from 1700px. The chapter list is the film's own captions, read off the frames at 0:09, 0:12, 0:19, 0:34, 0:38, 0:41, 0:44, 0:49, 0:51 and 0:55. It is the transcript, the seek control and the Clip names from one array in app/lib/film.ts, which also holds every URL and the duration. One defect found on the way: Next serialises the sitemap's uploader tag as `<video:uploader${info && ` info="..."`}>`, so omitting `info` — which its type says is optional — interpolates the string "undefined" into the OPENING tag and xmllint rejects the whole document. One malformed entry does not cost you one URL, it costs you the sitemap.

  3. · kamo-internal

    Twelve endings, and every one of them looks different

    A hand ends for everyone at the table at once and it ends differently for each of them. Until now it ended one way: somebody won a pot, and the window played the same show for the winner, the player they had just broken, and the person who folded on the first street and had been watching since. Twelve outcomes now, classified from the settled frame: BUSTED your stack is gone KNOCKOUT you won and somebody left the table on it MONSTER quads, a straight flush or a royal — the HAND was the event SCOOP a big pot at a showdown WON an ordinary one STEAL nobody called SIDE_POT you took a side pot and the main went elsewhere CHOPPED you shared it COOLER you lost a big one LOST you lost an ordinary one FOLDED you stopped paying, which is a decision and not a defeat RAIL you were not in it The precedence is the design, not the conditions — most of them are true at the same time. Busting outranks a side pot you took on the way down. A knockout outranks the size of the pot, because the pot comes back next hand and the player does not. A monster hand outranks the pot for the same reason in reverse: quads for two big blinds is still quads. Every rung is a test, and each was proved by substituting the rung below it. Four new things fall, drawn from the objects on the table rather than from a party. Chips erupt out of the centre when you are paid, in real cardroom denominations with the dashed edge ring. Card suits tumble when the hand itself was the event. Embers rise and go out when you lost one. Ash falls when you are out of chips. The bust is the one deliberate contradiction in the whole table: the loudest intensity in the file spent on the quietest thing in it. An iris closes the room to near-black, the flash and the shockwave and the window shake are all suppressed, and ash falls through it for three seconds. Answering the biggest moment in poker with the most light is the obvious move and the wrong one. A ceiling per outcome matters more than the floor. An ordinary loss can never reach the volume of a win however large the pot was, because the person it happened to is still sitting there about to be dealt another hand. Three defects only measurement found, all of them "quiet" rather than broken: ash spawned up to a screen above the window and fell at eighteen pixels a second, so it never arrived at all — 0 lit pixels for the first 1.2 seconds of the loudest ending in the game. Embers were forty two-pixel dots across three quarters of a million pixels. Suits spawned half a screen up and left their own canvas empty for the first second. Found by counting canvas alpha per layer at four moments, per outcome; a screenshot shows all three as merely subtle. Embers also take their own palette rather than the room's: falling brass and bone said "you won" while the type said the opposite.

  4. · kamo-internal

    A side pot says who is playing for it

    The last "consider" left in the request, and the one question a short stack actually has: when the middle splits in two, both piles look like money and only one of them may be theirs. Hovering a pot now names its eligible players under the breakdown — `hand.pots[].eligibleSeats` resolved to names, with the reader shown as "You". Only when the middle has actually split: with one pot everybody still in is eligible and the line is noise. It rides in on a `footer` slot on the cluster tooltip rather than a pot-shaped special case, so any pile with a fact its denominations do not carry has somewhere to put it.

  5. · kamo-internal

    One table at a time, and a table you can name

    The felt half of the four rules MediaService started enforcing in a02dd1d. ONE WINDOW. The registry said `maxInstances: Infinity`, reasoning in a comment that a member can sit at more than one table. That is the thing that is no longer true, so a second window could only ever show a table its owner is not playing at. One instance also makes accepting an invitation work: openTool with a different tableId merges props into the window that exists and raises it, so switching tables reuses the felt instead of stacking a second one behind it. Pinned by a test, because nothing about a cap looks like a game rule from inside the registry — the next person tidying that file sees a number. A REFUSAL THAT IS REALLY A QUESTION. Sitting down while seated elsewhere comes back 409, but a member doing that is choosing a table, not making a mistake. So `join` is not `run`: run would turn it into a red sentence and play the failure tone at somebody who has done nothing wrong. It catches SEATED_ELSEWHERE, opens a dialog naming both tables, and the confirmation retries the SAME call with leaveOthers — closed over, so the dialog never learns whether it is confirming a seat or a new table. Branching is on `code`, never on the message: the message is prose the server may reword, and matching on prose is how a client stops offering the confirmation the day somebody improves the wording. RENAMING, for whoever opened the table. The field owns no name of its own — a controlled input holding table.name would fight the socket, and a frame arrives every time anybody acts, so somebody renaming mid-hand would watch their typing disappear as their opponents played. The draft is seeded once when editing opens. There is a test for exactly that, and it is the one that caught two others passing vacuously: assigning input.value does not reach React, which updates its own value tracker, so onChange concludes nothing changed. Every assertion expecting nothing to happen then passes for the wrong reason. The rename field is the first thing a member types into a poker window, so both draft-guard questions had to be answered. The control is transient — restoring a half-typed rename would bring the window back holding text its owner does not remember typing, over a name the server still says is correct. And closing the window does not stand you up: the seat, the chips and the hand are the server's, `leave` is only ever an explicit button, so a close discards a view of a game that carries on without it. 204 holdem+registry tests green, 807 across tools, tsc clean, and all three image-build guards pass.

  6. · kamo-internal

    The whole window celebrates when somebody wins

    A win used to be a sentence in a strip under the board. It is now the entire tool window: an impact flash and a shockwave, light turning behind the board, fireworks, two volleys of confetti, the word itself slammed into the middle of the screen, and the window taking the hit. Both people see the SAME show. The loser's window is not spared and is not muted — it plays the winner's fireworks, in the winner's colours, with the winner's name across the board they just lost on. The 'defeat' tone changes the words and the vignette and nothing else; softening it for the person who lost would be a different feature. The word is what actually happened, never a generic 'victory'. Chess says CHECKMATE or RESIGNED, out of the same endings vocabulary the result band uses. The felt says the number — what a poker player just gained or just lost is the amount, and it is dramatic at any size. Neither needs a new dictionary key. Scaled to the moment, because a table deals a hand a minute and the maximum show every time would be an obstruction. potIntensity() measures a pot in big blinds, logarithmically, so an ordinary pot is not flattened to nothing and a monster does not run away; chess is always full, because a correspondence game is days of thinking and exactly one winner. A pot you took yourself gets a floor under it either way. Two things only a real browser said: - The fireworks were FRAME-COUNTED, so the show ran at the display's refresh rate: measured at ~30fps headless, the first shell opened at 1.75s instead of 0.5s, by which time the confetti had already fallen. The same code on a 120Hz screen would have run the whole celebration at double speed. Fixed timestep now, with a catch-up cap so a tab that was away does not simulate a minute at once. Measured again by canvas ALPHA per layer: lit from 0.7s, carrying the back half while the confetti decays. - CHECKMATE at the size that suits 12,400 runs off both edges of a popup. The headline is sized against the WINDOW (container units, not vw) and divided by its own character count. The overlay is pointer-events: none throughout, so the rematch button under it stays live, and aria-hidden, because the result panel has already said it once. Reduced motion gets the moment without the movement: no canvases, no flash, no shake, the word simply appears and holds. Proved in Chrome under emulateMediaFeatures — 0 canvases, animation-name: none, opacity 1, and elementFromPoint still passing clicks through. The felt's guard walks only app/components/tools/holdem, so this brings its own, built so it cannot go vacuous: every walk that could return nothing is paired with an assertion that it found something, and all three ways to hide an animation from it were tried by mutation.

  7. · kamo-internal

    The Directory leads the tab strip, Chats follows

    Flips the two always-present tabs. The strip now reads Directory, Chats, Sales Scripts, Terminals. The panel still OPENS on Chats: the button that opens it is the Chat button, and landing on the Directory would put the common case — carry on a conversation you are already having — a tab switch away every time. So position and landing place are deliberately no longer the same tab, which is worth saying out loud because TAB_ORDER also feeds the slide transition's direction; it moves with the strip, so switching to the Directory from the opening tab now slides rightwards, as the eye expects. Verified in jsdom rather than by reading the JSX: a throwaway render of the real panel reported the tabs in DOM order as members, chats, salesScripts, terminals with chats still aria-selected.

  8. · MediaService

    No empty tables, numbered tables, and one table per member

    Four rules, all of them things a member reported seeing broken. EMPTY TABLES. A lobby showed three tables while one game was being played. Two had no seats at all: one created before opening a table meant buying into it, one emptied by a path that predated the check. closeIfEmpty already covers the two ways a seat goes away, and it is correct — the gap is that nothing ever looks at a table AGAIN once the last thing that cared about it stopped. findDueToDeal only sees tables with a deal pending and an empty table has none, so an empty table is not merely wrong, it is unreachable: no request, no timer and no member will ever cause it to be examined. HoldemTableReaper closes them every thirty seconds, and lobbyFor decoders to advertise a seatless table even in the half-minute before that runs. Closing the row is the fix; not offering it is the guarantee, because only one of the two can lose a race. The reaper publishes CLOSED, which is what sends a spectator back to the lobby. Spectators are members without seats, so they are exactly the people still pointed at a dead table and exactly the people no other path would tell. NUMBERED, NOT NAMED AFTER ANYBODY. "Sage's table" claims ownership of a thing that is shared the moment a second person sits down, and it was the same string three times over. Tables are Table 1, Table 2, Table 3, taking the LOWEST free number so closing Table 1 means the next table opened is Table 1 again — a lobby of Table 1, Table 4, Table 9 has been counting since the feature shipped rather than describing what is in front of you. Only OPEN tables hold a number and only exact names count, so renaming one gives its number back. RENAMING. Whoever opened the table, and nobody else. A name is the one thing here that is not a game action. ONE TABLE AT A TIME. Two seats means two thirty-second clocks running on one person, and the table they are not looking at times out hand after hand in front of people waiting on them. Refused on both sit and create — but refused with a QUESTION, not a wall: SeatedElsewhereException carries the table they are at, the client asks "leave Table 2 and join Table 4?", and the answer comes back as leaveOthers on a retry of the very call that was refused. One extra round trip, no extra endpoint. A distinct exception type rather than a recognisable sentence, because the sentence is translated and the code is not. A live hand is where the swap stops. Chips in front of somebody in a pot are already at risk and are not theirs to carry to another table, so they are told to finish the hand — a few seconds, against either a stolen pot or a seat they are not at. 22 new tests, and they are not vacuous: removing the naming, the one-table guard and the lobby filter fails seven of them, hitting exactly the ones that name each rule. 682 green overall.

  9. · kamo-internal

    A put-away table says you are on the clock

    The other half of the hexhead turn indicator. A minimized Hold'em window now lights its head while the action is on you, and drains the head's own outline — green, then amber, then red — as the clock runs down. Told from here rather than from a sync component beside HexHeadUnreadSync for the reason chess is: a minimized window is parked off-viewport rather than unmounted, so this component keeps running and keeps its subscription, and whose turn it is is a fact it already holds. A bridge would be a second copy of it, free to disagree. It also hands the head the tool's recovery for nothing — `holdem.member.*` has no stream behind it, so a frame published while the socket was down is simply gone, and the re-read on focus that fixes the felt re-dispatches this at the same moment. The DEADLINE travels, never a countdown: one event when the clock starts, one when it stops, and the head animates the seconds in between. A tick crossing this boundary would re-render the whole floating hexhead layout five times a second, for every head on a clock. Reads `table` and writes nothing to it — the felt already has four unordered writers and a `supersedes` guard keeping them in order, and this adds no fifth.

  10. · kamo-internal

    A minimized game says it is your move

    A head could already say a message had arrived. It could not say a game was waiting on you — chess tinted its border brass, which on a forty-pixel hexagon at the corner of the dock is not an event, and Hold'em said nothing at all. Heads now carry a turn. It is deliberately not an unread count: a number on a chess head reads as unread messages and there are none. What the member needs to know is that it is their move, which is a state rather than a quantity, so the head's own outline says it and breathes while it is true. Hold'em's turn has a clock, and the clock is drawn as that outline draining — green, then amber, then red — against a dim track. A head is a shape with a strong outline already; a rectangular bar bolted underneath would be a second object to find at a size with no room for two. **The countdown is a CSS animation, and that is the whole design.** The obvious build is a 200ms interval writing a fraction into React, which is what the felt's own ring does and is right there — it is on screen inside a component already re-rendering. A head is not. It floats over the application on a physics-driven layer that moves hexagons around, it outlives the window being minimized, and it is very often on a tab nobody is looking at. So the ring is one keyframe animation whose duration is the whole allowance and whose animation-delay is NEGATIVE by however much has already gone: the browser starts it part-played, runs it on the compositor, and keeps it exact whether or not anything re-renders and whether or not the tab is in front. A timer there would be a re-render of the whole floating layout several times a second, and Chrome throttles a hidden tab's timers to one wake-up a minute — a clock that stops when you look away and jumps when you come back is worse than no clock, because it is one you would have trusted. The head and the felt agree about WHEN each stage begins, because the seconds half of `clockPressure` is converted into fractions once per turn. They differ only in colour, and on purpose: the felt ramps through brass because brass is the table's accent, and a head sits on the application chrome with no card room around it to make brass mean anything. Two things a browser found that reading could not: - The step between colours was written as two keyframe stops at the same percentage. CSS keeps only the last rule for an offset, so the pair collapsed and the ramp became one long blend: at five seconds left the ring painted rgb(244,91,46), a mixture of amber and red rather than either, on the one part of the ramp that is supposed to be an alarm. - The "your move" ring stayed green underneath the countdown, so three seconds from a fold the head was a green hexagon with a small red tick on it. It is the track the clock drains against now, and dim. And the deadline PASSING is a state change rather than a comparison, because nothing re-renders when it passes and `holdem.member.*` has no stream behind it — a lost fold frame would otherwise leave a head breathing an empty red ring about a hand somebody was folded out of a minute ago. One timeout per turn. Hold'em's own dispatch is not in this commit: HoldemTool is held by another session. Chess is wired end to end.

  11. · kamo-internal

    The Show button, and the clock hook mounted

    The client half of showing a hand nobody made you show, plus the mount point for the turn-clock audio that landed dead in the previous commit. SHOWING OUTRANKS EVERYTHING ELSE THE STANDBY BAR OFFERS. It is the only thing there with a clock on it — a few seconds between hands and then it is gone — while sitting down, buying back in and topping up are all still there next hand. So when `you.canShow` is true it IS the bar, and the rest waits. The server decides whether the button exists, not the client: there is a window to do it in, the hand was yours, and nobody has seen the cards already. A control that appears and then refuses is worse than one that never appeared. The reveal needed no new machinery. A seat whose cards arrive on a completed hand already flips rather than appearing, because the two cards at a seat are two stable elements whose `faceDown` changes — measured in a browser: before the show, an observer sees the winner's seat with nothing face up; 120ms after, two cards face up with the reveal animation on both. NOT ALL OF THIS FILE IS MINE. It also carries the concurrent chip session's invite work — `applyTable`, the `keepInvitable` wiring, and the two readers that use the held list — because `git commit <pathspec>` commits the file rather than your diff of it, and three sessions want this one file today. They left it uncommitted on purpose rather than sweep up my half, which is the courtesy I failed to return earlier: their server-side `invitable` change went out inside my mediaservice 21ba0f2 under a message about showing hands. Recorded here so the history is legible to whoever bisects it.

  12. · kamo-internal

    Your own clock, out loud

    A thirty-second clock a player is not watching is a hand lost to a timeout while reading an email, and the felt's draining bar only helps somebody already looking at it. A member asked for a beep that accelerates and then a flat tone at the end, which is exactly the right shape: the last ten seconds beat once a second, then twice, then four times, and then the beats STOP and one sustained note runs to zero. The change from a beat to a tone is the signal — a rate you have to measure to read is not a warning, but "the beeping has become one sound" needs no measuring at all. ONLY EVER YOUR CLOCK. Ten seats mean ten clocks, and a table that beeps for all of them is a table nobody can sit at. `hold` is the first sustained tone in the palette, and it is four overlapping layers rather than one long note because the engine's envelope always ramps a layer to zero across its duration — there is no sustain stage. Each layer's linear fall is covered by the next one's attack. It is also the one tone allowed past the library's second-and-a-half limit, and that limit is right for everything else: a tone ANNOUNCES something, and something that has arrived is over, so a tone that outlasts its own event turns a burst into a drone. This one does not announce, it MEASURES — its whole job is to occupy the two seconds the beats left behind. The exception is named and bounded in the test, so the next long tone has to come and argue with the comment. The beats go to `startRepeatingEventSound`, which commits them to the AudioContext's clock: a JavaScript timer is throttled to one wake-up a minute in a page hidden for five, measured, and that is precisely the situation a warning tone exists for. The timers here only decide WHEN THE RATE CHANGES, as absolute moments computed from the server's own deadline — the same deadline the bar drains against, so the sound and the picture cannot disagree about how long is left. A turn that arrives with four seconds on it starts at four seconds' worth of urgency rather than replaying the calm ones. The hook is not mounted yet: HoldemTool currently also carries a concurrent session's invite work, and that lands first.

  13. · kamo-internal

    Every logo on the Branding card can be downloaded

    The download control shipped on one of the six artwork slots — the compact company logo — because the row was written inline at that one call site. The other five (company full, company tab icon, and the three software ones) kept a bare caption, so a member could look at artwork they owned and had no route to a copy of it: the assets sit behind theme-host URLs nobody has written down, and where the platform's own tracer produced the SVG, this screen holds the only vector that exists anywhere. The row is now one component used at all nine call sites (six in the editor, three in the read view), which is what stops the next slot from being added without one. It reserves the control's height whether or not the control is there — an IconButton with a 14px glyph is 24px against the caption's 20px, so before this a filled slot pushed its preview 4px below an empty neighbour's, measured at 117.88 vs 113.89 in a browser and level at 366.53 after. Filenames are stamped with the brand the slot belongs to rather than with the organization: both brands can carry the same name — a software slot with no software name set falls back to the company's — so downloading both simple logos otherwise delivered acme-simple-logo.svg twice and let the browser rename one "(1)". A tab icon keeps its own noun, since it is generated from a logo but is not one. The control is hidden rather than disabled on an empty slot: there is genuinely no file to ask for, and the placeholder and Upload button already say so.

  14. · MediaService

    Show the hand nobody made you show

    A pot taken with everybody folding is the one hand at the table whose contents only its owner knows, and there was no way to say what it was. Showing a bluff is how a table learns to read somebody, and a game where that is impossible has taken away half of what the next hour is about. A member asked for it. An ACTION rather than a setting, because it is a choice — and guarded on all four of the things that make them your cards to show: the hand is over, you won something in it, you were never made to show (a real showdown is not this), and the window has not closed. Nobody shows anybody else's hand, and nobody shows one from a hand still being played, which would be telling the table what you hold while there is still money to be won with it. TWO WINDOWS, and the second is counted from the right end. A hand that ended uncontested waits four seconds instead of five, because there is now a decision in the gap. Showing then resets the next deal to three seconds FROM THE MOMENT OF SHOWING — somebody who takes three of their four seconds to decide would otherwise reveal a hand that is swept a second later, which is the same as not showing it. A name only when there is a board to make one from. A hand that ended pre-flop has no ranking anybody could check, and inventing one would be the felt making poker up; a hand that ran to the river gets the same sentence a showdown would have given it. `you.canShow` asks the same four questions in advance, so the button appears only when pressing it would work. A control that appears and then refuses is worse than one that never appeared. Nine tests. Two fixture bugs on the way through, both of which made correct code look broken: a seat stub that re-stubbed rather than accumulated left the winner with no chair, so the view answered as if they were a spectator — which it should.

  15. · kamo-internal

    A table that has ended sends you to the lobby, with the news

    An invitation card outlives the table it points at. Somebody follows one ten minutes later, the game is over, and the window answered with a red error bar over an empty list — the truth, and nothing anybody could act on. It lands them in the lobby now, with the list already loaded and a line saying which of the two things happened: the table ENDED (it existed, everybody stood up), or it is GONE (the server will not answer for it at all, which from an invitation card is the same news said less precisely). Underneath that are the only two things still available — open one yourself, at a price it names, or take a seat at something already running. Three bodies rather than one, because "or take a seat at one below" is a lie when the list is empty and "open a new one" is a lie when they cannot afford it. In that last case the notice offers no button at all and says what a table costs against what they have, which is the answer to the question they were about to ask. A SPECTATOR IS THE CASE THIS EXISTS FOR. Somebody watching from the rail has no seat to be removed from, so when the table closes the frame is the only thing that can tell them — and it is the last frame that table ever sends. Every reader of that frame now checks the status: the socket message, the refresh on focus and on reconnect, the re-read after a result lingers, and the open-by-id on mount. Checked BEFORE the frame is applied, because a terminal state is not something the ordering rules should be allowed to reject as stale, and latched so several frames announcing it produce one exit. Opening a table costs the buy-in now, so the button says so — "New table · 20,000" — and is disabled when the reader demonstrably cannot afford one. That needed the balance, which the lobby's rows have never carried: a row is a summary and deliberately pays for no wallet round trip of its own, so it rides on the ENVELOPE instead. It is optional in the type: a client can deploy before the server, and a missing balance is unknown rather than nothing. While the notice is up the header's own New table button goes quiet — the notice is asking the same question, and two identical brass buttons one above the other is one decision too many. Measured in a browser at 560 and 420, in all four states (no notice, ended with tables, gone with none, ended-and-broke): no page overflow, nothing off an edge, no page errors, and exactly one primary action on screen at a time.

  16. · MediaService

    Opening a table buys you into it, and the last one out closes it

    Both ends of a table's life allowed the same thing: a table with nobody at it. OPENING ONE COST NOTHING AND SEATED NOBODY. A host could put a table in front of the whole organization without taking a seat at it, so the first colleague to accept the invitation arrived alone at a game whose creator was not playing — and waited for a second player who had never intended to be one. The host now takes seat zero and pays the buy-in in the same transaction. The charge runs LAST, after every other write, because it is the only step that leaves the service and a rollback cannot un-debit a wallet; if it is refused, the whole transaction goes with it and there is no table left behind advertising a seat nobody bought. Invitees are still asked rather than billed — being in a chat where somebody started a game is an invitation to play, never a commitment of your money. AND A TABLE EVERYBODY LEFT STAYED OPEN FOR EVER. It kept its row in every colleague's lobby, offering a seat at a game that had finished. `closeIfEmpty` runs on the two paths a seat can actually go away — standing up, and the delayed stand-up a player who asked to leave mid-hand gets when the hand settles — and a closed table drops out of the lobby, which reads only OPEN tables. There is no third path: a busted player keeps their seat and is offered a rebuy. Closing publishes CLOSED, which is the point of it. A player who stands up walks themselves back to the lobby; a SPECTATOR has no seat to leave and would otherwise sit at a felt that will never send another frame. When the table empties as a hand settles, that one frame carries both — the result and the status — rather than showing somebody a finished hand and then separately telling them the table it was on no longer exists. The lobby also answers with the reader's balance now, as a RECORD rather than a map: opening a table costs money, so the button can name its price and refuse in advance instead of sending somebody through a server error to find out — and `Map.of` throws on a null value, which would have made the one reply whose normal answer can be absent ("the wallet did not answer") the one that 500s. Null and zero stay different answers: null disables nothing, because telling somebody they are broke on the strength of a failed network hop is worse than letting them try.

  17. · kamo-internal

    The captures get the room the notation was taking

    Two things the member asked for after seeing it. **The move list is gone.** Nobody was reading a column of notation; what two colleagues playing each other want on screen is what they have taken and what has been taken from them. So the shelf gets the row under the board in every stacked layout rather than only the narrowest one — two labelled shelves, the pieces standing on a hairline at a size worth looking at, and the lead in brass beside whoever is ahead. The position review went with it, because the list was the only way to choose a ply and machinery nothing can reach is worse than no machinery. `reviewFen` is off the board, `GET /games/{id}/moves` keeps existing on the server but the browser no longer calls it, and the per-side think time now comes off the frames instead — the interval between two published positions, which is the same figure without needing a list to read it from. **A column beside the board is a maximized-only thing.** It was appearing in every window 520 pixels or wider, which is most of them. The board is square, so the 236 pixels that column took came off its HEIGHT as well as its width: a visibly smaller board in a window that looked no fuller for it. In a popup the furniture goes above and below now, where the space is free — a board is square and a window is not, so there is always room there and never room beside it. That rule is `chessLayout.ts` rather than a condition inside a `sx` prop, because both halves of it have been wrong once: the column appeared where there was no room for it, and the furniture's SIZE was derived from the same condition as its placement, which gave a 700x1000 window a docked rail's furniture. jsdom does no layout at all, so a pure function that a test can argue with is the only place this can be held.

  18. · kamo-internal

    The organisation's colour, and chips in the compact pot

    Finishes two things the chip subsystem had built and not wired. THE BRAND, where a card never sits. The betting line, the wash under the pot and the lift under the pointer are the organisation's own colour now. The chips are not, and never will be — a brand mixed into a denomination gives some organisation two chips nobody can tell apart, which is the rule `holdemTheme.ts` exists to state. `brandAccent.ts` was already written and proved; this is the call site. CHIPS IN THE COMPACT POT. The rail layout was the one surface in the tool with nothing in the middle. It draws `hand.pot` — the chips already collected — and NOT `totalPot`, because the difference is the bets still drawn in front of the players in the rail above it, and drawing those here as well would put the same chips on screen twice. Its three chip surfaces have accessible labels now; a pile of chips said nothing at all to a screen reader before. Also: a player's own chips stack twenty to a column rather than twelve. Cut at twelve, a full 20,000 buy-in came out as two squat blocks that read as "some chips" rather than as twenty of them — the height IS the information, and halving it to save width threw the information away. And the chip hooks read the org context WITHOUT throwing. `useOrgContext` throws with no provider, and a chip is a leaf that turns up where an org context does not — a test rendering one seat, a harness, a preview. A chip with no mark is a chip; a chip that throws is a blank table. Measured at 1400x820, 1180x760, 1000x660, 960x600 and 880x540 on the felt, and 420, 460, 560 and 860 wide on the rail: nothing past an edge, no page overflow in either axis. 7,191 tests pass.

  19. · MediaService

    The plies, and how long each one took

    Two things the move panel needs, neither of them on the published frame. `GET /games/{id}/moves` hands over every ply in order. It is a separate read rather than a field on `ChessGameView` on purpose: the move list and the per-ply positions used to ride on every publish, several kilobytes per recipient per move, for a panel nothing rendered. They came off the frame and they stay off it — the window fetches this when it opens and when the game ends, and appends each live move from `lastMoveSan` in between. Authorized by handing the id to the same `require(...)` the board itself uses, because an endpoint that hands over the whole course of a game must refuse exactly the people the board refuses. `THINKING_MS` has existed since the table did and nothing ever wrote it, so every row in the database carries a null and the panel had a time field it could never fill. `applyMove` is the one place the answer is knowable: the position last changed when the previous ply was written, or when the game was created if this is the first move. Not `dateUpdated`, which looks like the same fact and is not — `@PreUpdate` stamps it on every save, so a draw offer or an opponent opening the board would reset it and the next move would report a think time of a few seconds. Recorded, not enforced. There is still deliberately no clock.

  20. · kamo-internal

    The board is a table now

    The first move of every game was wrong. Pieces were keyed by their position in a FEN scan, and scan order is not identity: after 1.e4 the element that had been on a2 was told to travel to e4 while four of its neighbours each slid one file left, and the piece that faded out on a capture was whichever happened to be last in scan order rather than the piece that was taken. Everything else here is downstream of fixing that. `chessTransition.ts` takes identity from the MOVE, reads what kind of move it was off the two positions, and then PROVES it: the derivation renders a predicted board and compares it against the authoritative FEN, and a mismatch means snap rather than animate. That one check collapses a missed frame, a reload, a rematch and a game opened halfway through into the default path. It is replayed over 130 plies of three real games out of the live database, 43 of them captures, and every one is understood. On top of that identity: a shadow layer under the pieces that separates as they lift, lift-travel-settle with a heavier spring for a capture, a rook coordinated with its king, an en-passant victim that leaves from its own square, drag and drop that follows the pointer with no spring in the way, a refused move that shakes the square it came from, and a check ring that pulses three times and then HOLDS. Every duration lives in `chessMotion.ts`, and a test fails the build on any component that invents one. The pieces are lit rather than filled — one renderer clipping a gradient, a specular and a contact shadow into the piece's own silhouette, so a different set drops in by supplying paths and nothing else. The move list is back without going back on the reason it was removed: a new read endpoint, fetched twice and appended locally in between, so a published frame costs exactly what it did before. Clicking a move reviews that position and cannot touch the live game. Every square is a real labelled button now — "e4, White pawn" — with arrow-key play, a live region that says each move in words, and a promotion card reachable from the keyboard. Two defects found by measuring a rendered board rather than by reading it: - With a near-black tenant brand the Kamo scheme's dark square resolved to luminance 0.136 against its own black piece at 0.097. Making the PIECES literal fixed half of this last time; the square moves too. Both ends are bounded now, and because `color-mix(in srgb, ...)` is plain linear interpolation — predicted 0.413, Chrome painted 0.413 — the whole family of tenant boards is a unit test instead of a comment saying a browser is needed. - `applyView` is the only way a published state reaches the board now, and it drops a frame older than the one on screen. One human move against the computer publishes two frames seconds apart while the HTTP response returns the later of them, so the response could land first and the intermediate frame arrive after it — the board jumping forward and then visibly snapping back. Deliberately not built: clocks. `ChessGame` says there are none on purpose and the invitation copy says "take days if you like"; the brief conditions every clock section on their already existing. What those sections actually want — knowing whose turn it is, and the weight of the moment — is the arriving YOUR TURN, the active seat, and per-move think time, which the ply rows have always had room for and nothing had ever written.

  21. · kamo-internal

    The chips are objects on a table

    A member asked for the poker chips to stop being decoration. They are a subsystem now: real denominations, real stacking, racks, a betting line, pots that add up, and chips that travel between them. WHAT A CHIP IS. `chipDenominations.ts` is the whole economy — the standard casino ladder **************** the set a TABLE deals in derived from its stakes, and one exact breakdown function everything else asks. At 100/200 with a 20,000 buy-in that set is 1K/500/100, so a full stack is one rack of yellows, which is what that game looks like in life. Greedy down the ladder is also MINIMAL here, which is a fact about this ladder rather than about greed — greedy makes six out of 4+1+1 on a 1/3/4 system — so it is proved by exhaustive comparison against a dynamic program rather than assumed. WHAT A CHIP LOOKS LIKE. An SVG seen from a player's own eye level: a top face with a rim, edge spots and an inlay, and a side WALL. A stack is one wall per chip and exactly one face, which is both how a real stack reads and why a hundred chips in a rack is one DOM element. The organisation's Simple Logo is the inlay — in its own colours on the white chip, and as an alpha-preserving white silhouette on every other, because a coloured mark on a coloured chip is unreadable on half the ladder. This organisation's logo is a 3.5 MB, 1,494-path photo trace, so it is rasterised ONCE per tab into two ~10 KB data URLs that every chip shares. WHERE A CHIP GOES. `chipGeometry.ts` puts a betting line on the cloth: chips outside it are yours, chips inside it are the pot's, which is the affordance a real cardroom uses and needs no legend. Placement is by CONSTRUCTION, not by tuned constants — a seat cluster is a fixed 110x91 pixels while its position is a percentage, so as a fraction of the felt it GROWS as the table shrinks, and every set of constants that looked right at 1180 was wrong at 880. Each pile is walked out until it clears its own plate, the board, the edge, its neighbours, and the halfway bearing to the next player. WHAT MOVES. `useChipChoreography` diffs consecutive authoritative frames and `ChipFlightLayer` flies the difference: the chips a player pushed forward, the street swept in, each pot carried to the winners the server named, an uncalled bet going home. Animation is presentation and the frame is the truth — every visual hold carries a deadline enforced by a scheduled STATE CHANGE, never by a `Date.now()` comparison during render, which is a moment that never arrives. A fresh mount holds nothing, so a reconnect snaps. Mid-hand the middle draws `hand.pot` and the bets draw `streetCommitted`; drawing `pots[]` there would put the same chips on the felt twice, because the server recomputes those from every committed chip. Also here: the seat ring moved in, because a 91px cluster at 5% of a 508px box hangs six pixels off the top of the window; the board shrinks with the table for the same reason; and `holdemMotion.test.ts` now reads the COMPONENTS rather than the stylesheet — the old assertion scanned the half of the sheet before the media query, which holds only @keyframes bodies, so it collected an empty set and its loop never ran once. Rewritten, it found four animations running for people who had asked for none. Measured in a browser at 1400x820, 1180x760, 1000x660, 960x600 and 880x540: no pile over a plate or a card, no pile pointing at the wrong player, no clipped cards, no page overflow in either axis.

  22. · kamo-internal

    The table says what just happened

    The felt re-rendered a STATE. It never presented an EVENT. Of twenty-six things that happen in a hand of poker, exactly two had any presentation at all — a card mounting, and the lamp brightening on an all-in — and a check, a call, a bet, a raise, a re-raise, a fold, a street change, a split pot, a player joining or leaving existed only as a value quietly changing, or as a line in a history panel that is maximised-only and behind a toggle. A member who looked away for two seconds could not find out what they had missed. THE ARCHITECTURE. holdemEvents.ts turns two consecutive authoritative frames into the list of things that happened between them. Pure — two frames in, a list out, no React and no timers — and idempotent by IDENTITY rather than by bookkeeping: every event carries an id derived from the state that produced it (`{handId}:a{seq}`, `{handId}:flop`), so a re-render produces nothing, a repeated frame produces ids already seen, and a window that missed three frames produces the three missing events in order. Actions are read out of hand.actions, which is the whole list rather than the last thing that happened, so catching up is reading forward. The first frame a window ever sees produces NOTHING: a window opened halfway through a hand has not just watched four people act. useHoldemChoreography.ts turns those into decorations, and the rule it is built around is that AN ANIMATION IS NEVER ALLOWED TO DECIDE ANYTHING. Every value decorates a felt already rendering whatever the last frame said. A late packet, an interrupted animation, a minimised window, a throttled tab, a reconnect — the felt is still correct, only less decorated. Decorations carry the moment they stop being true and ONE timer is armed for the earliest of them, so ten labels cost one timer, an unmounted window costs none, and anything whose moment passed while the tab was frozen is simply gone rather than running late. WHAT IS PRESENTED. A compact broadcast-style label beside the seat that acted — CHECK, CALL 600, RAISE 900, RE-RAISE 2,700, FOLD, ALL IN 12,300 — in five treatments across three tiers, so a table is not equally loud about a check and a shove. The street's name sweeping across the felt as the cards arrive. Board cards that TRAVEL, from the deck beside the dealer, per street (the flop's three stagger; the turn and the river no longer wait 165ms and 220ms alone for nothing). A turn handoff that runs from the seat that was on the clock to the seat that is on it now, so the eye is led round the table. A dealer button that WALKS. Four levels of clock pressure instead of two. A showdown that turns hands over one at a time, in dealer order from the button, then names the winner and the hand — translated, and with the five cards that actually won it lit. Per-pot resolution, because two winners of two different pots is not a split and saying so implies one of them beat the other. A fold that mucks rather than dimming. An elimination that reads differently from a sit-out. A local echo when YOU click, replaced by the server's own frame and expiring on its own clock if that frame never comes. AND SOUND, through the app's own catalogue rather than beside it: a `games` category and nine holdem events, configurable at the platform, organization and member layers like everything else that makes a noise. holdem.card.dealt alone has no throttle — a flop is three cards 95ms apart and any throttle worth the name turns a deal into a click. FOUR DEFECTS FOUND ALONG THE WAY, three of which only a browser could show: A showdown never flipped. Seat.tsx rendered face-down and face-up cards as two BRANCHES, and React swaps a branch by unmounting one and mounting the other — so the 3D flip PlayingCard was built for never ran at the one moment in poker worth animating. Two stable elements whose faceDown changes is the whole fix. A "not before" moment that never fires. The verdict and the per-seat reveals were read as `Date.now() >= at` at render time. The timer armed for them fired, found nothing expired, returned the same state object — and React, correctly, did not re-render. The winner was never named and the cards never turned over. Both are state changes now. Every unit test passed throughout, because a test that asks the hook a question calls it and gets the answer. A callout above the top three seats went off the felt entirely; a browser put one at y = 0. Which side it goes is a fact about where the seat is drawn. ActionBar's "call all in" label sat behind `callAmount >= toCall && callAmount < toCall`, which cannot be true. A player calling for less than the bet — going all in — was told they were calling. The raise control now says what actually LEAVES YOUR STACK as well as the total for the street, because a big blind raising to 600 is putting in 400 and every client that shows only the first number has made somebody shove by accident. Verified in a real browser at 1440/1200/960/880 and in the compact layout, with nine maximum-length labels on screen at once: nothing off any edge, nothing covering a name, no page overflow in either direction. Under prefers-reduced-motion, measured: no animation running at all, every callout still readable at opacity 1 with no transform, the decorative bloom and flare gone. Somebody who asks for less motion is asking to be told without being moved, not to be told less.

  23. · kamo-internal

    A poker table in a tool window, and Cheddar in the nav bar

    Two genuinely different layouts, a lobby, an invite path that reaches anyone in the organization, and the wallet readout that appears whenever Games does. ## The two layouts are two designs, not one design resized Chosen by the window's SHAPE — read from the real viewport when maximized, because `width` is the STORED size and a maximized window still reports it, so a width breakpoint is silently false for a maximized window docked at 460. compact a rail seat: a scannable strip of opponents, the board at full size, and YOUR two cards large at the bottom where your hands would be felt the table: ten seats on an ellipse, rotated so the reader is always at the bottom, with the hand history and empty-seat invites beside it At 460px an ellipse of ten seats is ten unreadable specks, so the compact layout is the right design for that shape rather than a fallback. ## The design A late-night private card room, not a casino floor: bottle-green cloth, oxblood, brass, and one warm lamp over the centre with the board and pot inside it. The lamp BRIGHTENS while somebody is all-in — the only ambient state change on the table, and the whole of the boldness budget spent in one place. NOTHING on the felt is brand-derived, and that is a rule. Chess derived both the square and the piece from `--color-primary` and produced, on a mid-grey brand, a black piece measurably the colour of the square under it. The felt has more exposure, not less. The brand appears only in chrome, where a card never sits beside it. The 52 faces are generated from a rank/suit spec rather than drawn, so the same component is the 38px card at a seat and the 70px card in your hand — and the pip LAYOUTS are the standard ones, because a player reads a seven by its shape long before they read the corner index. ## The browser owns no poker Every frame carries `you.legalActions` with exact minimum and maximum amounts, the way chess carries `legalMoves`. The action bar cannot disagree with the server about a minimum raise because it was never asked. Amounts are TOTALS for the street — an increment has to be interpreted against a bet that may have moved between the frame being drawn and the click landing. Hole cards a member is not entitled to never enter a frame addressed to them, so there is no client flag that could reveal one. ## The wallet A 🧀 chip in the fixed right-hand chrome, drawn on exactly the condition `useGamesSections()` draws the Games menu on — one source, so the two cannot drift. Click opens a statement covering the WHOLE wallet: land purchases and claw plays alongside buy-ins, because it is one wallet. It ticks live during play with no request of its own: every Hold'em frame carries the reader's balance, dispatched across the tool-dock boundary as an event. An unreachable wallet renders as an em dash and never as zero — zero is a fact about somebody's money and null is a fact about the network. ## Also * `gameCatalogue` grew from an exact `participants` count to a RANGE, so a group chat now offers something where it used to offer nothing. * KAMO_UNIVERSE and CHESS had no `appServiceIcons` entry either, so all three Games children drew the anonymous app-grid square on the Features screen. Fixed with them. * The compact rail asks for no scrollbar of its own — `data-kamo-scrollbar="off"` is the app's one vocabulary for that, and styling a bar here would make it the app's second.

  24. · MediaService

    A server-authoritative Texas Hold'em, and the Cheddar wallet seam

    Up to ten players a table, started from any conversation, with anyone else in the organization invitable into it. Every rule lives here; the browser owns none. ## The engine is pure Java and it is proved by enumeration HandEvaluator ranks all C(52,5) = 2,598,960 five-card hands in its own test and checks the count in each category against the textbook distribution — 40 straight flushes, 624 quads, 3,744 full houses, 5,108 flushes, 10,200 straights, 54,912 trips, 123,552 two pair, 1,098,240 pair, 1,302,540 high card. That is the perft of poker: an evaluator that ranks the wheel ace-high, or reads a kicker off the wrong slot, passes every hand anybody would think to write down and fails here, because the miscounted hands have to go somewhere and the totals stop adding up. PotSolver's randomised test found a real defect before this shipped: with two folded players committed above the last player standing, 102 chips vanished into neither a pot nor a refund. An uncalled bet needs TWO caps — nobody contests more than the largest anybody else put in, AND nobody contests more than the largest any player still in the hand put in. A solver that destroys money in a state it believes unreachable is the bug that surfaces the day it turns out to be reachable. HoldemEngine gets the three rules everybody gets wrong: the big blind's option (posting is not acting), an incomplete all-in NOT re-opening the betting, and a minimum raise that tracks the last raise rather than the blind. ## Chips are not cheddar HOLDEM_SEATS.STACK moves on every action; the wallet moves exactly TWICE per seat. Charging per bet would put a cross-namespace round trip inside a thirty-second action clock and turn one evening into four hundred unreadable ledger rows. The buy-in is two-phase: the seat row is written first and the idempotency key **************** is DERIVED from it, so a retry after a crash, a timeout or a duplicate click is a no-op rather than a second $20. HoldemSettlementSweep finishes any seat left half-bought, which is safe precisely because the key is derived and not generated. ## holdem.member.{memberId}, and why it could not be per table Every seated player needs a DIFFERENT redaction of the same table — their own two cards and nobody else's — and a railbird a third. A per-table subject can carry one payload, so it would contain either everybody's hole cards or nobody's; neither is poker. Eleven recipients mean eleven frames, each built for the person it is addressed to. The SEND half of StompDestinationAuthz matters more here than anywhere: a frame carries hole cards and legal actions, and the felt draws whatever arrives. ## Deadlines are data ACTION_DEADLINE and NEXT_DEAL_AT are columns, enforced by a one-second sweep on every pod with conditional updates guarded by the hand's action sequence. Two pods racing produce one fold; a player who acted a millisecond early is folded by neither; a pod dying mid-hand cannot freeze nine colleagues. Tests: 35 across the evaluator, the solver, the engine, the topic guard, the idempotency keys, and a boot-time compile of every hand-written @Query — a malformed one crash-loops the pod and nothing else catches it.

  25. · kamo-shared-library

    The app, the right, and the six tables a poker game needs

    HOLDEM (36) joins KAMO_UNIVERSE and CHESS under GAMES, with PLAY_HOLDEM (319). Both ids were the next free ones; nothing is renamed and nothing moves, because ORG_FEATURES persists a ServiceType id and **************** persists a RoleRightType NAME. COMPLETED + enabled, like its siblings: AppCatalogService derives availability from the stage and FeatureController's auto-provision loop skips an unavailable app, so a lower stage would ship an app no organization could be given. The six entities: HOLDEM_TABLES the room, which outlives every hand played in it HOLDEM_SEATS one occupied chair and the chips in front of it HOLDEM_HANDS one hand, deck and all HOLDEM_HAND_SEATS a player's part in one hand, as a snapshot HOLDEM_ACTIONS append-only: everything anybody did, in order HOLDEM_TABLE_MEMBERS who may open this table — invitation list and rail roster Three shapes worth reading the javadoc for: * HOLDEM_SEATS.STACK is CHIPS, not cheddar. The wallet moves exactly twice per seat; everything between is a local transaction. BUY_IN_SEQ exists because the wallet's idempotency key is DERIVED from the row, so a retry after a crash is a no-op rather than a second $20. * HOLDEM_HANDS stores the WHOLE shuffled deck. Storing only the board would mean re-shuffling to deal the turn, and a player all-in on the flop is entitled to have that card already exist. * ACTION_DEADLINE and NEXT_DEAL_AT are COLUMNS. A timer in one pod's heap dies with that pod and takes nine colleagues' evening with it. Every enum column is @Enumerated(STRING): an ordinal column freezes a CHECK constraint at today's size and fails the day a value is appended. NotificationKind gains HOLDEM_INVITE — its own kind because the kind is the unit a member mutes, and "stop inviting me to card games" should not also silence everything else.

  26. · kamo-internal

    The chat entry opens the pair's board, and you can ask an opponent back

    Two colleagues who have played once have a board together for good, so the chat entry stops offering to introduce them. It reads "Open your game" and goes straight there — a finished game opens on its final position with Play again on it. Only a pair who have never played are offered a start, and only they see the confirmation. "Game in progress — open it" described a state; this describes what the click does, which is the same thing whatever the state. The nudge is a megaphone beside the other controls, and it appears only where somebody could actually be away: a game in play, against a person, and not your turn to move. Inside its cooldown it dims and says when it opens rather than vanishing and leaving somebody wondering whether they imagined it — and it stops lifting on hover, because a control that will not respond should not promise it will. The countdown ticks once a second and ONLY while a cooldown is running, so a member with nothing to wait for pays for nothing. It is a courtesy, not the limit: the limit is the server's, and it refuses regardless of what the button believes. silently taken the last one and it would have worked, which is exactly why the dead first declaration was worth removing rather than leaving for somebody to puzzle over.

  27. · MediaService

    The chat entry is a door to the pair's board, and you can ask them back

    Two colleagues who have played once have a board together for good. So the chat entry stops offering to introduce them: `gameWith` answers with the SHARED game — live if one is running, otherwise the last one they played — and a finished game opens on its final position with Play again on it. Same door either way, which is what makes "chess with Daniel" one action rather than a question about state. A DEcoderD row is excluded: an invitation somebody turned down is not a game they have, and treating it as one would point a pair permanently at a board neither ever used. And a way to ask them back, rate-limited to one every five minutes. The clock is the SERVER's, against DATE_LAST_NUDGE: a cooldown that lives in a button is not a cooldown, since a second window, a reload, or anybody holding the endpoint gets round it. 429 when it is too soon, and the view carries when it next opens so the button can count down honestly. One clock per GAME, not per sender, so two people cannot take turns. The automatic your-turn line stamps the same clock — otherwise a member could follow it a second later with a near-identical one of their own. Nudging the computer is refused rather than silently ignored. It is never away from its desk.

  28. · kamo-shared-library

    Record when a player was last prodded to come back

    DATE_LAST_NUDGE, one clock per game rather than per sender — otherwise two people can take turns nudging each other every five minutes and call it twelve messages an hour.

  29. · kamo-internal

    A capture shelf where the move list was, and a hexhead that says it is your turn

    THE SHELF. A move list is a record; between two colleagues playing for fun the pieces you have taken are the thing worth looking at, so they get that space and the notation goes. They stand at the edge of the table now rather than being a row of 14px marks squeezed into a name rail — a hairline under each row and a soft ellipse under each piece, so they read as objects set down. A new capture drops in and settles, which is the one moment in this window worth animating, because taking a piece is the thing that just happened. The lead is a brass numeral carried only by whoever is ahead. Their haul sits near their side of the board and yours near you: the shelf mirrors the table. Removing the list removed the position review with it — there is no way to choose a ply without a list of them — so the review state went too rather than being left as machinery nothing can reach. THE HEXHEAD. A minimized head turns brass when the board is waiting on you, driven by the same STOMP frames the board is: `yourTurn` arrives with every published state, so the head changes in the instant the board would, with no second subscription and no poll. It lives in the tool rather than beside HexHeadUnreadSync and HexHeadTerminalStateSync because those bridge facts the tool cannot see — conversations, terminals — and whose turn it is is a fact this component already holds. A bridge would be a second copy of it, free to disagree. A colour rather than a badge: a number on a chess head reads as unread messages, and there are none. The test found the defect in the first version. Brass was #C9A227 against a resting border of #d97706 — both ambers, 0.095 apart in luminance, so "your move" was orange going very slightly lighter orange. The resting head should not already wear the alert colour; it is a quiet warm stone now, 0.337 apart, and the test holds them there.

  30. · kamo-internal

    The board you play on now lives in the same room as the pane that started it

    You chose a game in a dark room of board lacquer and brass, pressed start, and landed on a white MUI form with a board floating on it — `var(--surface)`, `divider`, `text.secondary`, every default. They were two different products. chessSurface.ts is the one palette both read from, so they cannot drift again. The colours are the object's own: a tournament roll-up board is dark green and buff rather than black and white, and a chess clock has brass trim on a felt base. None of it is brand-derived — the BOARD carries the member's chosen scheme; the room around it stays the room. A rail per player, opponent above the board and you below, which is where the two players actually sit. Each carries a disc in that player's piece colour, so "which side am I" needs no words; the name; the head-to-head in brass; the captured pieces and the material lead. The border and the disc light brass when the board is waiting on that player, with a pip that pulses — a window sitting behind three others is read at a glance or not at all. Three things that are not styling: - The brass hairline and shadow are on the BOARD, via a `framed` prop, not on its container. Built as a frame around the area first, and since the area is never square the frame floated away from the board it was the edge of. - A finished game is a card over the final position — the result, how it ended, and Play again — rather than a status line quietly changing. The board stays visible underneath, because the final position is the thing you want to look at. - Captured pieces are ivory silhouettes. They were drawn in the BOARD's piece colours, so the classic scheme's near-black captures sat on a near-black rail and the trays looked empty in exactly the games where somebody was winning. A tray only ever holds one side's pieces, so the fill has no colour to encode.

உங்கள் உலகம் காத்திருக்கிறது.

நிமிடங்களில் இலவசமாகத் தொடங்கவும். நீங்கள் தயாராக இருக்கும் போது பொருட்களை சேர்க்கவும் - எந்த மாற்றமும் இல்லை, புதிய உள்நுழைவுகள் இல்லை.

எப்பொழுதும் இலவசம் — கடன் அட்டை தேவையில்லை