ADR-0007 — Section tone: live-captured background → bounded enum → render band¶
Status: Accepted
Date: 2026-07-05
Deciders: Cathal Dempsey
Related: ADR-0004 (adapter pattern / loud-fail), ADR-0002 (primitive
neutrality — tones are props, not per-site CSS), Slice 2 (brand-palette
identification, commit 6919e41)
Context¶
FCR Wix sections carry meaningful background bands — a brand-colour CTA
strip, a grey "muted" content band, a near-black footer-style section.
The replatform dropped all of them: every section rendered on the white
page canvas because Wix section fills come from parastorage CSS classes
we deliberately strip (Wix IP — see memory no-wix-css-copy), so the
fill is only observable on the rendered live page.
We want to recover the band per section without (a) copying Wix CSS or (b) hand-tuning per-site CSS (ADR-0002: visual variation is a component prop, the matcher sets defaults). The variation across the portfolio is wide, so the render lane needs a bounded contract it can style once.
Decision¶
A fixed 5-value tone enum is the contract between capture and render.
default transparent / near-white → no band (renders as today)
muted grey band (any non-brand fill collapses here)
brand site primary (saturated / mid-dark, hue-close to primary)
brand-soft light tint of the primary
dark near-black band
The enum is bounded by contract — off-brand accent fills (e.g. a
yellow-green strip on a cyan-primary site) intentionally lose their hue
and collapse to muted. The render lane styles exactly these five; the
capture lane must never emit anything else.
Capture (live, like theme-extractor)¶
lib/capture-section-backgrounds.js — Playwright crawl at 1440×900 (same
viewport as theme-extractor so computed styles match). For each top-level
section it resolves the true painted background: the section element
itself is almost always rgba(0,0,0,0); the real fill is on a child
wrapper (colorUnderlay / wixui-box) or an ancestor. resolveSectionBg()
walks self + full-coverage descendants in paint order, composites
translucent layers, and detects full-bleed media (image/video-backed
sections must NOT be treated as colour bands — the image is the
background). Output: builds/<domain>/section-backgrounds.json.
Classify (pure)¶
lib/section-tone.js — classifySectionTone(bgHex, primaryHex),
deterministic given (bgHex, primary) so it re-runs over a stored capture
without re-crawling. Thresholds are named per the contract (near-white
≥225/≥245, near-black ≤40, brand-hue window 25°, tint split on
lightness/saturation). No fs/DOM/sharp.
Join (two paths — artifact presence decides)¶
- Normal path —
lib/cms/seed-from-build.jsmakeToneLookup()+transformer.jstransformLayoutMap(map, { toneFor }). When localbody.htmlartifacts exist, join capture→blocks by Wixcomp-*id, falling back to content-section order index (_matchPosition). Stampsblock.tone(+_bgaudit hex). - In-place path —
lib/cms/apply-section-tones.js. For a site whose seed exists but whose build artifacts live elsewhere (garvanbay on 2026-07-05), align the live captured section list against the seed's block list per page: heading → text-containment → blog-tag → gap-tone → positional. Anything unmatched is reported and left tone-less (renders as today) per ADR-0004 loud-fail — never fabricated.
Render¶
packages/components-v3/…/ui/section/section.astro — the Section
primitive maps tone via CVA to Tailwind utilities (brand → bg-primary
text-primary-foreground, muted → bg-muted, dark → bg-foreground
text-background, …) and emits data-slot="section" + data-tone. The
fcr-block wrappers thread node.tone through to it; the emdash
marketing-blocks schema exposes a tone field so an operator can override
per block. Committed as the "render foundation" (commit 30bffd3).
The render-scan gap (found + fixed during verification)¶
The render foundation shipped but did not paint: 4 of 5 tones carried
the right class + data-tone in the HTML yet had no CSS rule, rendering
white. Root cause: apps/cms Tailwind v4 (@tailwindcss/vite) auto-
detects content rooted at apps/cms and ignores node_modules;
@fcr/components-v3 is a node_modules symlink, so tone utilities that
live only in section.astro were never generated. Pre-existing gap (also
dropped bg-card/bg-destructive). Fixed for the CMS by emitting a
Tailwind @source directive from emitGlobalCss (commit 13deb1b).
Verified via production build: CSS 253KB→299KB, all five tone bands
generate. See known-patterns.md "Workspace-package utility classes need
an explicit @source" and known-issues.md (per-site lane follow-up).
Slice tracker¶
| Slice | Scope | Status |
|---|---|---|
| 2b render foundation | Section tone CVA variant + data-tone; fcr-block threading; marketing-blocks tone schema field. |
Done 2026-07-05 (30bffd3). |
| 2b capture lane | Live capture (capture-section-backgrounds.js), pure classifier (section-tone.js), both join paths, garvanbay seed stamped 58/58. |
Done 2026-07-05 (8ee6755). |
| 2b render fix | CMS Tailwind @source for components-v3 so tones actually paint. |
Done 2026-07-05 (13deb1b). Per-site static-build lane still needs it — see known-issues. |
Consequences¶
- The tone enum is a locked contract. Extending it means touching both
the classifier and the
SectionCVA + the schema field — don't add a sixth value casually. - Media-backed sections classify to
default(null hex): the image is the background, a colour band would fight it. Their auditunderHexis kept. - Per-site static builds (
assembler-fulldev) consume the sameSectionprimitive and share the Tailwind-scan gap; the fix there is a follow-up.