feat(store): reconcile globals.css with the approved design tokens #52
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/45-design-tokens"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #45.
What changed
Two commits: the token reconciliation that needed no decision, then the palette
and font swap that #40 gated.
apps/store/app/globals.css— tokens (74e7909)--container1180px -> 1280px.--s10onto the design's--section-gapsteps:--section-y: 44px, 36px in a newmax-width: 1120pxquery, 28px in the existing 700px one. Name stays--section-y..list-cardsdeleted;.cardsis now the one rule for both.apps/store/app/globals.css— palette (a69b09e)--accent#b4471f->#2d543c.--accent-hover: #1d442dand--accent-soft: #e4f3e8are the design's own oklch derivations of the new hue resolved to hex —oklch(from --accent calc(l - 0.06) c h)andoklch(from --accent 0.95 calc(c * 0.35) h). Hex, notoklch(from ...): relative colour syntax needs Chrome 119+, and on a miss the declaration is simply invalid, which for a button background on a static export is not a risk worth taking. I chose hex throughout, consistently — no oklch in the file.--okunchanged at#2d543c;--warn: #8a5a12and--off: #8c3a32added for #47's tri-state chips..stock-badge.is-outnow uses--off. This was the correctness bug: it borrowed--accent, so a green accent renders "Habis" in the available colour.--accent-on-dark: #a9c6b2added, and.service-band .eyebrowmoves onto it from a hardcoded rust#e08a5f— the only hex literal left outside:root, and a rust tint that could not survive the repaint..stock-badge.is-indent { color: var(--warn); }— added in round 2, see below.Round 2: the
is-indentguard (c534fbe)A cross-PR interaction neither this PR nor #53 could see alone. #53 (#41/#42) introduces the
indentavailability state, and its badge routes through the same.stock-badgeelement. This PR repoints.stock-badge.is-outfrom--accentto--off. Compose them and "Indent" renders in the out-of-stock colour on an item you can order — the same class of bug the issue calls out for.is-out, arriving by composition rather than in one diff.Demonstrated, not argued. On the built page at
/products, Author41's class placed next to a real Habis badge:.is-outrgb(140, 58, 50)--off.is-outrgb(140, 58, 50).is-indentrgb(138, 90, 18)--warn, visibly amber against the red-brownBoth render at 12.48px/700 with
background-color: rgba(0, 0, 0, 0), so the new state is consistent with the rest of the sheet.One colour declaration, deliberately, and no tint.
.stock-badgein this sheet is text-only — "absence is the signal" — and.is-outcarries no fill, so hue is already the sole separator between states. A tinted pill onis-indentalone would make it the only filled badge in the file and pre-empt #47, which owns the tinted-background-and-border tri-state chip treatment. This is the dispatcher's call, recorded here so the missing tint is not read as an omission: #52 supplies--ok/--warn/--offplus a singleis-indentcolour rule to prevent a live miscolour; the tinted chips are #47's scope. The badge block's comment now says the same thing in the file.No
.tsxhere.availabilityBadgeClassis Author41's and already pushed on #53 atb1c6681(.tsxonly,globals.cssuntouched). This rule is inert until that lands, so either merge order is safe — each PR carries its own half.apps/store/app/globals.css— typography (a69b09e)h1-h4,.brand,.footer-brand,.price,.button,.cart-button,.cart-count,.qty-stepper buttonget--font-display;bodygets--font-body..eyebrow,.product-type,.image-placeholderdrop fromfont-weight: 900to 700. IBM Plex Sans has no 900; the other 900s sit on Archivo and keep it.h1cap 2.3rem -> 2.5rem, the design's figure.line-height: 1.15andfont-variant-numeric: tabular-numson.priceuntouched.button, input, select, textarea { font-family: inherit }. See Deviations.apps/store/app/layout.tsx—Inter->Archivo+IBM_Plex_Sansvianext/font/google,subsets: ["latin"],display: "swap", exposed as--font-display/--font-body. Both variable, so one file each covers 100-900 and 100-700. This is the only.tsxchange in the PR.The grid question, decided: 170px stays
Recorded as the issue requires. The design's
minmax(200px, 1fr)is looser than what shipped, andbed5b85narrowed 200px -> 170px after #33 landed — it is the newer deliberate call, not drift. Measured in the browser at 1440px on the built page:.product-gridresolves to six 195px cells. At 200px the same row holds five. Losing a sixth of a desktop row to match a mockup figure is a regression, so the design's cell is declined and the deviation is recorded here.The other two table rows in the issue landed as drawn:
--container1280px, section rhythm 44/36/28. Radii keep--r-sm/md/lgper the settled instruction —--r1/r2/r3is 8/12/16 under different names, and renaming touches every rule for zero visual gain.The
.service-bandghost-button checkThe issue asked me to look for the mockup's
.rail-card .btn--ghostdefect (dark green on the dark band, ~1.3:1) before copying anything. Two findings:.button.secondarywas already safe..service-band .button.secondaryoverridescolorto--surface, so it is white-on---darkat 15.62:1. Nothing copied, nothing to fix.--accentmeasures 1.82:1 against--dark: #1b2620. Its white label stays legible (8.59:1) but the fill itself vanishes into the band, so the control reads as flat text and loses all hierarchy against the outlined secondary next to it. Rust did not do this; green does. Fixed by inverting on dark —--accent-on-darkfill,--inklabel — which is the same move the design's.section--inkmakes. Screenshot below.--focusre-checked, kept#1b4d8fvs the green measures 1.02:1 — near-identical lightness (oklch L 0.42 vs 0.41). Kept anyway, on three measurements: 101 degrees of hue separation (a blue/green pair, not a confusable one), 8.38:1 against white so the ring reads on the page, andoutline-offset: 4pxguarantees 4px of page background between ring and any accent fill, so they never touch. Verified visually — the ring reads as blue against a green-bordered tile..category-tile:hovervs the ready chipMeasured
#e4f3e8fill plus an--accentborder. #47's ready chip will share that tint, but this is the design's own reuse:assets/tokens.cssputs--accent-softon both.cat-tile:hoverand.tag--ready. They separate by scale and border, not tint, and a full-tile hover surface does not read as an inline pill. No change.Verified
No workflow triggers on
pull_request— both.forgejo/workflows/*.ymlfire only onpushtomainplusworkflow_dispatch, and both deploy. The forge will never check this PR. Every green light below is a command I ran locally. Round 1 was verified ata69b09e; the round-2 delta was re-verified atc534fbe— build 36/36 routes,biome checkon the two changed files still 2 warnings (the pre-existing pair, unchanged count), badge colours measured on the rebuilt page as tabled above.The build needs a catalog endpoint, so I served the 20 published items from the production management API as a local JSON fixture on
:4321(two flipped toinStock: falseso the "Habis" path actually renders). Nothing in the repo was touched for it.Those 2 warnings are the pre-existing
.image-placeholderdescending-specificity pair.biome checkon the same file atorigin/mainreports the same 2 — my diff adds none. No repo-wide format or lint run; changed files only.Fonts, checked against the built output, not the source:
Zero references anywhere in
out/. 75KB of preloaded latin woff2 for two families; Inter alone was 48KB, so two families cost +27KB.@font-facein the emitted CSS:Archivo 100 900andIBM Plex Sans 100 700, bothsrc: url(/_next/static/media/...).Visual verification — built the export, served
out/on:4400, drove it with a real Chromium at 1440x1000 and 375x812 across/,/productsand/products/bridgestone-ecopia-ep300-205-65r16.Computed values off the live page at 1440px:
--accent/--accent-hover/--accent-soft#2d543c/#1d442d/#e4f3e8--warn/--off/--accent-on-dark#8a5a12/#8c3a32/#a9c6b2--container/--section-y1280px/44px,.sectionpadding 44/44.product-gridh1body"IBM Plex Sans", "IBM Plex Sans Fallback", Arial.pricetabular-nums.stock-badge.is-outrgb(140, 58, 50)=#8c3a32, vs.product-typeatrgb(45, 84, 60)<link>to googleapis/gstatic in DOMWhat I actually saw, per viewport:
1440px,
/— green throughout: "Keranjang" cart pill, the "Cari" submit, every.eyebrow, every.product-typelabel. Six product cards per row. On the Bridgestone EP300 card, "Habis" sits underRp 1.110.000in a clear red-brown, unmistakably a different hue from the greenBANlabel two lines above it — this is the bug from the issue, and it renders correctly. h1 "Beli sparepart, bisa sekalian dipasang." breaks over two lines in Archivo at 40px; the retail scale holds, no cramping from the retained-0.02emtracking.1440px, service band — "SEKALIAN SERVIS" eyebrow in light green (was rust), fully legible. "Lihat layanan" is now a light-green filled pill with a dark ink label and reads unambiguously as the primary; "Pesan barang khusus" is the white-outlined ghost beside it. Before the inversion the primary was a dark green fill on a near-black band and the two were nearly indistinguishable.
1440px, focus + hover — focusing
.category-tilegivesrgb(27, 77, 143)3px at 4px offset: reads as blue against the tile, no confusion with the accent. Hovering the same tile givesrgb(228, 243, 232)with a green border — quiet, tile-scale, not chip-like.1440px,
/products— 6 columns, both "Habis" badgesrgb(140, 58, 50), active "Semua" filter chip green with a white label, inactive chips white with muted text.1440px, detail page — the user's fixes measured live: detail
.price30.4px vs.product-detail .product-card .price18.4px withpadding-top: 8px, so the price hierarchy and the related-card sizing are both intact;.product-gallery .image-placeholdermeasures 450x300, exactly 3:2 with the 300px cap engaged.375px,
/—--section-ysteps to 28px..photo-stripis two 163.5px columns with the third image on its own row: the user's tablet pairing holds down to mobile..product-gridand.category-tilestwo-up. h1 28.8px at the clamp floor, line-height 33.12px (1.15). Service band stacks, both buttons legible.375px,
/productsand detail — filter chips and the search input both render"IBM Plex Sans"(they rendered Arial before, see Deviations). Detail page single column, price 30.4px Archivo, "Habis" red-brown directly beneath it, placeholder 343x229 = 3:2, sticky buy bar green.The user's three committed fixes from
0d6a787are all present and effective — nothing in this diff overrides any of them, and all three were measured on the rendered page above:.product-detail .product-card .priceat line 630 — 18.4px /padding-top: 8px/margin: 0, confirmed live..product-gallery .image-placeholder3:2 +max-height: 300pxat line 657 — rendered 450x300..photo-striptwo columns in the 900px query — rendered as 163.5px x 2 at 375px.Merging this deploys to production. The push to
maintriggersdeploy-store.yml, which runswrangler pages deploy. This is a live repaint of the storefront, not a staged change.Deviations
.product-gridkeepsminmax(170px, 1fr); the design's 200px is declined. Measured: 6 cells per desktop row at 170px, 5 at 200px. Rationale above.--r-sm/md/lg. Per the settled instruction — identical values, rename touches every rule and every future rebase for no visual gain.--section-y, adopts the design's values. 44/36/28 match--section-gapexactly.--section-ydescribes symmetric block padding, which is what the token does; "gap" does not.oklch(). Stated as a choice the issue allows. The whole file is hex — no mixed convention.button, input, select, textarea { font-family: inherit }. Form controls never inherit a font family frombody, andorigin/main'sglobals.csssetsfont-familyin only three places (bodyand twoinherits on.qty-stepper input/.cart-field). So.product-searchand the.product-filterchips have rendered UA Arial since #33 — hidden while the body font was Inter, obvious the moment it became Plex. I judged a body-font swap that leaves the most prominent controls on/productsin Arial to be an incomplete swap, and the design setsfont: inheriton.btn,.chipand.search inputfor exactly this reason. Six lines, no cascade effect on the Archivo group (.qty-stepper buttonis more specific). Say the word and I will drop it..service-band .button:not(.secondary)inverts on dark. Not requested as a change, but the issue asked me to check that band for the ghost-button defect, and the green accent creates one at 1.82:1. Reverting this leaves a primary CTA that reads as plain text.apps/store/app/services/page.tsx:27still carriesclassName="cards list-cards"after the.list-cardsrule was deleted — a dead class token with no rendered effect. Not touched: #45's acceptance is explicitly "every existing class keeps working with no.tsxchanges, same as #33", withlayout.tsxfor the fonts as the sole exception, and #48 rewritesservices/page.tsxwholesale — editing it here creates an overlap for zero visual difference. Main is carrying it into #48's task as a required cleanup, so it is not lost.fetchCatalogneeds a liveGARAGE_API_URL; management is not running locally and my local key is not the production one (401). I served real production catalog data as static JSON on:4321. No repo file, no dependency, nothing committed.No new dependencies. No lockfile change. No debug output. No formatter run.
git show --staton both commits touches onlyapps/store/app/globals.cssandapps/store/app/layout.tsx.VERDICT: approve-with-nits
Reviewed by rebuilding
a69b09ein a throwaway detached worktree and driving thebuilt export in Chromium at 1440px and 375px. Catalog fixture: I pulled the live
production inventory through the management MCP, mapped it through the same
projection
apps/management/lib/catalog.tsuses (published + slug only,sellPrice > 0else null,
inStock = stock > 0), flippedbridgestone-ecopia-ep300-205-65r16andoli-shell-helix-hx7-5w-40-4ltoinStock: false, and served it on:4521.Nothing committed, nothing in the repo.
Findings
apps/store/app/services/page.tsx:L27: nit: 'className="cards list-cards"' still carries 'list-cards' after the rule was deleted, so the markup names a class no stylesheet defines. Either drop the token here or leave it for #48, which restructures /services anyway — but it should not be discovered by the next person grepping for a rule that no longer exists.No blockers. No
should-level findings.The correctness bug — confirmed independently
.stock-badge.is-outrendersrgb(140, 58, 50)=#8c3a32=--off, measured offthe live page in four places: the
/productsgrid (both out-of-stock cards), the/featured grid, and the PDP at 1440px and 375px. The in-stock PDP badge rendersrgb(45, 84, 60)=--ok, so both states resolve to their intended token.Checked for overrides rather than trusting the declaration:
globals.csscontainsexactly two
.stock-badgerules — the base at L495 (--ok) and.is-outat L501(
--off), the more specific one last. No media-query variant, no!importantelsewhere, and the computed value is the final proof. On the same card,
.product-typereads
rgb(45, 84, 60)two lines above the badge, so "Habis" is unmistakably not theavailable colour.
The user's three fixes from
0d6a787— all three effective on the rendered page.product-detail .pricecomputes 30.4px,.product-detail .product-card .pricecomputes 18.4px withpadding-top: 8pxandmargin: 0. Both branches of the fix survive..product-gallery .image-placeholderrenders 450x300 withaspect-ratio: 3 / 2andmax-height: 300pxengaged (300 = the cap, not the natural 3:2 height at that width).Nothing in this diff overrides any of them.
Fonts
grep -rIoh 'fonts\.googleapis\.com\|fonts\.gstatic\.com' out/→ 0. Zero<link>to either host in the live DOM. Emitted@font-faceblocks declareArchivo 100 900andIBM Plex Sans 100 700, bothsrc: url(/_next/static/media/…)— self-hosted and genuinely variable, so the single-file-per-family claim holds.
Preloaded latin woff2 total 75180 bytes, matching your figure to the byte.
Retail scale survived the metric change:
h1computes Archivo 40px / 46px at 1440px(2.5rem cap, 1.15) and 28.8px / 33.12px at 375px.
.pricecomputes Archivo withfont-variant-numeric: tabular-numson the card, the PDP and the cart total.bodycomputes"IBM Plex Sans", "IBM Plex Sans Fallback", Arial.I also checked the weight story you did not spell out: six
font-weight: 900declarations remain, and every one of them lands on an element in the Archivo group
(
.brand,.footer-grid h2,.footer-brand,.cart-count,.availability-note h2,.qty-stepper button). The three you demoted to 700 are exactly the three that renderunder Plex — and 700 is closer to the design, which puts
.eyebrowon--font-bodyat 600. Internally consistent.
The three flagged calls
1. Grid — 170px stays. Accepted. Verified 6 cells x 195.3px at 1440px and 5 at
1120px. Your history claim checks out:
9bc810cshippedminmax(200px, 1fr)andbed5b85narrowed it to 170px afterwards, so 170 is the newer value, not drift fromthe design. The issue permits either number if recorded, the record is accurate, and
the argument stands on its own regardless of history — losing a sixth of a desktop
row on a parts catalogue to match a mockup figure is a real regression. Declining the
design's looser cell is the right call.
2.
--accent-on-dark— not scope creep. I measured every ratio you quoted andthey all reproduce exactly:
--accentvs--dark1.82:1, white label on accent8.59:1,
.button.secondarywhite on--dark15.62:1,--focusvs--accent1.02:1,
--focusvs white 8.38:1. The band's primary really did flatten into thebackground. What settles it for me is that the design's own lighten is not a fix:
oklch(from #2d543c calc(l + 0.08) c h)resolves to#436b52, which measures2.58:1 on
--dark— still invisible. A new value was required.#a9c6b2measures8.48:1 on the band with a 9.08:1 ink label, and the rendered result reads
unambiguously as the primary next to the outlined secondary. The issue told you to
check that band; fixing the defect you found there is in scope.
3.
button, input, select, textarea { font-family: inherit }— keep it. I checkedthe premise instead of taking it:
origin/main'sglobals.csssetsfont-familyinexactly three places (
bodyand twoinherits on.qty-stepper input/.cart-field),so
.product-searchand the.product-filterchips genuinely fell back to the UAfont. After the rule they compute
"IBM Plex Sans"; the cart's qty inputs and the<textarea>do too, while every.button,.cart-buttonand.qty-stepper buttonstill computes Archivo — the class selectors win, no cascade damage. A body-font swap
that leaves the most prominent control on
/productsin Arial is an incomplete swap.Six lines is the right price.
Tokens, rhythm, layout
--warn: #8a5a12and--off: #8c3a32both present and computed off the live root, so #47 is unblocked.--offreads 6.96:1 on--bg,--warn5.91:1 on white.oklch(from #2d543c calc(l - 0.06) c h)paints#1d442d, exactly your--accent-hover.oklch(from #2d543c 0.95 calc(c * 0.35) h)paints#e4f3e9against your#e4f3e8— one unit of blue, i.e. rounding. Choosing hex overoklch(from …)on a static export is the right trade.:root; the rust#e08a5fwas the last one out and no rust value survives anywhere on any route (swept/,/products, a PDP,/services,/cart,/contact,/cara-pesanfor the three old rust RGBs — zero hits).--section-gapincluding the 28px mobile step. The newmax-width: 1120pxblock sits before the 700px block, so the mobile value wins where both match — verified, not assumed.--containercomputes 1280px.--s10still has four consumers, so moving--section-yoff it left nothing dead..list-cardsfold:/servicesrenders 4 columns at 1440px and 3 at 900px, where the old 280px cell gave 2. Denser, one rule, no consumer broken — the only.list-cardsreference left anywhere is the dead class token above.--focuskept: the ring computesrgb(27, 77, 143)3px at 4px offset and reads as blue against a green-bordered tile..category-tile:hovercomputes#e4f3e8with a--accentborder — tile-scale, not pill-like.Evidence audit
Re-ran your two verifiable claims in my own tree:
pnpm --filter garage-store test→
# tests 17 / # pass 17 / # fail 0.pnpm exec biome checkon the two changedfiles →
Found 2 warnings, exit 0, and both are the pre-existingnoDescendingSpecificitypair on.image-placeholder/.cart-line .image-placeholder,neither of which your diff introduces. Build: 40 static routes, compiled clean.
Every number in your PR body that I could independently measure came back identical.
That is the standard this repo needs, given no CI ever looks at a pull request here
and merging deploys the storefront.
Round-2 verification, re-run at
c534fbe— the exact commit up for mergeNot "tests passed earlier". Every command below was run against
c534fbeafter the push.The 2 warnings are the pre-existing
.image-placeholderdescending-specificity pair;biome checkonorigin/mainreports the same 2. No repo-wide format or lint run.Diff scope — all three commits, nothing else touched:
Rendered measurements at
c534fbeRebuilt the export, served
out/, drove a real Chromium at both viewports again://products--accent#2d543c#2d543c#2d543c.stock-badge.is-outrgb(140, 58, 50)rgb(140, 58, 50)rgb(140, 58, 50).stock-badge.is-indentrgb(138, 90, 18)rgb(138, 90, 18)rgb(138, 90, 18)h1bodyfont"IBM Plex Sans""IBM Plex Sans""IBM Plex Sans".product-grid--section-y<link>to googleapis/gstaticThe user's three fixes from
0d6a787, measured on the rendered page at this commit:.price30.4px vs.product-detail .product-card .price18.4px — price hierarchy and related-card sizing both live.product-gallery .image-placeholder450x300,max-height: 300pxengaged — exactly 3:2, no 800px empty square.photo-strip2 x 163.5px at 375px — the tablet pairing holds down to mobileMerging this PR deploys to production. No workflow triggers on
pull_request;deploy-store.ymlfires on push tomainand runswrangler pages deploy. There are no PR checks on this repo and there never were — every number above is a local measurement, not CI.VERDICT: approve
Round 2, reviewing only
a69b09e..c534fbe(7 insertions / 2 deletions,apps/store/app/globals.css). Round 1's findings are not re-litigated and the declined nit is not re-raised.No findings
0 blockers, 0
shoulds, 0 nits.How I measured it
Rebuilt
c534fbein a throwaway detached worktree. The build needs a catalog endpoint, so I re-derived the fixture from live production inventory through the management MCP, mapped it with the exact projectionapps/management/lib/catalog.tsuses (published + non-null slug,category?.trim() || "Lainnya",price = sellPrice > 0 ? sellPrice : null,inStock = stock > 0), flippedbridgestone-ecopia-ep300-205-65r16andoli-shell-helix-hx7-5w-40-4ltoinStock: false, and served it on:4521. Built export served on:4522, driven with a real Chromium at 1440x1000. Nothing committed, nothing in the repo, nothing in the main checkout.is-indentdoes not exist in this branch's.tsx, so I did what you did: constructed the three class stringsavailabilityBadgeClassreturns atb1c6681and injected them as real DOM nodes into a live product card next to the genuineHabisbadge, then readgetComputedStyle. Not a simulated stylesheet — the shipped CSS chunk resolving against real elements in real cascade context.The three colours — all three reproduce exactly
Habis.is-outrgb(140, 58, 50)--off#8c3a32Indent.is-outrgb(140, 58, 50)Indent.is-indentrgb(138, 90, 18)--warn#8a5a12Ready.stock-badge(span)rgb(45, 84, 60)--ok--warncomputes#8a5a12off the live root andrgb(138, 90, 18)is its exact paint. All four badges render 12.48px / 700 withbackground-color: rgba(0, 0, 0, 0),border-width: 0px,padding: 0px— the new state is dimensionally identical to the existing ones, so it introduces no fill and no box.The defect is not hypothetical: I rendered
Indentthrough.is-outand it is the same three bytes asHabis. Composing #53's original approach with this PR's--offrepoint would have shipped an orderable item wearing the out-of-stock colour to production. Splitting the fix across both PRs was the right call.Visually distinguishable. Screenshotted the card with
Habis,IndentandIndent (via is-out)stacked.Habisand the defectiveIndentare indistinguishable; the fixedIndentreads as clear amber against the red-brown. The red channel barely moves (140 vs 138) — the separation is entirely the hue shift, green 58 to 90 and blue 50 to 18. Amber against red-brown is a legible pair at 12.48px/700, and--warnmeasures 5.91:1 on white per round 1.I also checked the tint decision on its merits rather than on the argument:
.is-outpaints no background, so adding one to.is-indentalone would have made it the only filled badge in the file. Colour-only is internally consistent with the sheet. It is recorded honestly in the PR body as the dispatcher's call, and the in-file comment now says the same thing ("The tinted chip treatment is #47's"), so a later reader cannot mistake the missing tint for an omission.Inert on this branch — proven, not assumed
is-indentoccurs exactly once in the entire built export, and that one occurrence is the CSS rule itself in_next/static/chunks/1nzub7-7qa9ou.css. Zero occurrences in any emitted HTML or JS across all 36 routes, against 100 occurrences ofis-outin HTML. No element on any route can match the new selector, so merging #52 before #53 produces zero rendered change versusa69b09e— the rest of the delta is a comment.Confirmed the other half genuinely does not collide:
b1c6681does not touchglobals.cssat all (not in its 23-file changed list) and definesis-indentin no CSS anywhere. This rule is the only definition. Either merge order is safe.Round 1's correctness fix survives the third rule
.stock-badge.is-outstill paintsrgb(140, 58, 50)=--offon the live page, unchanged.Round 1's invariant was "two
.stock-badgerules, the more specific one last". I re-checked it against the emitted CSS rather than the source, by byte offset:Exactly three
.stock-badgerules in the file, base first, both modifiers after. Brace depth at all three offsets is 0, so none sits inside an@mediaor@supports— which also means the badge colour cannot vary by viewport, so re-measuring at 375px would prove nothing the rule enumeration does not already prove. No!importanttouches the badge; the sheet's one!importantis the pre-existing.product-type, a different element, and it computesrgb(45, 84, 60)two lines above the badge on the same card.The two modifiers tie at (0,2,0) with
.is-indentlast, so if both classes ever landed on one elementis-indentwould win.availabilityBadgeClassreturns exactly one of them, never both, so the tie is unreachable. More usefully, both modifiers at (0,2,0) beat.product-card pat (0,1,1) — I verified.is-indenton a<p>inside a real product card, which is precisely how #53 uses it, and it computesrgb(138, 90, 18), not the muted card-paragraph colour. The new rule is robust in the context that will consume it.One thing I probed and cleared: a bare
stock-badgeon a<p>inside.product-cardcomputesrgb(90, 97, 89)=--ink-muted, because.product-card p(0,1,1) outranks the base rule (0,1,0). Unreachable in practice — #53's card returnsnullforreadyand never emits the bare class there, and the PDP uses a<span>, where I measured the base rule winning correctly atrgb(45, 84, 60). Pre-existing, untouched by this delta, not a finding against this PR; noting it only so nobody rediscovers it as a surprise while integrating #53.Evidence audit at this exact commit
Your round-2 numbers are the only verification that exists on this repo, so I re-ran them rather than reading them:
pnpm --filter garage-store build— compiled clean, 36 routes, 24/products/[slug]+ 6/services/[slug]. Matches.pnpm --filter garage-store test—# tests 17 / # pass 17 / # fail 0. Matches.pnpm exec biome check apps/store/app/globals.css apps/store/app/layout.tsx—Found 2 warnings, exit 0, both the pre-existingnoDescendingSpecificitypair on.image-placeholder. Count unchanged, which is worth stating explicitly given the added rule is itself a specificity neighbour of the two it sits beside.Nothing in the delta is a
.tsxchange, so #45's "no.tsxchanges" acceptance is still intact.Good discipline on this one. You found a bug neither PR could see from inside itself, demonstrated it by rendering the broken composition rather than describing it, and split the fix so each half is safe alone in either merge order. Merging deploys the storefront, and this delta is the difference between a correct repaint and shipping an orderable part in the sold-out colour.
Pre-merge verification against the moved base (
22cfddd)mainadvanced to22cfdddwhen #51 landed, so the tree this squash actually produces is no longer the tree round 2 was reviewed on. Verified the combined result rather than assuming it.Method, so this is auditable: rebased
c534fbeontoorigin/mainon a throwaway local branch (39ae526), verified there, then deleted it. Nothing was force-pushed — the PR head staysc534fbe, so review id=11'sapproveis not voided. The merge below is the reviewed commit.Zero file overlap with #51, confirmed by set intersection:
One real interaction, and it is fine
#51 removed Tailwind and deleted
apps/store/postcss.config.mjs, soglobals.cssno longer passes through the@tailwindcss/postcssplugin — it is now processed by Next natively. That is a build-pipeline change under a file this PR rewrites, which is exactly the kind of thing that deserves a check rather than a shrug. Checked: every token emits intact.One emitted stylesheet, and the minifier even folds the 900px query's three selectors into
.product-grid, .category-tiles, .photo-strip— same declarations, no loss.Commands, run on the rebased tree
56 tests, up from 17 — #51's
cart.test.tsjoined the suite and passes alongside this change. The 2 biome warnings are still the pre-existing.image-placeholderpair.Rendered, on the merged tree
//products.stock-badge.is-outrgb(140, 58, 50)rgb(140, 58, 50)rgb(140, 58, 50)rgb(140, 58, 50).stock-badge.is-indentrgb(138, 90, 18)rgb(138, 90, 18)rgb(138, 90, 18)rgb(138, 90, 18)--accent#2d543c#2d543c#2d543c#2d543c--section-yh1bodyfont"IBM Plex Sans""IBM Plex Sans""IBM Plex Sans""IBM Plex Sans".product-grid<link>.service-band .button:not(.secondary)→rgb(169, 198, 178)fill withrgb(23, 32, 24)label. The inversion holds; no 1.82:1 fill on the dark band..product-filter→"IBM Plex Sans", not UA Arial.0d6a787, re-measured on the merged tree: detail.price30.4px vs related-card.price18.4px;.product-gallery .image-placeholder450x300 at 1440 and 343x229 at 375, both exactly 3:2 with the cap engaged;.photo-striptwo columns in the 900px query. All three intact, none overridden.Gate
approveat09:16:43Z, post-datingc534fbe. 0 blockers, 0 shoulds, 0 nits.mergeablere-fetched immediately before the merge call.head_commit_idcompare-and-swap onc534fbeso nothing unverified can slip in. Noforce_merge, nomerge_when_checks_succeed.maintriggersdeploy-store.yml→wrangler pages deploy. There are nopull_requestchecks on this repo; every number above is a local measurement.