Rams MCP · The full engine, now in your coding agent
jgpt-automated on GitHub

jgpt-automated/bnr-power-washing

Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.

10 files reviewed·July 21, 2026

View on GitHub

Elevated

Accessibility needs attention.

21issues
Critical, Serious & Moderate · top 6 shown below

Top fix

Fix gold eyebrow and tag contrast across all light slides

See the fix

Verdict

A solid design system undone by inconsistent follow-through, tokens and reduced-motion logic exist but get ignored in the newest layers. The biggest risk is accessibility drift compounding silently across every slide, filter, and dot the team adds next.

Files Rams reviewed

gallery.html

index.html

proposals/phase2/index.html

services/commercial.html

services/driveway-concrete.html

services/house-washing.html

services/patio-deck-fence.html

services/roof-cleaning.html

services/window-cleaning.html

styles.css

89/100

Accessibility

1 critical4 serious
AccessibilityCritical

proposals/phase2/index.html:113

Gold eyebrow and tag labels fail contrast on every light slide

The `.eyebrow` labels (e.g. "Phase I Recap", "The Strategic Position") and `.card .tag` labels (e.g. "Right Now, Today", "Tier 2") render in `var(--gold)` (#B8943F) at 11-12px on `var(--paper)`/`var(--cream)` backgrounds (#FDFCF9 / #F8F7F4). Computed contrast is ~2.7:1.

Why it matters

Every eyebrow and card tag across slides 2-8 falls well below the 4.5:1 small-text minimum and even below the 3:1 large-text floor: these micro-labels are effectively illegible for a large share of readers, and they're the first thing scanned on each slide.

Fix

Darken the gold used for text on light backgrounds while keeping the lighter gold reserved for dark slides.

.eyebrow {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 16px;
}
.eyebrow {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #7A5B22;
  margin-bottom: 16px;
}
AccessibilitySerious

styles.css:1

Fine-print text in ink-3 fails contrast on white surfaces

`--ink-3: #8E919A` on `--surface`/white backgrounds computes to roughly 3.2:1, below the 4.5:1 small-text minimum. It shows up repeatedly in new components: - `.reviews-link-wrap .arrow` (11px) - `.rq-finepr` (12px) - `.svc-price-detail` (12px)

Why it matters

These aren't decorative flourishes, they're the review-link arrow, form disclaimer, and price-detail copy: real reading content that low-vision users can't reliably read at these values.

Fix

Move fine-print text to a text tone with at least 4.5:1 against its background.

.reviews-link-wrap .arrow{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--ink-3);transition:transform 200ms var(--ease)}
.reviews-link-wrap .arrow{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--ink-2);transition:transform 200ms var(--ease)}
AccessibilitySerious

gallery.html:1

Filter buttons change look on click but never announce selection to screen readers

The six `.gx-filter` buttons ("All", "House Washing", "Driveway"...) toggle an `active` class via JS to show the current filter, but none carries `aria-pressed` or `aria-selected`. The only signal of which filter is active is a visual style change.

Why it matters

A screen reader user tabbing through the filter row hears six identical buttons with no indication of which one is currently applied, so they can't confirm their filter choice took effect.

Fix

Expose the toggle state programmatically with aria-pressed, updated in the same JS that toggles the active class.

<button class="gx-filter active" data-filter="all" type="button">All<span class="count">56</span></button>
<button class="gx-filter active" data-filter="all" type="button" aria-pressed="true">All<span class="count">56</span></button>
AccessibilitySerious

index.html:1

Utility bar's aria-label hides the visible phone number from screen readers

`<a href="tel:+18324010741" aria-label="Call us">` wraps an icon and the visible text "(832) 401-0741", but the aria-label replaces that entire visible text for screen reader users, so the actual number is never announced.

Why it matters

A screen reader user hears only "Call us, link" and never gets the phone number itself, even though sighted users can read it directly off the page: an inconsistent experience for a piece of information this business leads with.

Fix

Remove the redundant aria-label so the link's own visible text (the phone number) becomes its accessible name.

<a href="tel:+18324010741" aria-label="Call us"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z"/></svg> (832) 401-0741 </a>
<a href="tel:+18324010741"><svg class="icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z"/></svg> (832) 401-0741 </a>
AccessibilitySerious

proposals/phase2/index.html:456

Carousel dot navigation is unlabeled, unfocusable spans

The dot indicators in `#dots` are built as bare `<span>` elements with an `onclick` handler and no `aria-label`, `role`, or keyboard affordance: only the `‹`/`›` buttons are real, labeled controls.

Why it matters

Keyboard and screen-reader users can't jump directly to a slide via the dots; they only learn a dot exists once a mouse hovers it, and none of them announce which slide they represent.

Fix

Build the dot indicators as real buttons with a descriptive accessible name.

for (let i = 0; i < total; i++) {
  const d = document.createElement('span');
  if (i === 0) d.classList.add('active');
  d.onclick = () => goTo(i);
  dotsEl.appendChild(d);
}
for (let i = 0; i < total; i++) {
  const d = document.createElement('button');
  d.type = 'button';
  d.setAttribute('aria-label', `Go to slide ${i + 1}`);
  if (i === 0) d.classList.add('active');
  d.onclick = () => goTo(i);
  dotsEl.appendChild(d);
}
97/100

Motion

1 critical
MotionCritical

proposals/phase2/index.html:349

Image placeholder pulse animation ignores prefers-reduced-motion

`.ha-img-placeholder` runs `animation: ha-img-pulse 1.5s ease-in-out infinite` with no `prefers-reduced-motion` guard anywhere in the stylesheet.

Why it matters

Anyone with vestibular sensitivity who has set reduced motion at the OS level still gets a looping opacity pulse on every placeholder image, with no way to opt out.

Fix

Disable looping animation for users who have requested reduced motion.

.ha-img-placeholder{display:flex;align-items:center;justify-content:center;flex-direction:column;gap:6px;background:#f4f4f5;border:1px dashed #d4d4d8;border-radius:8px;color:#71717a;font-size:12px;font-family:system-ui,sans-serif;min-height:80px;padding:16px;box-sizing:border-box;animation:ha-img-pulse 1.5s ease-in-out infinite}
.ha-img-placeholder{display:flex;align-items:center;justify-content:center;flex-direction:column;gap:6px;background:#f4f4f5;border:1px dashed #d4d4d8;border-radius:8px;color:#71717a;font-size:12px;font-family:system-ui,sans-serif;min-height:80px;padding:16px;box-sizing:border-box;animation:ha-img-pulse 1.5s ease-in-out infinite}
@media (prefers-reduced-motion: reduce){.ha-img-placeholder{animation:none}}

Typography

No issues found

Color

No issues found

Spacing

No issues found

Components

No issues found

UX

No issues found

Craft

No issues found

Working well

  • The single `@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:0ms !important;transition-duration:0ms !important}}` rule near the base of the file is smart because it's universal: every animation added later in the V2 and service-page passes (the coverage pin pulse, the gallery blur-up, the AI-quote spinner) is automatically covered without anyone having to remember to add a reduced-motion branch to each new keyframe.
  • The hero's three CTAs are correctly differentiated by weight, not just color: "Call (832) 401-0741" is a filled navy .btn-primary, "Request a Quote" is a bordered .btn-ghost, and "Or get an AI quote in 60 seconds" is a plain text link with an arrow. That's a real three-tier hierarchy: primary action, secondary action, tertiary escape hatch: and it reads instantly without anyone needing to think about it.
  • The before/after slider (`.ba-range`) is built on a real native `<input type=range>` under an invisible layer, with a `:focus-visible` ring that draws a visible outline around the knob (`box-shadow:0 0 0 4px #fff,0 0 0 7px var(--blue)`) instead of the fake custom-drag-handle pattern most sliders like this ship with: keyboard and screen-reader users get full control for free.
  • The nav's own CSS comment says it best: "Nav buttons: PEER pills: same height, padding, radius. Only color/role differs." The call button (.btn-outline-gold) and the Instant Quote button (.btn-primary) share identical geometry and differ only in fill and role, which is exactly how a button system should scale: one shape, meaning carried by color and weight alone.

Scored July 21, 2026 with Rams Engine v0.0.2 · Engine changelog

Score your own repo.

Free on public repos, no account. The same engine that scored this page reads your UI code and mints a score page like this one.

Public repos only. The full engine reviews the UI code and mints a public score page — we email you the link too. Already-scored repos open instantly.

Or get this review on every PR.