Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/projects/2509-css-migration/TASK-TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,29 @@ last-wins gate that caught both real regressions this phase.
accidents make per-line archaeology cost grow while the evidence rule
shows ~zero shipped-byte win (PurgeCSS already strips unused legacy
rules per bundle).
## 🔄 PHASE E IN PROGRESS (2026-07-19, branch css-migration/phase-e-paved-path)

Mandate (Paul): a person must be able to create a new page from scratch
reusing existing components, consistently. Plan + measured facts:
- E1 info-card component: the pp-infobox card pattern repeats ~21x across
home (6: home-service-*-box), services (6: services-card-*), about
(3: about-value-*), careers (6: careers-value-*-box). MEASURED:
home-cto vs services-cto = 24 rules identical modulo class name
(A=36 B=50); variance = home-only :hover colors, services-only
equal-height flex framework + empty-body hover stubs. Protocol:
C1-style intersection -> components/info-card.css keyed on ONE shared
class (jt-info-card), re-key all 21 template instances + page CSS
keeps, css-winners + qtest gates per page.
- E2 stat-row component: about-stat-*, home-proof-stat*, service-stat*
counter rows - same treatment (unmeasured yet).
- E3 paved path docs: docs/workflows/new-page.md (bundle-slice recipe +
order warning, purge greedy-prefix trap, qtest PAGE_TESTS entry,
ownership-map entry) + a starter/example page template.
- E4 (optional, riskier): collapse the 6 inline CTA template copies onto
partials/page/cta.html - copies diverge at OUTER wrapper classes (page
keeps), needs per-page params + visual gates; the partial is already
usable for NEW pages today. Also fix cta.html duplicate data-node attr.

- PROPOSED Phase E (awaiting Paul): "new-page paved path" - a page
starter template/archetype, 2-3 more extracted section components
(hero, card-grid, stat-row) via the proven C1 protocol, and
Expand Down
99 changes: 99 additions & 0 deletions docs/workflows/new-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Adding a new page — the paved path

The checklist for building a new page that reuses the shared components and
stays consistent with the design system. Every trap listed here has bitten a
real sprint; do not skip steps.

## 1. Template

Create `themes/beaver/layouts/page/<name>.html`. Start from
`themes/beaver/layouts/page/clients.html` (the cleanest example) and keep the
structural shell:

```
{{ define "header" }} <- CSS bundle slice (step 2)
{{ define "main" }}
<div id="fl-main-content" class="fl-page-content" role="main">
... fl-row / fl-col-group / fl-col / fl-col-content nesting ...
```

The `fl-*` framework classes are load-bearing (layout grid from
`critical/fl-layout-grid.css`) — keep the nesting; name YOUR elements with a
page prefix (`<name>-hero`, `<name>-grid`, ...). Never introduce hash-like
class names.

## 2. CSS bundle slice

In the template's `header` block, compose the bundle. **Order is
load-bearing** — keep exactly this shape:

```
{{- $resources := slice
(resources.Get "css/critical/base.css") <- pure-@import prelude; MUST be first
(resources.Get "css/components/<used>.css") <- one line per component you use
(resources.Get "css/pages/<name>.css") <- your page CSS
(resources.Get "css/dynamic-icons.css" | resources.ExecuteAsTemplate "css/dynamic586.css" .)
(resources.Get "css/586.css")
(resources.Get "css/vendors/base-4.min.css")
(resources.Get "css/style.css")
(resources.Get "css/legacy-theme-skin.css")
(resources.Get "css/footer.css")
-}}
{{ partialCached "assets/css-processor.html" (dict "resources" $resources "bundleName" "<name>") "<name>" }}
```

TRAPS:
- `@import` only works in `critical/base.css` (the concat prelude). An
`@import` in `pages/*.css` is silently skipped by postcss-import —
components join as slice members, never imports.
- `bundleName` must be unique repo-wide (a collision silently races —
the blog-list/taxonomy incident).

## 3. Reusable components (include partial + add CSS slice line)

| Component | Markup | CSS slice line |
|---|---|---|
| Testimonials section | `{{ partial "page/testimonials.html" . }}` | `components/testimonials.css` |
| CTA banner ("Let's get started now") | `{{ partial "page/cta.html" . }}` | `components/cta-banner.css` |
| Info card (pp-infobox) | `<div class="fl-module fl-module-pp-infobox jt-info-card <name>-card-x">` + pp-infobox markup (copy one instance from `page/services.html`) | `components/info-card.css` |
| Header CTA trio | (only if the page renders the bf72bba header variant) | `components/header-cta.css` |
| Technologies strip | `{{ partial "technologies.html" (dict "colorVariant" "dark") }}` | `technologies.css` |

Page-specific tweaks to a component (spacing, hover colors) go in
`pages/<name>.css` under YOUR page class — they win by concat position.
Never edit `components/*.css` for one page's needs.

## 4. Design tokens

Use the vars from `foundations/css-variables.css` (inlined site-wide):
`--color-ruby` (#cc342d) · `--color-primary` (#1a8cff) · `--color-dark`
(#121212) · `--color-muted` · `--font-system-ui` · `--radius-default` ·
`--spacing-sm/md`. Visual language: `.stitch/design.md` (JetVelocity).

## 5. PurgeCSS greedy prefix (DO NOT SKIP)

Add your page's class prefix to the `greedy` list in `postcss.config.js`
(next to `/^testimonials-/ ...`). Without it, any selector that pairs your
class with a runtime-injected class (swiper, PowerPack `pp-*`) gets purged
in production only — the visible-skip-link class of bug.

## 6. Tests + maps

- Add the page to `PAGE_TESTS` in `bin/qtest` (key = `pages/<name>.css`
basename → test-name regex).
- Add desktop + mobile screenshot tests
(`test/system/desktop_site_test.rb` / `mobile_site_test.rb`); anchor on
visible text, then `assert_stable_screenshot "<name>"`.
- Add the bundle to `docs/projects/2509-css-migration/css-bundle-ownership-map.md`.

## 7. Verify

```
bin/qtest <name> # fast scoped gate (build + screenshots + guards)
bin/rake test:critical # before commit
bin/test && bin/dtest # both platforms before the PR
```

FORCE_SCREENSHOT_UPDATE=true regenerates baselines (commit macos/ AND
linux/ together). A failing screenshot run overwrites baselines — restore
via `git checkout -- test/fixtures/screenshots` before rerunning.
2 changes: 1 addition & 1 deletion postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const purgecss = createPurgeCss({
// protection their selectors previously got from /^fl-node/ (PowerPack
// runtime classes like pp-swiper-button/pp-review-image are never in
// hugo_stats.json, so without this the renamed rules get purged).
/^testimonials-/, /^cta-banner/, /^career-/, /^clients-/,
/^testimonials-/, /^cta-banner/, /^jt-info-card/, /^career-/, /^clients-/,
/^notfound-/, /^use-case-/, /^services-/, /^service-/, /^about-/, /^home-/, /^careers-/,
// Brand CTA buttons — preserve any selector mentioning these classes.
// Standard safelist didn't catch tag+class compound selectors like
Expand Down
126 changes: 126 additions & 0 deletions themes/beaver/assets/css/components/info-card.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* Shared info-card (pp-infobox) core - Phase E paved path.
The 16-rule intersection of all 21 card instances across homepage,
services, about-us, careers (measured 2026-07-19). Applied via the
ADDITIVE class jt-info-card on each card module; page-specific
variants (hover colors, equal-height framework, media tweaks) stay
in the page files and win by concat position.

New pages: <div class="fl-module fl-module-pp-infobox jt-info-card">
with pp-infobox markup inside - see docs/workflows/new-page.md.

affects pages: homepage, services, about-us, careers - see
docs/projects/2509-css-migration/css-bundle-ownership-map.md */




.fl-col-group-equal-height .jt-info-card, .fl-col-group-equal-height .jt-info-card .fl-module-content, .fl-col-group-equal-height .jt-info-card .fl-module-content .pp-infobox-wrap, .fl-col-group-equal-height .jt-info-card .fl-module-content .pp-infobox-wrap .pp-infobox, .fl-col-group-equal-height .jt-info-card .fl-module-content .pp-infobox-wrap > .pp-infobox-link, .fl-col-group-equal-height .jt-info-card .fl-module-content .pp-infobox-wrap > .pp-more-link {
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
flex-shrink: 1;
min-width: 1px;
max-width: 100%;
-webkit-box-flex: 1 1 auto;
-moz-box-flex: 1 1 auto;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
}



.jt-info-card .pp-infobox .pp-infobox-title-prefix {
display: none;
}



.jt-info-card .pp-infobox-image {
text-align: left
}



.fl-builder-content .jt-info-card .pp-infobox-image img {
height: auto;
max-width: 100%;
}



.jt-info-card .pp-infobox:hover .pp-infobox-image img {
}



.jt-info-card .pp-infobox .animated {
-webkit-animation-duration: 500ms;
-moz-animation-duration: 500ms;
-o-animation-duration: 500ms;
-ms-animation-duration: 500ms;
animation-duration: 500ms;
}



.jt-info-card .pp-infobox-wrap .layout-3-wrapper, .jt-info-card .pp-infobox-wrap .layout-4-wrapper {
}



.jt-info-card .pp-infobox-wrap .layout-1 .pp-heading-wrapper, .jt-info-card .pp-infobox-wrap .layout-2 .pp-heading-wrapper {
display: flex;
align-items: center;
}



.jt-info-card .pp-infobox-wrap .layout-2 .pp-infobox-description, .jt-info-card .pp-infobox-wrap .layout-2 .pp-heading-wrapper {
float: left;
}
@media only screen and (max-width: 1115px) {

.jt-info-card .pp-infobox {
text-align: left;
}
}
@media only screen and (max-width: 860px) {

.jt-info-card .pp-infobox-wrap .pp-infobox {
text-align: left;
}


.jt-info-card .pp-infobox-wrap .layout-2 .pp-infobox-description, .jt-info-card .pp-infobox-wrap .layout-2 .pp-heading-wrapper {
float: left;
}
}
@media only screen and (max-width: 480px) {

.jt-info-card .pp-infobox-wrap .layout-3-wrapper, .jt-info-card .pp-infobox-wrap .layout-4-wrapper {
flex-direction: column;
align-items: flex-start;
}
}



.jt-info-card .pp-infobox-title-wrapper .pp-infobox-title {
font-size: 22px;
}



.jt-info-card .pp-infobox-wrap .layout-3 .pp-icon-wrapper {
margin-right: 10px;
}



.jt-info-card .pp-infobox-wrap .layout-4 .pp-icon-wrapper {
margin-left: 10px;
}
Loading
Loading