diff --git a/.changeset/rail-cell-rings-and-blink.md b/.changeset/rail-cell-rings-and-blink.md
new file mode 100644
index 000000000..1d9a314e6
--- /dev/null
+++ b/.changeset/rail-cell-rings-and-blink.md
@@ -0,0 +1,5 @@
+---
+"@internal/server": patch
+---
+
+Two rulings on the rail's cells. Every open pane now wears ONE thin selection ring, the focused one included: the wider focus ring read too heavy at cell size, so focus and set share the soft single line. And the working cell retired its bright-green beat: it now pulses the half-green fill behind a constant white letter, and a cell with no selection ring keeps a thin dark frame so the empty half is still a square, never a floating glyph.
diff --git a/apps/server/web/docs/sidebar.md b/apps/server/web/docs/sidebar.md
index b44fc787f..90f119705 100644
--- a/apps/server/web/docs/sidebar.md
+++ b/apps/server/web/docs/sidebar.md
@@ -86,9 +86,12 @@ size, never a second encoding**: fills from `DOT_CLASS`, the bell from
`BELL_TONE`/`showsBell` (exported for this), the row's gestures whole on one
element, and the same reveal tooltip, opened BELOW the cell (a right-side
popup lands on the next cell in the rail), labels bolded by
-`TooltipLabelledLines`. Its working blink is layered (plate, fading fill,
-glyph-sized letter chip): at 24px both the dot's vanishing act and a dark
-letter on the bare plate read broken. Cells wear their PANE's initial, a
+`TooltipLabelledLines`. Its working blink pulses the FILL only, the
+half-green idle tile fading in and out behind a constant WHITE initial (the
+solid-green beat and its dark knockout letter were retired 2026-09-27). On
+the empty half the square stays there: an un-ringed working cell carries a
+static `--border` frame the fill pulses inside, while a selected/focused one
+needs no frame under its ring. Cells wear their PANE's initial, a
hint never a key, and the flat plates are FNV-1a over the machine NAME into
eight theme-tuned `--node-tint-*` tokens (278–332, the canvas hue is 296):
pure function, no per-machine state, a rename can move a colour, two
diff --git a/apps/server/web/src/components/sidebar/__tests__/rail-subshells.test.tsx b/apps/server/web/src/components/sidebar/__tests__/rail-subshells.test.tsx
index a18422eb0..29dee140a 100644
--- a/apps/server/web/src/components/sidebar/__tests__/rail-subshells.test.tsx
+++ b/apps/server/web/src/components/sidebar/__tests__/rail-subshells.test.tsx
@@ -294,10 +294,11 @@ describe("the cell modes keep the row tree's facts", () => {
describe("a workspace's selection (operator ask 2026-09-27)", () => {
const nodeOf = (header: HTMLElement | undefined) => header?.getAttribute("aria-controls") ?? "";
- it("soft-rings every open pane and white-rings only the dock-focused one", async () => {
+ it("soft-rings every open pane, focused included, at one width", async () => {
// Standing in /workspaces/w1 with a AND b open. Both are SELECTED (the set)
- // so both soft-ring; the dock has focused `a`, so only `a` upgrades to the
- // bold white ring (operator 2026-09-27). Focus arrives through the
+ // so both wear the soft ring; the dock-focused `a` wears the SAME ring — the
+ // `ring-2` focus upgrade was retired the same day it arrived (operator
+ // 2026-09-27: too heavy at cell size). Focus arrives through the
// workspace-focus store the dock publishes — the rail cannot see dockview.
setWorkspaceFocusedId("a");
await withRail(
@@ -316,9 +317,14 @@ describe("a workspace's selection (operator ask 2026-09-27)", () => {
.slice(0, 2)
.map((l) => l.getAttribute("href")),
).toEqual(["/subshells/a", "/subshells/b"]);
- expect(tok("a")).toContain("ring-2"); // focused → white
- expect(tok("b")).toContain("ring-foreground/70"); // set → soft
- expect(tok("b")).not.toContain("ring-2"); // only the focused pane is bold
+ // One ring, one width, the whole set — the bare `ring-1` token is the
+ // selection (hover adds only the prefixed `hover:ring-1`), and the
+ // retired `ring-2` must be gone from both.
+ for (const id of ["a", "b"]) {
+ expect(tok(id)).toContain("ring-1");
+ expect(tok(id)).toContain("ring-foreground/70");
+ expect(tok(id)).not.toContain("ring-2");
+ }
},
{ path: "/workspaces/w1", panes: [{ subshellId: "a" }, { subshellId: "b" }] },
);
diff --git a/apps/server/web/src/components/sidebar/__tests__/subshell-cell-grid.test.tsx b/apps/server/web/src/components/sidebar/__tests__/subshell-cell-grid.test.tsx
index e91b802d6..a898329e2 100644
--- a/apps/server/web/src/components/sidebar/__tests__/subshell-cell-grid.test.tsx
+++ b/apps/server/web/src/components/sidebar/__tests__/subshell-cell-grid.test.tsx
@@ -259,13 +259,15 @@ describe("SubshellCell", () => {
const link = await screen.findByRole("link");
expect(link.className).toContain("bg-destructive");
// The "you are here" ring is the theme ink (`--foreground`: frost on the
- // void) at FULL brightness, width 2 — the operator's ask (2026-09-27) was
- // a WHITE ring around the open pane. Asserted as bare TOKENS: the focus
- // variant is `focus-visible:ring-2`/`ring-ring` and hover is
- // `hover:ring-1`/`hover:ring-foreground`, all prefixed, so a bare `ring-2`
+ // void) at 70, width 1 — the operator's ruling (2026-09-27): ONE soft
+ // ring for the whole open set, the `ring-2` focus bump retired the same
+ // day as too heavy. Asserted as bare TOKENS: the focus variant is
+ // `focus-visible:ring-2`/`ring-ring` and hover is
+ // `hover:ring-1`/`hover:ring-foreground`, all prefixed, so a bare `ring-1`
// or `ring-foreground/70` can only be the (soft) selection.
const toks = link.className.split(/\s+/);
- expect(toks).toContain("ring-2");
+ expect(toks).toContain("ring-1");
+ expect(toks).not.toContain("ring-2");
expect(toks).toContain("ring-foreground/70");
expect(link.className).toContain("focus-visible:ring-ring");
// Hover answers "which one am I on" with the same INK at full
@@ -326,7 +328,7 @@ describe("SubshellCell", () => {
}
});
- it("the blinking cell is a two-state tile: active/idle pulses, a legible letter every beat (operator ask 2026-09-27)", async () => {
+ it("the working cell pulses its fill only: no solid-green beat, one white initial (operator ruling 2026-09-27)", async () => {
const restore = mockFetch();
try {
renderCell(
@@ -338,54 +340,78 @@ describe("SubshellCell", () => {
/>,
);
const link = await screen.findByRole("link");
- // A cell cannot blink to nothing the way the dot does (no letter to
- // strand, no outline to empty), so it blinks BETWEEN two states: a full
- // green ACTIVE tile with a dark initial pulsing over a half-green IDLE
- // tile with a white initial on the opposite beat. Exactly one tile is
- // opaque at any moment, so a box with a readable letter is always there.
- // The anchor carries only posture: relative, a transparent field, and
- // NEITHER a fill nor a pulse of its own (both live on the two child
- // tiles). The interim "green survives as an edge" reading (the ring) and
- // the single-fade reading (empty tile / stranded letter) are both dead.
+ // The blink lives on the FILL alone: a half-green (`bg-success/50`, the
+ // idle fill) tile pulsing on the plain `subshell-dot-blink` over the
+ // bare rail, with a constant white initial above it. The retired shape
+ // blinked full-green + dark-knockout over half-green + white so no beat
+ // lacked a box; the operator killed the solid-green beat the same day
+ // (transparent background, white letter). The anchor carries posture
+ // only: relative, transparent, NO fill and NO pulse of its own.
const has = (el: Element | undefined, tok: string) => !!el && el.className.split(/\s+/).includes(tok);
expect(link.className).toContain("relative");
expect(link.className).toContain("bg-transparent");
- expect(link.className).not.toContain("bg-success");
- expect(link.className).not.toContain("subshell-dot-blink");
+ // Exact tokens: a solid `bg-success` (the retired ACTIVE fill) must be
+ // gone while `bg-success/50` is its own token and still present.
+ const linkToks = link.className.split(/\s+/);
+ expect(linkToks).not.toContain("bg-success");
+ expect(linkToks).not.toContain("subshell-dot-blink");
expect(link.className).not.toContain("border-success");
const spans = [...link.querySelectorAll("span")];
expect(spans.find((s) => s.className.includes("bg-muted"))).toBeUndefined();
- // The IDLE tile (under): half green, white initial, complementary pulse.
- const idle = spans.find((s) => has(s, "bg-success/50"));
- expect(idle).toBeTruthy();
- expect(has(idle, "subshell-dot-blink-alt")).toBe(true);
- expect(has(idle, "text-foreground")).toBe(true);
- expect(has(idle, "absolute") && has(idle, "inset-0") && has(idle, "rounded-md")).toBe(true);
- expect(idle?.getAttribute("aria-hidden")).toBe("true");
- expect(idle?.textContent).toBe("A");
-
- // The ACTIVE tile (over): full green, dark initial, the plain pulse.
- const activeTile = spans.find((s) => has(s, "bg-success") && has(s, "subshell-dot-blink"));
- expect(activeTile).toBeTruthy();
- expect(has(activeTile, "subshell-dot-blink-alt")).toBe(false);
- expect(has(activeTile, "text-background")).toBe(true);
- expect(has(activeTile, "absolute") && has(activeTile, "inset-0") && has(activeTile, "rounded-md")).toBe(true);
- expect(activeTile?.getAttribute("aria-hidden")).toBe("true");
- expect(activeTile?.textContent).toBe("A");
-
- // Exactly one pulse of each kind across the cell, and both carry the
- // initial — so the link's own text reads the letter twice even though a
- // viewer only ever sees one (the visible half differs by opacity, not DOM).
- expect(spans.filter((s) => s.className.includes("subshell-dot-blink-alt"))).toHaveLength(1);
- expect(
- spans.filter((s) => s.className.includes("subshell-dot-blink") && !s.className.includes("-alt")),
- ).toHaveLength(1);
- expect(link.textContent).toBe("AA");
- // The viewed pane still rings on the anchor (the soft white selection ring).
- expect(has(link, "ring-2")).toBe(true);
+ // The FILL: half green, pulsing, aria-hidden, and it carries NO letter
+ // (the retired `blink-alt` complement and the dark knockout are gone
+ // with the solid-green beat).
+ const fill = spans.find((s) => has(s, "subshell-dot-blink"));
+ expect(fill).toBeTruthy();
+ expect(has(fill, "bg-success/50")).toBe(true);
+ expect(spans.filter((s) => s.className.includes("subshell-dot-blink"))).toHaveLength(1);
+ expect(spans.find((s) => s.className.includes("blink-alt"))).toBeUndefined();
+ expect(has(fill, "absolute") && has(fill, "inset-0") && has(fill, "rounded-md")).toBe(true);
+ expect(fill?.getAttribute("aria-hidden")).toBe("true");
+ expect(fill?.textContent).toBe("");
+
+ // The LETTER: exactly one, white, positioned above the fill.
+ expect(link.textContent).toBe("A");
+ const letter = fill?.nextElementSibling ?? undefined;
+ expect(has(letter, "relative")).toBe(true);
+ expect(has(letter, "text-foreground")).toBe(true);
+ expect(has(letter, "text-background")).toBe(false);
+ expect(letter?.textContent).toBe("A");
+ // The viewed pane still rings on the anchor (the soft 1px selection ring).
+ // The ring IS the frame here, so no border rides along with it.
+ expect(has(link, "ring-1")).toBe(true);
expect(has(link, "ring-foreground/70")).toBe(true);
+ expect(has(link, "ring-2")).toBe(false);
+ expect(has(link, "border-border")).toBe(false);
+ } finally {
+ restore();
+ }
+ });
+
+ it("an un-ringed working cell keeps the dark frame the fill blinks inside (operator 2026-09-27)", async () => {
+ const restore = mockFetch();
+ try {
+ renderCell(
+ ,
+ );
+ const link = await screen.findByRole("link");
+ const has2 = (el: Element | undefined, tok: string) => !!el && el.className.split(/\s+/).includes(tok);
+ // The transparent half must not be a letter floating on bare rail: the
+ // anchor carries a static `--border` frame the fill pulses inside. Only
+ // where there is no ring — a selected/focused cell's frame is the ring.
+ expect(has2(link, "border")).toBe(true);
+ expect(has2(link, "border-border")).toBe(true);
+ expect(has2(link, "ring-foreground/70")).toBe(false);
+ // And it does NOT blink: the frame is on the anchor, the pulse stays on
+ // the fill child.
+ expect(has2(link, "subshell-dot-blink")).toBe(false);
} finally {
restore();
}
@@ -641,7 +667,7 @@ describe("SubshellCellGrid", () => {
// GROUPED view's cell-to-cell exactly (operator: "we should have the
// same gap as we do in the grouped view"), and a 4px tint gutter on ALL
// sides — the selection ring paints OUTSIDE the cell box, so p-0.5 left
- // it touching the plate edge (p-1 clears the ring-2 selection on every side).
+ // it touching the plate edge (p-1 clears the ring-1 selection on every side).
expect(plateA?.className).toContain("p-1");
expect(plateA?.className).toContain("gap-1.5");
expect(plateA?.className).toContain("rounded-lg");
@@ -683,7 +709,7 @@ describe("SubshellCellGrid", () => {
}
});
- it("soft-rings the open SET and white-rings only the focused pane", async () => {
+ it("soft-rings every open pane, focused included, at one width", async () => {
const restore = mockFetch();
try {
const rows = [sub({ id: "a", name: "one" }), sub({ id: "b", name: "two" }), sub({ id: "c", name: "three" })];
@@ -701,13 +727,15 @@ describe("SubshellCellGrid", () => {
(screen.getAllByRole("link") as HTMLElement[]).find((l) => l.getAttribute("href") === `/subshells/${id}`)
?.className ?? ""
).split(/\s+/);
- // (operator 2026-09-27): the SET (a, c) wears the soft `ring-foreground/70`;
- // only the focused pane (c) upgrades to the bold `ring-2`. Bare tokens —
+ // (operator 2026-09-27): ONE ring for the open set — a, c and the focused
+ // c alike wear the soft `ring-1 ring-foreground/70`. Bare tokens:
// hover/focus add only prefixed variants, so a lone `b` carries none.
- expect(toks("a")).toContain("ring-foreground/70");
- expect(toks("a")).not.toContain("ring-2");
- expect(toks("c")).toContain("ring-2");
- expect(toks("b")).not.toContain("ring-2");
+ for (const id of ["a", "c"]) {
+ expect(toks(id)).toContain("ring-1");
+ expect(toks(id)).toContain("ring-foreground/70");
+ expect(toks(id)).not.toContain("ring-2");
+ }
+ expect(toks("b")).not.toContain("ring-1");
expect(toks("b")).not.toContain("ring-foreground/70");
} finally {
restore();
diff --git a/apps/server/web/src/components/sidebar/rail-subshells.tsx b/apps/server/web/src/components/sidebar/rail-subshells.tsx
index fd2aafc98..5634b1df2 100644
--- a/apps/server/web/src/components/sidebar/rail-subshells.tsx
+++ b/apps/server/web/src/components/sidebar/rail-subshells.tsx
@@ -229,14 +229,15 @@ export function RailSubshells({
(sub: SubshellView) => nodeLabelFor(sub.nodeId || FALLBACK_NODE_ID, nodeData?.nodes, nodeData === undefined).label,
[nodeData],
);
- // Two selection levels (operator ask 2026-09-27). SELECTED is the SET of open
- // panes — the viewed `/subshells/:id` plus every pane the current
- // `/workspaces/:id` holds open (the SAME cached query the route runs, a hit
- // while a workspace page is up, disabled elsewhere). FOCUSED is the single
- // pane: the URL's on a subshell page, or the one the DOCK has active on a
- // workspace page (the dock publishes it through `lib/workspace-focus`; the
- // sidebar is a different tree and cannot otherwise see dockview's focus). The
- // set marks every open cell/row; only the focused one wears the bold ring.
+ // SELECTED is the SET of open panes — the viewed `/subshells/:id` plus every
+ // pane the current `/workspaces/:id` holds open (the SAME cached query the
+ // route runs, a hit while a workspace page is up, disabled elsewhere).
+ // FOCUSED is the single pane: the URL's on a subshell page, or the one the
+ // DOCK has active on a workspace page (the dock publishes it through
+ // `lib/workspace-focus`; the sidebar is a different tree and cannot otherwise
+ // see dockview's focus). The set marks every open cell/row. On the ROWS the
+ // two still differ (set = accent fill, focus adds the ring); on the CELLS one
+ // ring now covers both (operator 2026-09-27: the wide focus ring read heavy).
const focusedFromUrl = subshellIdFromPath(location.pathname);
const workspaceId = workspaceIdFromPath(location.pathname);
const { data: workspaceDetail } = useWorkspace(workspaceId ?? "", { enabled: workspaceId !== null });
diff --git a/apps/server/web/src/components/sidebar/subshell-cell-grid.tsx b/apps/server/web/src/components/sidebar/subshell-cell-grid.tsx
index a311265e1..e10627acd 100644
--- a/apps/server/web/src/components/sidebar/subshell-cell-grid.tsx
+++ b/apps/server/web/src/components/sidebar/subshell-cell-grid.tsx
@@ -145,15 +145,15 @@ export function flatCellRows(
const INITIAL_CLASS = "text-foreground";
/** Solid fills where white drops below AA (design-tokens contrast math):
- * `waiting` on `--warning` (1.34:1), `node-offline` on `--destructive` (2.77:1),
- * and the working cell's ACTIVE tile on full `--success` — so `active` is listed
- * even though it only ever shows as the top tile of the active/idle blink (see
- * the render site). Each keeps its own pairing: the bright green tile takes a
- * dark initial, the semitransparent idle tile beneath it keeps white (4.34:1).
- * Ruled 2026-09-27 (image #9): the ask was the green BACKGROUND legible, not an
- * edge, and the operator named the darker font directly. */
+ * `waiting` on `--warning` (1.34:1) and `node-offline` on `--destructive`
+ * (2.77:1). `active` was the third entry — the blink's full-`--success` tile
+ * took a dark initial (image #9 ruling, 2026-09-27, same day) — but the
+ * operator retired that pairing: the working cell now shows NO full-green
+ * beat at all (see the render site), so there is no bright field to knock
+ * out. White on the remaining states stands: idle's `success/50` clears
+ * 4.34:1, and the working letter sits on the rail or the half fill, white
+ * both beats. */
const DARK_KNOCKOUT: Partial> = {
- active: true,
waiting: true,
"node-offline": true,
};
@@ -185,13 +185,14 @@ export interface SubshellCellLabels {
* same tooltip string.
*
* The square itself carries the state: `DOT_CLASS` fills for everything quiet,
- * the working state blinks between two tiles (see {@link blinking} below), and a
+ * the working state pulses a half-green fill behind a constant white initial
+ * (see {@link blinking} below), and a
* bell (`showsBell`) REPLACES the fill, the dot's posture — the glyph names
- * "unseen push", the tone keeps the state. Selection reads on TWO levels
- * (operator 2026-09-27): every open pane's cell (the workspace's SET) wears a
- * thin `ring-1 ring-foreground/70`, and the one cell the dock has FOCUSED wears
- * the thicker `ring-2 ring-foreground/70` — both share the `/70` tone, so focus
- * reads bolder by WIDTH, not a brighter line. The accessible name is "name: status
+ * "unseen push", the tone keeps the state. Every open pane's cell (the
+ * workspace's SET, focused included) wears ONE soft `ring-1 ring-foreground/70`
+ * (operator ruling 2026-09-27, same day as the two-width version it replaces:
+ * the `ring-2` focus ring read too heavy at cell size, and at one width the
+ * two levels stop being tellable apart, so there is one level). The accessible name is "name: status
* word", and for a bell "name: unseen notification (status word)" — the shared
* {@link bellAnnouncement} — since a square of colour has nothing to read out
* and its glyph is aria-hidden.
@@ -210,7 +211,8 @@ export function SubshellCell({
subshell: SubshellView;
/** Open in the current workspace (or the viewed page) — marks the SET. */
selected: boolean;
- /** The pane the dock has focused — the single cell with the white ring. */
+ /** The pane the dock has focused. It rings like every other open cell; kept
+ * because a focused pane must wear the ring even before the SET catches up. */
focused: boolean;
/**
* The cell is a pane of the workspace on screen, so a click should FOCUS that
@@ -222,13 +224,19 @@ export function SubshellCell({
}) {
const indicator = subshellIndicator(subshell);
const bell = showsBell(subshell);
- // The ACTIVE square blinks, but between two states rather than green and
- // nothing: an ACTIVE tile pulsing over an IDLE tile (see the render site), so a
- // box with a legible initial is on screen every beat. A cell cannot blink to
- // empty the way the dot does — the dot has no letter to strand and no outline
- // to empty (images #10/#11/#14 were each a different symptom of that). Every
- // other state keeps its single static fill on the anchor exactly as before —
- // `terminated`'s hollow reading especially: nothing behind it.
+ // The ACTIVE square pulses its FILL only: a `bg-success/50` tile blinking
+ // over the bare rail (see the render site) behind a constant white initial.
+ // It used to blink between two painted tiles — full green with a dark
+ // knockout letter over the half-green one — so no beat ever lacked a box.
+ // The operator retired the solid-green beat on 2026-09-27: transparent
+ // background + white letter on that half, which also retires the dark
+ // knockout (images #10/#11/#14 were symptoms of stranding a DARK letter on
+ // an empty tile; a white one reads on the rail exactly as `terminated`'s
+ // does — and an UN-RINGED working cell keeps a static `--border` frame so
+ // even the empty half is a square, operator 2026-09-27 follow-up). Every
+ // other state keeps its single static fill on the anchor
+ // exactly as before — `terminated`'s hollow reading especially: nothing
+ // behind it.
const blinking = !bell && indicator === "active";
const cell = (
@@ -267,21 +275,26 @@ export function SubshellCell({
// and there is nothing to blink behind it.
"relative border border-border bg-transparent"
: blinking
- ? // No field and no frame of its own: the blinking green box
- // (with its letter) is a child layer, so the transparent
- // half is genuinely empty rail, not a dark letter stranded
- // inside a frame (image #10). Only the selection/focus ring
- // (below) can still name the cell in that gap.
- "relative bg-transparent"
+ ? // No field of its own: the blinking half-green fill is a
+ // child layer, so the transparent half is genuinely empty
+ // rail with the white initial riding over it (the
+ // constant letter is a sibling above the fill). The
+ // square keeps a dark `--border` frame on that half —
+ // operator 2026-09-27: the letter floating with no box
+ // read broken, the frame is what stays — but only when
+ // the cell has NO ring: a selected/focused working cell
+ // already wears its frame in ink, and a border under the
+ // ring would just be a second line.
+ cn("relative bg-transparent", !selected && !focused && "border border-border")
: DOT_CLASS[indicator],
- // Two selection levels (operator 2026-09-27), both SOFT so the
- // box matches the rows: the focused pane wears a `ring-2`
- // (`ring-foreground/70`), the other open panes (the set) keep the
- // same tone at width 1. Focus reads bolder by WIDTH, not by a
- // harsh full-brightness line. Hover answers "which one am I on"
- // at width 1 full ink; focus stays the orchid RING token.
+ // ONE ring for the whole open SET (operator 2026-09-27), SOFT so
+ // the box matches the rows: `ring-1 ring-foreground/70` on every
+ // open pane, focused or not. The focus bump to `ring-2` from the
+ // same day read too heavy and was retired the same day. Hover
+ // answers "which one am I on" at width 1 full ink; keyboard
+ // focus stays the orchid RING token.
"hover:ring-1 hover:ring-foreground",
- focused ? "ring-2 ring-foreground/70" : selected ? "ring-1 ring-foreground/70" : undefined,
+ focused || selected ? "ring-1 ring-foreground/70" : undefined,
)}
/>
}
@@ -289,38 +302,24 @@ export function SubshellCell({
{bell ? (
) : blinking ? (
- // The working cell blinks between two REAL states, never to an
- // empty tile: an ACTIVE tile (`bg-success` + a dark initial) on top
- // of an IDLE tile (`bg-success/50` + a white initial) underneath,
- // the two driven by complementary opacity pulses (`subshell-dot-blink`
- // / `subshell-dot-blink-alt`). Exactly one tile is fully painted at
- // any beat, so a box carrying a legible letter is ALWAYS there — the
- // dot can blink to nothing because it has no letter; a cell cannot
- // (that empty tile was image #14, the ghost #10, the stranded letter
- // #11). Under reduced-motion neither class applies, the ACTIVE tile
- // paints over the idle one, and the cell rests as solid green + dark
- // initial. Each tile keeps its own state's contrast pairing.
+ // The working cell pulses its FILL, never its letter: a half-green
+ // tile (`bg-success/50`, the idle fill, driven by the plain
+ // `subshell-dot-blink` opacity pulse so it switches on the same
+ // beat as the dot) with a constant WHITE initial riding above it.
+ // The transparent half is the bare rail with the white letter on it
+ // — legible exactly as `terminated`'s hollow cell is, which is what
+ // lets the solid-green + dark-knockout beat go away (operator
+ // ruling 2026-09-27: that pairing read wrong; the letter is white,
+ // the background transparent). The fill is aria-hidden layout, the
+ // letter is the one the link reads out. Under reduced-motion the
+ // class carries nothing: the cell rests as the half-green fill +
+ // white initial, like idle, with the tooltip still saying working.
<>
- {labels.initial}
-
-
- {labels.initial}
-
+ className={cn("subshell-dot-blink absolute inset-0 rounded-md", DOT_CLASS.idle)}
+ />
+ {labels.initial}
>
) : labels.initial ? (
// Solid light fills defeat white (see `DARK_KNOCKOUT`); every
@@ -397,9 +396,10 @@ export function SubshellCellGrid({
*/
selectedIds: ReadonlySet;
/**
- * The single pane the dock has focused (or the viewed page's id) — the one
- * cell that upgrades to the bold white ring. `selectedIds` and `focusedId`
- * are different signals on purpose (operator 2026-09-27).
+ * The single pane the dock has focused (or the viewed page's id). Since the
+ * one-ring ruling (operator 2026-09-27) it renders no differently from the
+ * SET — it is carried so a just-focused pane rings even a beat before the
+ * SET's query catches up.
*/
focusedId: string | null;
/**
diff --git a/apps/server/web/src/styles.css b/apps/server/web/src/styles.css
index 94b74f8a7..21de11ec6 100644
--- a/apps/server/web/src/styles.css
+++ b/apps/server/web/src/styles.css
@@ -329,18 +329,15 @@ body {
animation: subshell-dot-blink 1.6s steps(1) infinite;
}
- /* The exact complement of `.subshell-dot-blink` (same 1.6s / steps(1), the
- two opacity halves swapped). The rail's working CELL needs both at once:
- the green box and its dark letter ride the plain blink, and a WHITE letter
- underneath rides THIS one, so a readable initial is present in either
- half. Without it the transparent half leaves an empty tile (the dot can
- vanish clean because it carries no letter; a cell cannot) — operator device
- review 2026-09-27, image #11. Under `prefers-reduced-motion` neither class
- exists, both letters sit at full opacity, and the green box paints over
- the white one, so the resting cell is the solid green + dark letter. */
- .subshell-dot-blink-alt {
- animation: subshell-dot-blink-alt 1.6s steps(1) infinite;
- }
+ /* The rail's working CELL rides this same class, on the fill only: a
+ `bg-success/50` tile blinking behind a constant white initial, so the
+ letter never rides the pulse. It used to need the exact complement of
+ this class (a second, `blink-alt` pulse carrying the idle tile under the
+ solid-green one, so a DARK letter was never stranded on an empty beat —
+ operator device review 2026-09-27, image #11); the letter turned white
+ the same day and the complement went with the solid-green beat it
+ existed to pair with. Under `prefers-reduced-motion` the class carries
+ nothing and the cell rests as the half-green fill + white initial. */
/* Input queue badge (spec 2026-09-21 Wave A): the three chevrons pulse in a
stagger so in-flight keystrokes read as moving, not stuck. One keyframe,
@@ -365,17 +362,6 @@ body {
opacity: 0;
}
}
-/* The complement of `subshell-dot-blink`: visible exactly where that one hides,
- so the white initial underneath the blinking green box reads through the
- transparent half. See `.subshell-dot-blink-alt`. */
-@keyframes subshell-dot-blink-alt {
- 0% {
- opacity: 0;
- }
- 50% {
- opacity: 1;
- }
-}
@keyframes assistant-in {
from {
opacity: 0;