The header's height depended on the viewport width, and not even monotonically:
width
before
after
1440
112px, controls in 2 rows, tabs in 2
72px, one row
1280
—
72px
1100
169px, tabs across four ragged rows
72px
900
131px
72px
720
246px
72px
480
191px
76px
Both control groups wrapped, so every width produced a different ragged block. Four different control heights (32, 34, 45 and 54px) sat in the same row — the 54px one being the rig picker with its summary stacked underneath — and on narrow viewports the icon buttons stretched to fill half the row, rendering a play triangle centred in a 249px box.
The change
Both groups are now one row that never wraps. Controls are a uniform 2rem and no longer stretch, the rig summary sits inline beside its select, and the page tabs scroll instead of wrapping with the trailing edge faded so a partly-scrolled tab reads as "there is more" rather than as a label cut mid-word.
Secondary controls — layout, style and theme — move into a ⋯ overflow menu when the bar cannot hold them, leaving audio, record and the rig picker inline:
This is the part worth reviewing. Two obvious tests are both wrong here, and only screenshots caught it:
A viewport-width threshold left the theme toggle clipped at the card edge at 1440px. How much fits depends on the rig name and the label text, not the window.
bar.scrollWidth > bar.clientWidth reports overflow at 1600px when nothing is clipped, so every control got banished to the menu.
"the nav isn't scrolling" is unsatisfiable: with min-width: 0 the nav always shrinks to the leftover space, so it always reports scrolling.
What works is comparing natural widths against the space available — scrollWidth on a scroll container is its unconstrained content width:
Measured in real Chromium at 1600, 1440, 1280, 1100, 900, 720 and 480px: header 72px throughout (76px at 480), every control 32px, nothing clipped at any width. Full gate green — typecheck, lint, 34/34 test, build, test:browser.
Notes for review
The ⋯ menu is in use at every width. The card's max-width (~1100px) means eight tabs plus the identity block never leave room for the secondary controls inline. If they should appear inline on wide screens, the card max-width is the thing to change, not the bar.
The tab-strip fade is not scroll-aware. Pure CSS cannot detect scroll position, so the last tab keeps a slight fade even when scrolled fully right. The background-attachment: local alternative is scroll-aware but needs a colour-matched cover gradient, and .card is transparent — it would have to track the page background across both themes and all nine styles. A mask is colour-agnostic; making it scroll-aware would need a scroll listener.
No automated regression test for the geometry. The DOM fixture used by the unit tests cannot measure layout. The numbers above come from ad-hoc Chromium measurement. Folding a "header stays one row" assertion into browser-smoke.mjs would guard it if that is wanted.
## What was wrong
The header's height depended on the viewport width, and not even monotonically:
| width | before | after |
| --- | --- | --- |
| 1440 | 112px, controls in 2 rows, tabs in 2 | **72px**, one row |
| 1280 | — | **72px** |
| 1100 | 169px, tabs across **four ragged rows** | **72px** |
| 900 | 131px | **72px** |
| 720 | 246px | **72px** |
| 480 | 191px | **76px** |
Both control groups wrapped, so every width produced a different ragged block. Four different control heights (32, 34, 45 and 54px) sat in the same row — the 54px one being the rig picker with its summary stacked underneath — and on narrow viewports the icon buttons stretched to fill half the row, rendering a play triangle centred in a 249px box.
## The change
Both groups are now one row that never wraps. Controls are a uniform 2rem and no longer stretch, the rig summary sits inline beside its select, and the page tabs scroll instead of wrapping with the trailing edge faded so a partly-scrolled tab reads as "there is more" rather than as a label cut mid-word.
Secondary controls — layout, style and theme — move into a `⋯` overflow menu when the bar cannot hold them, leaving audio, record and the rig picker inline:
```
▶ REC [Primary fixture ▾] Smoke Fixture · RX · FM ⋯
```
## Deciding when things fit
This is the part worth reviewing. Two obvious tests are both wrong here, and only screenshots caught it:
- **A viewport-width threshold** left the theme toggle clipped at the card edge at 1440px. How much fits depends on the rig name and the label text, not the window.
- **`bar.scrollWidth > bar.clientWidth`** reports overflow at 1600px when nothing is clipped, so every control got banished to the menu.
- **"the nav isn't scrolling"** is unsatisfiable: with `min-width: 0` the nav always shrinks to the leftover space, so it always reports scrolling.
What works is comparing *natural* widths against the space available — `scrollWidth` on a scroll container is its unconstrained content width:
```ts
const needed = identity.offsetWidth + nav.scrollWidth + actions.scrollWidth + gutters;
return needed <= bar.clientWidth;
```
## Verification
Measured in real Chromium at 1600, 1440, 1280, 1100, 900, 720 and 480px: header 72px throughout (76px at 480), every control 32px, nothing clipped at any width. Full gate green — `typecheck`, `lint`, 34/34 `test`, `build`, `test:browser`.
## Notes for review
- **The `⋯` menu is in use at every width.** The card's max-width (~1100px) means eight tabs plus the identity block never leave room for the secondary controls inline. If they should appear inline on wide screens, the card max-width is the thing to change, not the bar.
- **The tab-strip fade is not scroll-aware.** Pure CSS cannot detect scroll position, so the last tab keeps a slight fade even when scrolled fully right. The `background-attachment: local` alternative *is* scroll-aware but needs a colour-matched cover gradient, and `.card` is transparent — it would have to track the page background across both themes and all nine styles. A mask is colour-agnostic; making it scroll-aware would need a scroll listener.
- **No automated regression test for the geometry.** The DOM fixture used by the unit tests cannot measure layout. The numbers above come from ad-hoc Chromium measurement. Folding a "header stays one row" assertion into `browser-smoke.mjs` would guard it if that is wanted.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
The header's height depended on the viewport width, and not even
monotonically: 112px at 1440, 169px at 1100, 131px at 900, 246px at 720.
Both control groups wrapped, so every width produced a different ragged
block — eight page tabs across four rows at 1100px, and action controls
across three. Four different control heights (32, 34, 45 and 54px) sat
in the same row, the 54px one being the rig picker with its summary
stacked underneath, and on narrow viewports the icon buttons stretched to
fill half the row, rendering a play triangle centred in a 249px box.
Lay both groups out as one row that never wraps. Controls are a uniform
2rem and no longer stretch, the rig summary sits inline beside its select,
and the page tabs scroll instead of wrapping. Secondary controls —
layout, style and theme — move into an overflow menu when the bar cannot
hold them, leaving audio, record and the rig picker inline.
Deciding when they no longer fit needs natural widths, not rendered ones:
the nav has min-width 0 and scrolls, so it always shrinks to the leftover
space and always reports scrolling, and the bar reports overflow even when
nothing is clipped. scrollWidth on the scroll container is its
unconstrained content width, which is what the fit test compares against
the space available.
Measured after the change: 72px at 1440, 1280, 1100, 900 and 480, every
control 32px, nothing clipped at any width.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
The page tabs scroll rather than wrap, so the last visible tab was sliced
mid-word ("Se…" for Settings), which reads as a rendering fault instead of
as an invitation to scroll.
Fade the trailing edge with a mask. A colour-matched cover gradient is
the usual trick, but the card is transparent, so a cover would have to
track the page background across both themes and all nine styles; a mask
is colour-agnostic. Only the trailing edge is faded, leaving the first
tab crisp while the strip sits at rest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
sjg
merged commit dd5760c436 into main2026-08-02 18:50:53 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What was wrong
The header's height depended on the viewport width, and not even monotonically:
Both control groups wrapped, so every width produced a different ragged block. Four different control heights (32, 34, 45 and 54px) sat in the same row — the 54px one being the rig picker with its summary stacked underneath — and on narrow viewports the icon buttons stretched to fill half the row, rendering a play triangle centred in a 249px box.
The change
Both groups are now one row that never wraps. Controls are a uniform 2rem and no longer stretch, the rig summary sits inline beside its select, and the page tabs scroll instead of wrapping with the trailing edge faded so a partly-scrolled tab reads as "there is more" rather than as a label cut mid-word.
Secondary controls — layout, style and theme — move into a
⋯overflow menu when the bar cannot hold them, leaving audio, record and the rig picker inline:Deciding when things fit
This is the part worth reviewing. Two obvious tests are both wrong here, and only screenshots caught it:
bar.scrollWidth > bar.clientWidthreports overflow at 1600px when nothing is clipped, so every control got banished to the menu.min-width: 0the nav always shrinks to the leftover space, so it always reports scrolling.What works is comparing natural widths against the space available —
scrollWidthon a scroll container is its unconstrained content width:Verification
Measured in real Chromium at 1600, 1440, 1280, 1100, 900, 720 and 480px: header 72px throughout (76px at 480), every control 32px, nothing clipped at any width. Full gate green —
typecheck,lint, 34/34test,build,test:browser.Notes for review
⋯menu is in use at every width. The card's max-width (~1100px) means eight tabs plus the identity block never leave room for the secondary controls inline. If they should appear inline on wide screens, the card max-width is the thing to change, not the bar.background-attachment: localalternative is scroll-aware but needs a colour-matched cover gradient, and.cardis transparent — it would have to track the page background across both themes and all nine styles. A mask is colour-agnostic; making it scroll-aware would need a scroll listener.browser-smoke.mjswould guard it if that is wanted.🤖 Generated with Claude Code
https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
The page tabs scroll rather than wrap, so the last visible tab was sliced mid-word ("Se…" for Settings), which reads as a rendering fault instead of as an invitation to scroll. Fade the trailing edge with a mask. A colour-matched cover gradient is the usual trick, but the card is transparent, so a cover would have to track the page background across both themes and all nine styles; a mask is colour-agnostic. Only the trailing edge is faded, leaving the first tab crisp while the strip sits at rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz Signed-off-by: Stan Grams <sjg@haxx.space>