Skip to content

feat(design): retune the palette and compose /account - #202

Merged
guarzo merged 3 commits into
mainfrom
worktree-design-reshape-account
Aug 10, 2026
Merged

feat(design): retune the palette and compose /account#202
guarzo merged 3 commits into
mainfrom
worktree-design-reshape-account

Conversation

@guarzo

@guarzo guarzo commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Feedback from a user was that the app "feels heavily AI generated", with the
colour scheme named specifically. This is the answer to that on /account,
which is the surface a member actually visits. Two commits, split so the colour
change is revertable on its own.

Screenshots (desktop + phone), and the measurements behind the palette:
https://claude.ai/code/artifact/f3c1c530-c41d-4eba-a61d-1c28c7a96891


Known defect, shipping deliberately

The per-row disclosure marker is stranded. Each manifest row ends with a
bare + at the far right of the table, ~490px from the name it expands,
because NAME is the elastic column. Ten of them float in an empty column.

It is cosmetic, not functional — a + at the end of a row is a conventional
affordance and the control keeps its aria-label. I originally called this a
breach of DESIGN.md ruling R4 and that was overstated: R4 governs facts
living in only one channel, and "there is more here" is stated visually by the
marker itself. The real complaint is that it looks unresolved.

Two fixes were built and reverted, both measurably worse than the defect:

attempt result
Lead the row with ACTIONS (+ [portrait] Name) +52px forced horizontal scroll at 320px: 134 → 186 against a 170 tripwire
Make NAME fit-width so ACTIONS takes the slack Starves the long-location case: 352px wanted, 151px measured

The route that works is moving the toggle inside the NAME cell and dropping the
ACTIONS column, which needs the leadCells prop split across page.tsx and
character-row.tsx. Recorded in globals.css under .row-toggle--actions and
in the <colgroup> comment in page.tsx, with both measurements, so the dead
ends are not re-derived. Follow-up work, not a blocker.


1. Palette (0965bd6)

The old ground was the second reflex, not the first. This design correctly
refused cyan-on-black sci-fi, then landed on navy-with-a-gold-accent — the
most-generated "premium dark tool" palette in circulation. That is what the
feedback was naming.

The ground is now a neutral near-black whose tint scales up with lightness, and
the ramp is the rule rather than an oversight. "Tint every neutral toward the
brand hue" only holds for mid and light neutrals; at near-black a warm hue reads
as brown. Two warm grounds were built and rejected on exactly that:

pass --void rgb R/B
navy (before) #080f1f 8, 15, 31 0.26
warm, first try #0e0906 14, 9, 6 2.33
warm, halved #0c0a08 12, 10, 8 1.50
neutral (now) #0a0a0a 10, 10, 10 1.00

Red-minus-blue is the wrong statistic at these luminances — a gap of 4 is
invisible at rgb(200) and a 50% cast at rgb(12). That mistake is why it took
three passes, and the rule is now written down in DESIGN.md.

Also in this commit:

  • Healthy states stop being green. DESIGN.md's status-token rule has always
    said colour is only for actionable state; .st--ok was where the shipped UI
    contradicted it. Every ok chip, every LINKED, every "all healthy" count
    rendered full-chroma green. A screenful of green dots reporting that nothing
    needs doing is the generic-dashboard signature.
  • --signal-bad lifts to 0.66. At 0.64 it measured 4.42:1 against
    --hull-hi — under the AA floor exactly when the pointer is on the row, the
    same class of bug as the disabled-opacity one already recorded in DESIGN.md.
  • public/brand/hero-account.webp is a modified asset. Recut for a dark
    ground (lightness −40); at full brightness it was the lightest object on any
    screen in the app. Modification is cleared by Faoble, and the master is in git
    history. The same treatment was tried on the 34px header mark and rejected —
    the gold rope goes out and the disc turns to mud.

Every text token was measured against all three grounds rather than asserted:
worst case --ink-faint at 4.63:1 on a hovered row, nothing under 4.5:1.

2. Composition (aa95d13)

The page had a dead half. .page is a 78rem column but nothing used more
than the manifest's 48rem, so ~470px sat empty at 1440px while the page ran long
vertically. "Sync schedule" and the closing illustration are material a member
reads but does not operate, so they now sit in a rail beside the manifest.
Collapses to one column below 64rem, manifest first. .page and
--measure-page are untouched, so the one-column origin (H1 left edge, rule
origins, header seal on one vertical across every route) is unchanged.

The manifest contradicted its own header. isNominal required onMapAcl,
while this page's own comments and core/account-health.ts both hold that map
membership cannot substantiate a fault. So a member on no map was disqualified
from the collapsed treatment and every row recited token ok, standings ok, map off — underneath a head reading "10 characters — all healthy". crewNorms
measures deviation against the crew rather than an absolute ideal: a fact every
character shares is one fact about the account, said once in the head. Parity
holds — the head gained "no characters on the map", and the table's <caption>
now says where the fact lives rather than promising it per row.

Reviewer focus

  • crewNorms edge cases: empty crew, single character, mixed, uniform-off with
    some rows faulted. Unit-covered in tests/account-page.test.ts.
  • The four specs rewritten because crewNorms changed what they measure — each
    should still guard its original intent, not just be green. The alignment test
    seeds a payout so two rule heads remain in the main column; without it the
    count guard had to drop to > 0 and the "several headings share one edge"
    half of the claim stopped being tested.
  • .account-layout escapes the :where() cap on class specificity. Worth a
    look that it cannot leak to other routes.

Verification

Run against the final state of the branch:

  • npx playwright test358 passed
  • npm test1329 passed (83 files)
  • npx tsc --noEmit — clean
  • npx eslint . — clean
  • npx prettier --check . — clean

Out of scope, but you will hit it

e2e/provision.ts:95 polls pg_isready inside the container. During
initdb the Postgres entrypoint runs a temporary server on the unix socket
only, so it reports ready while TCP is not serving; that server then restarts
and the migration connects into the shutdown window, failing with Connection terminated unexpectedly on CREATE SCHEMA IF NOT EXISTS "drizzle". It cost
four consecutive failed runs here and will look like flake in CI on a loaded
machine. Not touched in this PR.

guarzo added 2 commits August 9, 2026 22:51
Feedback was that the app reads as machine-made, with the colour scheme
named specifically. The old ground was the second reflex, not the first:
this design correctly refused cyan-on-black sci-fi, then landed on
navy-with-a-gold-accent, which is the most-generated "premium dark tool"
palette in circulation.

The ground is now a neutral near-black whose tint scales up with
lightness, and the tint ramp is the rule rather than an oversight.
"Tint every neutral toward the brand hue" only holds for mid and light
neutrals; at near-black a warm hue reads as brown. Two warm grounds were
built and rejected on exactly that before landing here:

  navy      #080f1f  R/B 0.26
  warm #1   #0e0906  R/B 2.33  (chroma copied from the navy's 0.035)
  warm #2   #0c0a08  R/B 1.50  (halved; red-minus-blue only 4, still brown)
  neutral   #0a0a0a  R/B 1.00

Red-minus-blue is the wrong statistic at these luminances — a gap of 4 is
invisible at rgb(200) and a 50% cast at rgb(12). Judge a ground by its
hex, and at near-black by the ratio.

Healthy states stop being green. DESIGN.md's status-token rule has always
said colour is only for actionable state, and `.st--ok` was where the
shipped UI contradicted it: every ok chip, every LINKED, every "all
healthy" count rendered full-chroma green. A screenful of green dots
reporting that nothing needs doing is the generic-dashboard signature.

`--signal-bad` lifts to 0.66 because 0.64 measured 4.42:1 against
`--hull-hi` — under the AA floor exactly when the pointer is on the row,
the same class of bug as the disabled-opacity one already recorded.

The account illustration is recut for a dark ground (lightness -40); at
full brightness it was the lightest object on any screen in the app. The
same treatment was tried on the 34px header mark and rejected — the gold
rope goes out and the disc turns to mud. Artwork modification is cleared
by Faoble; the master is in git history.

Every text token measured against all three grounds: worst case
--ink-faint at 4.63:1 on a hovered row, nothing under 4.5:1.
…differs

Two problems on /account, both structural rather than cosmetic.

The page had a dead half. `.page` is a 78rem column but nothing in it
used more than the manifest's 48rem, so roughly 470px of the column sat
empty at 1440px while the page ran long vertically. "Sync schedule" and
the closing illustration are material a member reads but does not
operate, so they now sit in a rail beside the manifest instead of
stacked under it, and the page is substantially shorter. Below 64rem the
rail collapses under the manifest, manifest first. `.page` and
`--measure-page` are untouched, so the one-column origin — H1 left edge,
rule origins, header seal on one vertical across every route — is
unchanged.

The manifest contradicted its own header. `isNominal` required
`onMapAcl`, while this page's own comments and core/account-health.ts
both hold that map membership cannot substantiate a fault. So a member
on no map was disqualified from the collapsed treatment and every row
recited "token ok, standings ok, map off" — underneath a head reading
"10 characters — all healthy". `crewNorms` measures deviation against
the crew rather than an absolute ideal: a fact every character shares is
one fact about the account, stated once in the head, not ten times in
the table. Parity holds — the head gained "no characters on the map", so
nothing left only one channel — and the table's `<caption>` now says
where the fact lives rather than promising it per row.

The per-row disclosure drops its visible "actions" caption, which
repeated identically on every row and was the loudest pattern in the
table.

KNOWN DEFECT, recorded in globals.css rather than fixed: that marker now
sits alone at the table's right edge, ~490px from the name it expands.
Two fixes were built and measured, and both cost more than the defect —
leading the row with ACTIONS costs 52px of the 320px forced-scroll
budget (134 -> 186 against a 170 tripwire), and making NAME fit-width so
ACTIONS takes the slack starves the long-location case (352px wanted,
151px measured). The route that works is moving the control inside the
NAME cell, which needs `leadCells` split across page.tsx and
character-row.tsx.

Specs: four rewritten where `crewNorms` changed what they measure, one
rewritten and one added for the rail. The alignment test seeds a payout
so two rule heads remain in the main column — without it the count guard
had to drop to `> 0` and the "several headings share one edge" half of
the claim stopped being tested. `crewNorms` gains unit coverage for the
empty, single, uniform and mixed cases.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1296afbc-743f-40a4-87a7-cec7e64461a1

📥 Commits

Reviewing files that changed from the base of the PR and between a9777e7 and 8beb780.

⛔ Files ignored due to path filters (1)
  • public/brand/hero-account.webp is excluded by !public/**
📒 Files selected for processing (6)
  • DESIGN.md
  • e2e/account.spec.ts
  • src/app/account/character-row.tsx
  • src/app/account/page.tsx
  • src/app/globals.css
  • tests/account-page.test.ts

Comment @coderabbitai help to get the list of available commands.

Five conflicts, all in the crew manifest, all from #199 landing
"let escape back out of the drawer, and drop the column with nothing in
it" in the same two files this branch reshapes.

Every one resolved as both-sides-keep. Nothing from either branch was
dropped:

- `character-row.tsx`: main's `onKeyDown={onEscape}` and its conditional
  ACTIONS `<td>`, plus this branch's icon-only toggle. The two are
  orthogonal — main changed when the cell renders and how it takes a
  keypress, this branch changed what the button says.
- `<colgroup>` / `<thead>`: main's `showActionsColumn` gate on the
  trailing `<col>` and `<th>`, plus this branch's comment recording why
  ACTIONS stays fit-width and trailing.
- The manifest `.map` body and both `colSpan` call sites: main's version
  taken wholesale, since its only code change was `manifestColumns()`
  gaining a second argument and its comment now explains the
  lone-survivor case (`applyNoMainRule` leaves a single character with
  `isMain === false`, so that row keeps a live `make main`). Re-indented
  one level for this branch's `.account-layout__main` wrapper.

`crewNorms` threading survived the largest resolution — verified by
grep rather than assumed, since that block was replaced wholesale.

Verified on the merge result, not on either parent:
  npx playwright test        371 passed
  npm test                   1372 passed
  npx tsc --noEmit           clean
  npx eslint .               clean
  npx prettier --check .     clean

Counts are up from 358/1329 because main brought its own specs, and they
pass against this branch's changes unmodified.
@guarzo
guarzo enabled auto-merge (squash) August 10, 2026 03:34
@guarzo
guarzo merged commit 46d101b into main Aug 10, 2026
7 checks passed
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