diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css
index 8c30aaa5db..b861518e4d 100644
--- a/apps/desktop/src/renderer/maka-tokens.css
+++ b/apps/desktop/src/renderer/maka-tokens.css
@@ -1228,6 +1228,39 @@
@layer base {
* { box-sizing: border-box; border-color: var(--border); }
+ /* One scrollbar for the whole app (visual system 2.0 T5-F): the platform
+ thin bar, coloured with `--border-strong` over a transparent gutter. The
+ universal selector is intentional because `scrollbar-width` is not
+ inherited. This base-layer default yields to element-owned declarations
+ in the higher Astryx and product component layers, such as a TabList
+ strip's `none` (#2538). */
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-strong) transparent;
+ }
+
+ *::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+ }
+
+ *::-webkit-scrollbar-track,
+ *::-webkit-scrollbar-corner {
+ background: transparent;
+ }
+
+ *::-webkit-scrollbar-thumb {
+ border: 2px solid transparent;
+ border-radius: var(--radius-pill);
+ background: var(--border-strong);
+ background-clip: content-box;
+ }
+
+ *::-webkit-scrollbar-thumb:hover {
+ /* One step above --border-strong's 16%, scoped to the thumb only. */
+ background-color: oklch(from var(--foreground) l c h / 0.28);
+ }
+
/* The root font-size stays at the browser default (16px) ON PURPOSE.
It used to be pinned to 13px to express Maka's density, but the root is
not a density knob: it is an implicit multiplier on every rem in the
diff --git a/apps/desktop/src/renderer/styles/base.css b/apps/desktop/src/renderer/styles/base.css
index 23b0804678..c6b0315f5e 100644
--- a/apps/desktop/src/renderer/styles/base.css
+++ b/apps/desktop/src/renderer/styles/base.css
@@ -126,36 +126,3 @@ button {
transition: none !important;
caret-color: transparent !important;
}
-
-/* One scrollbar for the whole app (visual system 2.0 T5-F). Both reference
- systems converged on the same recipe: a slim pill inset in a transparent
- gutter, one step darker on hover, no painted track. The 2px transparent
- border + content-box clip turns the 10px hit area into a 6px pill. The
- few surfaces that hide or thin their own bars are more specific and keep
- winning. */
-* {
- scrollbar-width: thin;
- scrollbar-color: var(--border-strong) transparent;
-}
-
-*::-webkit-scrollbar {
- width: 10px;
- height: 10px;
-}
-
-*::-webkit-scrollbar-track,
-*::-webkit-scrollbar-corner {
- background: transparent;
-}
-
-*::-webkit-scrollbar-thumb {
- border: 2px solid transparent;
- border-radius: var(--radius-pill);
- background: var(--border-strong);
- background-clip: content-box;
-}
-
-*::-webkit-scrollbar-thumb:hover {
- /* One step above --border-strong's 16%, scoped to the thumb only. */
- background-color: oklch(from var(--foreground) l c h / 0.28);
-}
diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx
index 645e408027..366fc522b2 100644
--- a/apps/desktop/stories/session-workbar.stories.tsx
+++ b/apps/desktop/stories/session-workbar.stories.tsx
@@ -1087,6 +1087,31 @@ export const SeveralFacesAtColumnFloor: Story = {
render: () => (
),
+ play: async ({ canvasElement }) => {
+ // Astryx's TabList hides its own overflow scrollbar (`scrollbar-width:
+ // none`) and scrolls the strip instead. The app default must not override
+ // that: as a `*` rule in `layer(components)` it out-ranked the component
+ // layer and re-showed the bar (#2538). The app default now lives in the
+ // lower `base` layer, so it no longer competes with the strip's `none`.
+ const tablist = await within(canvasElement).findByRole('tablist');
+ const nodes = [tablist, ...tablist.querySelectorAll('*')];
+ const strip = nodes.find((node) => getComputedStyle(node).overflowX === 'auto');
+ if (!strip) {
+ throw new Error('expected the tab strip to expose a horizontal scroll container');
+ }
+ expect(getComputedStyle(strip).scrollbarWidth).toBe('none');
+
+ // Keeping the default universal is equally important: `scrollbar-width`
+ // does not inherit, so a root-only rule would leave this scrollport `auto`.
+ const reviewPanel = await waitFor(() => {
+ const element = canvasElement.querySelector(
+ '.maka-session-review-panel',
+ );
+ if (!element) throw new Error('expected the review panel to render');
+ return element;
+ });
+ expect(getComputedStyle(reviewPanel).scrollbarWidth).toBe('thin');
+ },
};
// Below 991px the column stacks under the conversation at full width. The