Skip to content

DESIGN.Www

WarmlyYours WWW is the public-facing marketing and e-commerce site at warmlyyours.com. It sells radiant heating into a residential audience, so the visual identity leans warm, premium, and confidence-inspiring — not technical or corporate. The single non-negotiable brand element is the burgundy primary (#983333); everything else is interchangeable scaffolding around it.

The system layers Bootstrap 5.3 underneath bespoke SCSS in client/stylesheets/www/. Bootstrap is the structural skeleton (grid, buttons, forms, breadcrumbs); brand identity is layered through the variables in 00-variables/ and the components in 03-components/. Treat the tokens here as the canonical source — when SCSS and this file disagree, the SCSS is drift.

There are two typographic registers:

  1. Brand pages — sofia-pro (sans) for everything; orpheuspro (serif) only for occasional display moments where warmth needs to be amplified.
  2. Blog and long-form editorial — Inter for body, Playfair Display for headings. Self-hosted; do not reach for Google Fonts.

The palette has three layers:

Identity. primary (Rich Burgundy #983333) is the brand. It anchors buttons, links, headings, and primary surfaces. primary-hover is +10% lightness and is the only sanctioned hover treatment for the primary.

Neutrals. light-shade is the canonical page background — warmer than pure white, deliberately. Body text is dark-shade. Secondary/muted text should use gray-700 (the lowest gray that clears WCAG AA 4.5:1 on light-shade); avoid lighter grays for text. dark-accent is for body-on-body contrast, not text on light surfaces.

Mood palette. mint-green and warm-cream are page-section moods. They are not arbitrary accents — pick one to set tone for an extended section, then let the rest of the section inherit. cream is a translucent overlay tint for gradients, not a surface color; don’t set it as a section background.

The mood palette is deliberately short. muted-navy-blue, earthy-taupe, and mint-greener lived here as aspiration and shipped to zero consumers — they were removed rather than left as tokens nobody honored. Adding a mood means adding a page-section-* component alongside it, not just a hex.

Warm sub-palette. warm-copper / warm-copper-dark / warm-cream / warm-cream-light are consumed as CSS custom properties (var(--warm-copper)), declared in 00-variables/_01-colors.scss and used from ERB — they are the one part of the palette you won’t find via a $var grep. warm-copper is a fill color: 3.53:1 on cream, so it fails AA the moment it becomes text. warm-copper-dark is its text counterpart at 6.46:1. Cream pairs the same way — warm-creamwarm-cream-light is the sanctioned 180° gradient.

The numeric gray-100gray-900 scale exists for borders, dividers, and disabled states. Reach for it before inventing new hexes. The scale is piped into Bootstrap’s $grays map in client/stylesheets/bs.scss, which is why several entries carry no component of their own — they are consumed by Bootstrap utilities, not by a named component. An unreferenced grey is not a dead token here.

Sofia-pro and orpheuspro are the brand pair. Sofia-pro’s regular weight in this system is 300 (light) — that is intentional and gives the marketing surface its airy feel. 500 is “medium” (the actual semibold), used for headings and emphasis. 600 is reserved for buttons and small UI moments where sans-serif needs to feel solid.

Orpheuspro is a serif tucked in for display moments — hero subheads, pull quotes, occasional editorial sections. Don’t use it for running body copy.

Blog content uses Inter + Playfair Display, self-hosted from public/fonts/inter/ and public/fonts/playfair-display/. These are variable fonts; the weight ranges (400 600 for Inter, 400 700 for Playfair) load once and cover the whole spectrum. Don’t add additional weight files — use font-weight to interpolate.

font-display: swap is set on every face. Keep it that way — FOIT is worse than FOUT on a marketing site.

Bootstrap 5.3 grid with one custom breakpoint added: xxl at 1600px (default Bootstrap stops at xl: 1200px). Container max-widths likewise extend to 1540px at xxl. The intent: hero imagery and showcase grids breathe on large monitors without requiring a per-page max-width override.

Spacing scale follows Bootstrap’s 0–5 ladder (0 / 0.25 / 0.5 / 1 / 1.5 / 3 rem). Levels 6 and 7 (4.5rem, 6rem) are aspirational extensions for section spacing on marketing landing pages where Bootstrap’s 5 (3rem) is too tight. Use them for vertical rhythm between full-page sections, not for inter-component spacing.

A second -em track exists in SCSS (1-em, 2-em, etc.) for components whose padding should scale with their own font-size. Prefer rem-based spacing unless you’re authoring a typographic component.

Flat by default. The only sanctioned elevation moments:

  • Cards lift via background contrast against light-shade, not shadows.
  • Modal/overlay uses Bootstrap’s default box-shadow scale.
  • Sticky headers use a 1px bottom border in gray-200, not a shadow.

If you reach for box-shadow on a content surface, reconsider — the brand voice is calm, not floating.

Subtle, not pill. rounded.md (0.3rem) is the default for buttons, inputs, selects. rounded.lg (0.5rem) is reserved for promotional cards (warm palette). Pills are only for badges and tags.

Image thumbnails are explicitly square (rounded.none) — that is a catalog-imagery decision, not an accident.

The button hierarchy is:

  1. Primary — burgundy, on the warm body color. One per significant CTA moment.
  2. Secondary — sage (light-accent) with dark label text. Light text on sage is 3.00:1 and fails WCAG AA; dark-shade is 4.92:1. For “alternative” or “learn more” alongside a primary.
  3. Link — text-only burgundy. For tertiary or in-flow actions.

Forms use Bootstrap’s standard treatment with the brand layered: white-ish backgrounds (on-primary), brand burgundy is not the focus ring color — focus uses Bootstrap’s blue (#86B7FE) at 2px to avoid red-on-red ambiguity on validation states. This is a deliberate exception to the “primary everywhere” pattern.

The custom checkbox/radio treatment is unusually large (1.5rem) by spec and uses success green (not primary burgundy) for the checked state. Keep it.

Breadcrumbs sit on light-shade with subdued type and no underline; they’re navigational but should never compete with the H1.

Page sections are themed by background (page-section-* tokens). Pick one mood per section; do not chain three section themes vertically — that breaks rhythm.

Every interactive element expresses five states. Only hover is brand-themed; the rest are Bootstrap 5.3 defaults on purpose.

  • Hoverprimary-hover (#B43E3E) on primary, dark-shade on links. Hover is a lightness shift, never a new hue.
  • Focus — Bootstrap’s blue ring (#86B7FE, 2px). Brand burgundy is deliberately not the focus color. Never remove the ring; use :focus-visible if a mouse-click ring reads as noise.
  • Active/pressed — Bootstrap default. The only sanctioned override is the suppressed box-shadow on .btn-outline-primary.
  • Disabled — Bootstrap default (opacity: .65). Do not author a custom disabled palette. Guard hover rules with :not([disabled], .disabled), as 01-base/buttons.scss does.
  • Loading — Turbo owns it (shared/_turbo.scss). Don’t hand-roll button spinners.

Motion is functional, never decorative. Transitions use Bootstrap’s .15s ease-in-out; anything slower feels sluggish on a commerce page.

Smooth scrolling and reveal animations sit behind @media (prefers-reduced-motion: no-preference) in 02-objects/animations.scss. Every new animation either lives inside that guard or ships its own prefers-reduced-motion: reduce opt-out.

Font Awesome 7.2 Pro, Sharp onlysharp-solid, sharp-regular, and brands are the bundles loaded by www.index.scss. Classic, Light, Duotone, and Thin are not bundled and render as blank boxes.

Emit icons through IconHelper#fa_icon (<%= fa_icon('gear') %>), never a raw <i class="fa-…"> — the Heatwave/NoInlineFaIcon RuboCop cop enforces it. Custom marks are inline SVGs from app/assets/images/svgs/custom/. Don’t introduce a second icon set.

Mobile-first. Bootstrap 5.3 breakpoints with one custom tier (00-variables/_03-grid.scss):

Tier Min-width Container
sm 576px 540px
md 768px 720px
lg 992px 960px
xl 1200px 1140px
xxl 1600px 1540px

lg (992px) is the desktop boundary — navigation and multi-column layouts collapse below it, and secondary nav becomes an offcanvas drawer (see layouts/www/pages/account_portal.html.erb). Design the collapsed state first; the xxl tier is breathing room for hero and showcase grids, not a layout the page depends on.

Touch targets are 44 × 44 px minimum on any pointer-coarse surface. Where the visual control is smaller — the 1.5rem checkbox, for instance — the label or a wrapping anchor supplies the hit area. Don’t ship a tap target that is only as big as its icon.

Never hide content at a breakpoint to “fix” a layout. Reflow it, stack it, or move it behind a disclosure — d-none on real content is a mobile bug.

There is no dark mode. Bootstrap 5.3 ships color modes and we do not use them; nothing in the codebase sets data-bs-theme or reads prefers-color-scheme.

Do anchor every page in light-shade background and dark-shade body text. The warm off-white is the brand’s most recognizable neutral.

Do use gray-700 for muted/secondary text. Anything lighter fails WCAG AA on the warm background.

Do use named palette colors (mint-green, warm-cream) as section moods, not as one-off accents inside other sections.

Do lean on Bootstrap utilities (text-primary, bg-light, spacing classes) before writing new SCSS.

Do emit every icon through fa_icon, in the Sharp family. Raw <i> tags fail the Heatwave/NoInlineFaIcon cop.

Do design the ≤992px layout first, and give every tap target 44 × 44 px.

Don’t introduce new hex values for greys — the gray-100gray-900 scale exists for this. New grey tokens in component SCSS are drift.

Don’t write a raw hex in an ERB style= attribute. If the color is real it has a token; reach for var(--token) at minimum, a class by preference.

Don’t put text on warm-copper, and don’t use it as the light end of a gradient that carries text — 3.90:1 with white. It’s a fill. Text goes in warm-copper-dark (7.13:1 with white).

Don’t use Bootstrap’s deprecated color utilities. All three are gone from this codebase — reintroducing one is drift, and they’re removed in Bootstrap 6:

Deprecated Since Use instead
.text-muted 5.3.0 .text-body-secondary
.text-white-50 5.1.0 .text-white + .text-opacity-50
.text-black-50 5.1.0 .text-black + .text-opacity-50

.text-body-secondary is the muted-text default on both surfaces. It tracks --bs-secondary-color (the body color at 75%) rather than a fixed grey, so it stays correct if the body color moves, and it clears AA everywhere here: 6.38:1 on mint-green, 6.63:1 on warm-cream, 6.45:1 on cream, 6.68:1 on light-shade.

.text-gray is ours, not Bootstrap’s, and still pins gray-700 in 01-base/typography.scss. Reach for it only when you specifically want that fixed grey rather than secondary body text.

Don’t use orpheuspro for running body copy. It’s a display face.

Don’t add a third typographic register. Brand vs. blog is the boundary.

Don’t override the form focus ring with brand red. Brand red on a validation-red error state is unreadable.

Don’t stack box shadows. If a surface needs to lift, raise the background contrast or add a 1px border instead.

Don’t style a custom disabled state. Bootstrap’s opacity: .65 is the convention, and hover rules must exclude [disabled].

Don’t animate outside a prefers-reduced-motion guard.

Don’t add dark mode or data-bs-theme. The surface is light-only.

Don’t use d-none to make a layout fit a small screen. Reflow instead — hidden content is a mobile bug, not a breakpoint fix.