Skip to content

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.jsclassifySectionTone(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)

  1. Normal pathlib/cms/seed-from-build.js makeToneLookup() + transformer.js transformLayoutMap(map, { toneFor }). When local body.html artifacts exist, join capture→blocks by Wix comp-* id, falling back to content-section order index (_matchPosition). Stamps block.tone (+ _bg audit hex).
  2. In-place pathlib/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 Section CVA + 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 audit underHex is kept.
  • Per-site static builds (assembler-fulldev) consume the same Section primitive and share the Tailwind-scan gap; the fix there is a follow-up.