Skip to content

fix(nav): recenter the logo mark's L and add pointer cursor to nav buttons - #37

Merged
Mavengence merged 2 commits into
mainfrom
fix/nav-logo-centering
Aug 12, 2026
Merged

fix(nav): recenter the logo mark's L and add pointer cursor to nav buttons#37
Mavengence merged 2 commits into
mainfrom
fix/nav-logo-centering

Conversation

@Mavengence

Copy link
Copy Markdown
Contributor

The "L" wasn't centered — it read as a clip, but was a layout bug

The rotated square looked like it clipped its left side on scroll. It didn't — the . next to L faded via opacity alone, and opacity doesn't remove an element from layout. As an inline sibling of L inside the square's justify-content: center box, the invisible dot kept reserving its full width for the entire 60-130px scroll range it takes to fade out. The visible L sat left of true center that whole time, which on a rotated square reads as an asymmetric clip rather than off-center text.

Fix: the dot's span now collapses width to 0 over the same scroll range as its opacity fade — the same pattern the wordmark's own L already uses when it collapses into the icon (lWidth/lOpacity a few lines above). Once the dot is gone, L recenters.

Verified against an isolated reproduction of the exact box model (green line = square's true center):

State Result
Before (opacity-only fade) L sits left of center
After (width collapses too) L straddles center correctly
Mid-transition smooth, no jump — same useTransform(scrollY, [60,130], ...) range drives both opacity and width, so they're inherently synchronized

This is a scroll-position-driven motion value with no viewport-conditional logic, so it holds identically on mobile — confirmed the top-of-scroll render is unaffected/unclipped at a 390px viewport.

Nav buttons had no pointer cursor on hover

The three <button> elements in the nav — the Lernen/Praxis/Wissen dropdown triggers, and the mobile menu's open/close toggles — showed the plain arrow cursor on hover instead of a pointer.

Root cause: Tailwind v4's preflight removed the cursor: pointer default it used to add for buttons (a deliberate upstream change), and a native <button> is cursor: default in the browser's own UA stylesheet — unlike <a href>, which gets pointer natively. cursor-pointer is already the established convention elsewhere in this codebase (14 other usages); these three had just never picked it up.

Verification

  • typecheck clean, lint clean, vitest 537 files / 4630 tests pass
  • No tests reference the logo mark's internal motion values or button classes, so nothing needed updating
  • Built in an isolated worktree off origin/main (already has the previous logo fix, fix(nav): stop clipping the logo mark and give the lockup a real black cut #35) to avoid touching a concurrent session's checkout in the shared working directory

Heads up on CI

main's verify gate has a pre-existing, unrelated sitewide WCAG touch-target regression (documented on #35) that fails every PR regardless of diff. If that's still unresolved when this PR's CI runs, the same situation applies — a bypass-list merge may be needed, same as #35.

…buttons

The rotated square looked like it clipped its left side. It didn't — the "."
next to "L" faded via `opacity` only, so once invisible it still reserved its
full layout width as an inline sibling inside the square's
`justify-content: center` box. The visible "L" sat left of true center the
entire time the dot was fading (60-130px of scroll), which on a rotated
square reads as an asymmetric clip rather than off-center text. The dot's
span now also collapses `width` to 0 over the same scroll range (matching
the pattern the wordmark's own "L" already uses when it collapses into the
icon), so once the dot is gone the "L" recenters. Same scroll-driven
motion values regardless of viewport, so this holds identically on mobile.

Separately, the three <button> elements in the nav (the Lernen/Praxis/Wissen
dropdown triggers, and the mobile menu's open/close toggles) had no
`cursor-pointer`. Tailwind v4's preflight removed the pointer-cursor default
it used to add for buttons, and a native <button> is `cursor: default` in the
browser's own UA styles (unlike `<a href>`, which is `pointer` natively) — so
hovering them showed the plain arrow cursor. `cursor-pointer` is already the
established convention elsewhere in this codebase; these three had just
never picked it up.
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
platform Ready Ready Preview Aug 11, 2026 7:07pm

Request Review

content-lint's TSX comment-skip heuristic only recognizes single-line
comments and JSDoc-style `* `-prefixed continuation lines; a plain
multi-line `/* ...\n   continuation\n */` block, where a continuation line
does not itself start with `*`, reads as ordinary prose. My previous commit
added exactly that shape with an em-dash on a continuation line, which the
gate correctly caught. Rephrased around it rather than touching the shared,
canonical content-lint.mjs.
@Mavengence
Mavengence merged commit 53bc06c into main Aug 12, 2026
7 of 8 checks passed
@Mavengence
Mavengence deleted the fix/nav-logo-centering branch August 12, 2026 05:58
Mavengence added a commit that referenced this pull request Aug 12, 2026
…on (#38)

Added after investigating a report that the icon mark's "L" and "." looked
misaligned on scroll: production, dev, and both Chromium and desktop WebKit
all measured correctly centred and on the same line at every checkpoint
across 0-200px of scroll, on this build and live. No product bug reproduced
anywhere; the likely explanation is catching the ~2-minute window between
the #37 merge and Vercel's deploy finishing, or a cached page.

Given the fix is already shipped and verified rather than in question, the
useful "last PR" here is a permanent regression test locking the invariant
in for whoever touches this component next, so a revert of the width-collapse
fix (or a change that makes the dot a block-level sibling of "L") fails
immediately instead of shipping unnoticed.

Building it surfaced a real Playwright+WebKit gap along the way: neither a
programmatic `scrollTo` + a manually dispatched `scroll` event, nor a real
`mouse.wheel` gesture on a desktop WebKit context, ever produced a change in
this mark's rendered transform/opacity in Playwright's bundled WebKit browser
- headless or headed - while `window.scrollY` itself updated correctly every
time. That implicates framer-motion's `useScroll()` reporting to Playwright's
WebKit specifically, not this component: every other scroll-driven nav
property (icon size, border, wordmark tracking) rides the same MotionValue
and would be equally frozen. No existing spec in this suite currently
exercises that mechanism (a11y-reduced-motion.spec.ts covers
IntersectionObserver-driven whileInView reveals and explicitly excludes this
same hero/nav scroll-linked motion from its scan). Skipped on WebKit with a
documented reason rather than asserting against geometry that silently never
leaves its resting state; Chromium and mobile-chromium exercise the real
mechanism reliably and are the coverage that matters here.

Stress-tested 8+ consecutive runs across chromium/mobile-chromium/mobile-webkit
with zero flakes before landing.
Mavengence added a commit that referenced this pull request Aug 12, 2026
…aseline (#39)

Real bug, reported after #37 shipped: the "." visibly floated above "L"'s
bottom edge instead of resting on it. My own earlier Playwright measurements
had already caught this (dot bottom 6px above L's bottom at rest) but I
mis-scoped the check to "do these two elements' lines merely overlap",
which a 4-6px gap still satisfies, and dismissed it as imperceptible without
looking at the actual number.

Root cause: `inline-block` defaults to `vertical-align: baseline`, which
aligns the dot SPAN's own baseline to L's text baseline. Per spec, once
`overflow-hidden` is set (needed for the width-collapse animation from #37),
an inline-block's own baseline becomes its bottom margin edge rather than its
content's text baseline - so the dot's BOX bottom was landing at L's
baseline, not at L's own visible bottom. Measured directly (not eyeballed):
4-6px of gap depending on the icon's current font size (13-20px across the
scroll range).

`align-text-bottom` aligns to the bottom of the parent's font instead of the
dot's own computed baseline, landing exactly on L's bottom - measured 0.00px
of gap at every font size the icon uses, with a small (<1.3px) residual only
under full rotation, which is an axis-aligned-bounding-box measurement
artifact of rotating the box, not a real misalignment: it appears in the
screen-space rect of both L and the dot equally.

The test added in #38 could not have caught this: its overlap check is a much
weaker property than "sits on the same baseline". Replaced it with a strict
sub-1.5px baseline check (`sitsOnBaseline`), which would have failed against
the pre-fix code and now passes against this fix - measured directly on the
built output before committing, not just on the reasoning above.
Mavengence added a commit that referenced this pull request Aug 12, 2026
The mark paired "L" with a "." that faded and collapsed on scroll. That
pairing went through two rounds of alignment bugs in a row:

1. (#37) The dot faded via `opacity` alone. Opacity doesn't remove layout
   width, so the invisible dot kept reserving space inside the square's
   `justify-content: center` box, pushing "L" off centre.
2. Even after that fix, the dot's `inline-block` defaulted to
   `vertical-align: baseline`, which (per spec, once `overflow-hidden` is set)
   aligns the dot's own bottom edge to L's text baseline rather than to L's
   visible bottom - measured 4-6px of gap depending on the icon's font size,
   the dot visibly floating above where it should rest.

Two fixable bugs on the same pairing is a sign the pairing itself is the
wrong shape for this animation, not that the next fix will be the last one.
Dropped the dot: "L" alone, centred, rotating, has no second element to
misalign against, and no bug class from either regression above can recur.

Verified via Playwright measurement (not just visual review): "L" sits within
<1px of the square's true centre at every scroll checkpoint from 0-200px, and
the square's transform is non-identity once fully scrolled and returns to
"none" at rest. Stress-tested 8 consecutive runs across
chromium/mobile-chromium/mobile-webkit with zero flakes.

The e2e test is rewritten to match: it no longer asserts anything about a
dot, since there isn't one, only that "L" stays centred through the full
transition and that the square actually rotates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant