Skip to content

Fix the contrast and motion defects the light-base inversion left behind - #61

Merged
AndrewCTF merged 2 commits into
frontend-crash-optimizefrom
site-contrast-motion-fixes
Jul 25, 2026
Merged

Fix the contrast and motion defects the light-base inversion left behind#61
AndrewCTF merged 2 commits into
frontend-crash-optimizefrom
site-contrast-motion-fixes

Conversation

@AndrewCTF

Copy link
Copy Markdown
Owner

Follow-up to the light-base inversion (bf49562). Reviewing the page against
palantir.com surfaced a set of defects that were invisible at a glance but
destroyed information, plus two that broke the page outright for some visitors.
Every number below was measured in the browser before and after.

Text that could not be read

Inverting to a light base moved --txt to near-black without re-checking the
rules that paint onto a dark surface. Six had a duplicate declaration
overriding a correct earlier one:

Element Before After
Six app names in the workspace grid (.tile b) 1.01:1 17.5:1
Nav section links over the hero (.nav-links a) 2.36:1 13.5:1
Focus ring, first nine tab stops (:focus-visible) 1.10:1 18.2:1
Copy button (.copy-btn) 1.79:1 7.4:1
FAQ open-state chip (details[open] .x) 1.76:1 13.6:1
Wordmark inside the open mobile menu 1:1 18.1:1

.tile b declared no colour at all, so the six app names inherited near-black
onto dark screenshots while their lighter subtitles read fine — the section is
titled "Twelve apps, one workspace" and is that claim's only evidence.
summary:hover was #fff on the white page, so hovering a FAQ question erased
it.

The blend veil was eating the product

.bleed::after had an opaque --void top stop, putting a white wash over the
first 121px of the console recording, and a 92%-black bottom stop covering
another 167px. That is 288px of 758px, and precisely the two zones the app
draws its live status bar (LINK live · FEEDS 6/6 live · CLOCK live) and its
timeline in. The chip carries its own scrim, so the gradient bought nothing.
Bands butt instead.

Scroll reveals were load-bearing

All 43 .reveal blocks started at opacity: 0 with no scripting fallback, so
a visitor with JS blocked got three background images and nothing else — the
headline, every stat, the comparison table, the FAQ and both CTAs all gone.
And because the observer unobserved on first intersection, any block the
viewport jumped over stayed invisible for good: an End-key press stranded
eleven, including a 560px band that rendered as pure white.

The hidden state is now gated on a .js class set inline, and the observer is
idempotent rather than one-shot. Verified: 0/43 stranded after jumping to the
bottom and scrolling back.

Also

  • .shot had no position: relative, so its caption chip escaped to the
    initial containing block and rendered on the hero 5,000px above itself. The
    .reveal transform masked this until .in set transform: none.
  • The mobile menu's only CTA was unreachable. .nav-foot was declared
    z-index: 43 then re-declared 39, and .nav-links is 42 with an opaque
    background, so elementFromPoint at the button's centre returned
    .nav-links. Keyboard could reach it; a thumb could not.
  • Merged the evidence chain and the agent split into one inverted band. They
    were two sections on the same --surface tone separated by a 1px hairline,
    reading as 1,030px of a single flat empty block in the stretch of the page
    that already had the least happening. All six text roles pass AA on the new
    ground.
  • Display numerals moved from Geist Mono to Archivo. Mono gives the comma a
    full monospace advance, so the headline figures read as "21 , 186".
  • heroSettle now starts at exit 62%. It had the hero block at opacity 0.22
    by scrollY 700 of a 900px hero, dimming the primary CTA while it was still on
    screen and still being read.
  • Hero <source> order flipped to mp4 first. That webm encode is 2.34MB
    against the mp4's 1.74MB, so order was serving every Chrome and Firefox
    visitor 34% more bytes for the heaviest asset above the fold.

Asset upload safety

website/wrangler.toml sets [assets] directory = ".", so every file beside
the page is upload-eligible — including .env, which holds a live
OPENROUTER_API_KEY. Automatic dotfile exclusion was Pages behaviour; Workers
static assets uploads whatever is in the directory. Added .assetsignore.

Deploying from a directory containing only page content is still safer, since
that list has to stay in step with whatever lands in the folder.

Verification

Deployed and checked on projectvelocity.org: all fixes present, and the
sensitive paths return 404 with an empty body — /.env, /CLIENT-NOTES.md,
/wrangler.toml, /scripts/generate-bands.mjs.

Untouched and still open: the workspace grid still uses static screenshots
rather than hover video, there is no artifact in the evidence band, and three
domain thumbnails still show imagery borrowed from other domains.

Inverting the page to a light base moved --txt to near-black without
re-checking the rules that paint text onto a dark surface. Six of them had
a duplicate declaration overriding a correct earlier one:

  - .nav-links a resolved to --txt-2 over the hero (2.4:1). Colour now lives
    on .nav / .nav.scrolled, and the base rule no longer sets it.
  - .tile b declared no colour at all, so the six app names in the workspace
    grid inherited near-black onto dark screenshots at 1.0:1 while their
    lighter subtitles read fine. The figcaption ramp also faded out above the
    title; it now stays opaque where the name sits.
  - summary:hover was #fff on the white page, so hovering a FAQ question
    erased it.
  - details[open] .x kept --txt-2 on #202730, leaving the one element that
    carries open state with no visible state.
  - :focus-visible used the near-black --bone over the hero (1.1:1) across
    the first nine tab stops.
  - The wordmark and the hamburger bars stayed light inside the opened mobile
    menu, against a --void panel.

Drop the .bleed::after blend veil. Its top stop was opaque --void, which put
a white wash over the first 121px of the console recording, and its bottom
stop covered another 167px: 288px of 758px, and precisely the zones the app
draws its live status bar and its timeline in. The chip carries its own
scrim, so the gradient bought nothing. Bands butt instead.

Make the scroll reveals additive rather than load-bearing. All 43 blocks
started at opacity 0 with no scripting fallback, so a visitor with JS blocked
got three background images and nothing else; and because the observer
unobserved on first intersection, any block the viewport jumped over stayed
invisible for good. An End-key press stranded eleven, including a 560px band
that rendered as pure white. The hidden state is now gated on a .js class set
inline, and the observer is idempotent instead of one-shot.

Also:
  - .shot had no position:relative, so its caption chip escaped to the
    initial containing block and landed on the hero 5,000px above itself.
    The .reveal transform hid this until .in set transform:none.
  - Merge the evidence chain and the agent split into one inverted band. They
    were two sections on the same --surface tone separated by a 1px hairline,
    which read as 1,030px of a single flat empty block in the stretch of the
    page that already had the least happening.
  - Set the display numerals in Archivo. Geist Mono gives the comma a full
    monospace advance, so the headline figures read as "21 , 186".
  - Start heroSettle at exit 62%. It had the hero block at opacity 0.22 by
    scrollY 700 of a 900px hero, dimming the primary CTA while it was still
    on screen and still being read.
  - Put the hero mp4 before the webm. That webm encode is 2.34MB against the
    mp4's 1.74MB, so source order was serving every Chrome and Firefox
    visitor 34% more bytes for the heaviest asset above the fold.
website/wrangler.toml sets [assets] directory = ".", so every file beside the
page is a candidate for public upload — including .env, which holds a live
OPENROUTER_API_KEY, plus the client notes, the build plan and the image
generator scripts.

Pages used to exclude dotfiles and node_modules automatically. Workers static
assets does not: it uploads whatever is in the directory unless .assetsignore
says otherwise, so this file is the only thing standing between the key and
projectvelocity.org/.env.

Deploying from a directory that contains only page content is still the safer
habit, since this list has to stay in step with whatever lands in the folder.
@AndrewCTF
AndrewCTF merged commit 9b7afa8 into frontend-crash-optimize Jul 25, 2026
1 of 2 checks passed
@AndrewCTF
AndrewCTF deleted the site-contrast-motion-fixes branch July 25, 2026 15:47
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