Add spec and plan for the five-layer services accordion
This commit is contained in:
parent
5d4de0868f
commit
59e659d5ef
2 changed files with 437 additions and 0 deletions
315
docs/superpowers/plans/2026-11-03-mega-services-accordion.md
Normal file
315
docs/superpowers/plans/2026-11-03-mega-services-accordion.md
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
# MEGA Services — "Five layers" accordion, Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the MEGA Services section with a five-slice horizontal accordion that expands on hover or keyboard focus.
|
||||
|
||||
**Architecture:** A pure-CSS flex accordion — each slice is `flex-1` and transitions `flex-grow` to `4.2` on hover and on `:focus-within`. No JavaScript state, no new dependency. The slice data is derived from the existing `services` array so the two cannot drift apart.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript 5.8, Vite 6, Tailwind 3.4.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **No new runtime dependency.** `app/package.json` gains no `dependencies` entry.
|
||||
- **Pure CSS interaction.** No `useState` for the accordion; hover and `:focus-within` do the work.
|
||||
- **`services` must not change.** The quote form's service dropdown uses all seven services.
|
||||
- **`erasableSyntaxOnly` and `verbatimModuleSyntax` are on.** Type-only imports use `import type`.
|
||||
- **`allowImportingTsExtensions` is on.** Relative imports of local modules carry the `.ts` extension.
|
||||
- **`#services` anchor must survive**, and the section keeps its `01` label.
|
||||
- **All copy lives in `src/data/content.ts`.**
|
||||
- **Commit to `main` directly.** No PR workflow; the client cannot use the Forgejo web UI to merge.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Derive the five layers from `services`
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/src/data/content.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the existing `services: Service[]` array.
|
||||
- Produces: `export type Layer = { id: string; kicker: string; title: string; body: string; scope: string[]; image: string }` and `export const layers: Layer[]` — exactly five entries, ordered earthworks → municipal → stormwater → roads → electrical.
|
||||
|
||||
- [ ] **Step 1: Add the layer type and the derived array**
|
||||
|
||||
In `app/src/data/content.ts`, immediately after the `services` array, add:
|
||||
|
||||
```ts
|
||||
export type Layer = {
|
||||
id: string
|
||||
kicker: string
|
||||
title: string
|
||||
body: string
|
||||
scope: string[]
|
||||
image: string
|
||||
}
|
||||
|
||||
// The five layers every site is built from, in the order they go into the
|
||||
// ground. `services` stays the full list the quote form offers.
|
||||
const layerIds = ['earthworks', 'municipal', 'stormwater', 'roads', 'electrical'] as const
|
||||
const layerTitles: Record<string, string> = { municipal: 'Water & Sewer', roads: 'Roads & Paving' }
|
||||
const layerKickers = ['Layer one', 'Layer two', 'Layer three', 'Layer four', 'Layer five']
|
||||
|
||||
export const layers: Layer[] = layerIds.flatMap((id, i) => {
|
||||
const s = services.find((x) => x.id === id)
|
||||
if (!s) return []
|
||||
return [
|
||||
{
|
||||
id: s.id,
|
||||
kicker: layerKickers[i],
|
||||
title: layerTitles[s.id] ?? s.title,
|
||||
body: s.short,
|
||||
scope: s.scope,
|
||||
image: s.image,
|
||||
},
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the derivation produces five layers**
|
||||
|
||||
Run: `cd app && npx tsx -e "..."` — if `tsx` is unavailable, verify by reading the file instead. Simpler check that needs no extra tooling:
|
||||
|
||||
Run: `cd /Users/david/DEVELOPMENT/MEGA-WEBSITE && grep -n "const layerIds" app/src/data/content.ts`
|
||||
Expected: the five ids appear in the order `earthworks, municipal, stormwater, roads, electrical`.
|
||||
|
||||
- [ ] **Step 3: Type-check**
|
||||
|
||||
Run: `cd app && npx tsc -b`
|
||||
Expected: PASS with no errors. `services.find()` returns `Service | undefined`, which the `if (!s) return []` guard handles.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/data/content.ts
|
||||
git commit -m "Derive the five site layers from the services list"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: The accordion component
|
||||
|
||||
**Files:**
|
||||
- Create: `app/src/components/LayerAccordion.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `layers` and `img` from `@/data/content`.
|
||||
- Produces: `export function LayerAccordion(): JSX.Element`.
|
||||
|
||||
- [ ] **Step 1: Write the component**
|
||||
|
||||
Create `app/src/components/LayerAccordion.tsx`:
|
||||
|
||||
```tsx
|
||||
import { img, layers, type Layer } from '@/data/content'
|
||||
|
||||
// One slice of the accordion. On desktop the row is a flex line and the slice
|
||||
// grows on hover or keyboard focus; on mobile it is a full-width card with
|
||||
// the copy always visible.
|
||||
function Slice({ layer }: { layer: Layer }) {
|
||||
return (
|
||||
<article
|
||||
tabIndex={0}
|
||||
aria-label={layer.title}
|
||||
className="group relative flex flex-col overflow-hidden sm:block sm:min-w-0 sm:flex-1 sm:transition-[flex-grow] sm:duration-[800ms] sm:ease-expo sm:hover:flex-grow-[4.2] sm:focus-within:flex-grow-[4.2]"
|
||||
>
|
||||
{/* Photograph */}
|
||||
<div className="relative h-44 shrink-0 sm:absolute sm:inset-0 sm:h-auto">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center transition-[transform,filter] duration-[1200ms] ease-expo sm:scale-[1.08] sm:saturate-[0.55] sm:brightness-[0.42] sm:group-hover:scale-[1.02] sm:group-hover:saturate-100 sm:group-hover:brightness-[0.72]"
|
||||
style={{ backgroundImage: `url(${img(layer.image, true)})` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Scrim, so the copy stays legible over the photograph */}
|
||||
<div className="absolute inset-0 hidden bg-gradient-to-t from-ink via-ink/55 to-ink/15 sm:block" aria-hidden="true" />
|
||||
|
||||
{/* Vertical title, desktop only */}
|
||||
<span className="absolute inset-x-0 bottom-0 hidden p-[clamp(1rem,2vw,1.75rem)] font-display text-[clamp(1.1rem,1.7vw,1.6rem)] font-medium leading-none text-bone [text-shadow:0_2px_16px_rgba(12,12,10,0.9)] [writing-mode:vertical-rl] rotate-180 transition-colors duration-500 group-hover:text-gold sm:block">
|
||||
{layer.title}
|
||||
</span>
|
||||
|
||||
{/* Panel */}
|
||||
<div className="relative flex flex-col justify-end p-6 sm:absolute sm:inset-0 sm:p-[clamp(1.4rem,3vw,2.5rem)] sm:opacity-0 sm:translate-y-6 sm:transition-[opacity,transform] sm:duration-700 sm:ease-expo sm:group-hover:opacity-100 sm:group-hover:translate-y-0 sm:group-focus-within:opacity-100 sm:group-focus-within:translate-y-0">
|
||||
<p className="eyebrow text-gold">{layer.kicker}</p>
|
||||
<h3 className="display mt-3 text-[clamp(1.5rem,2.6vw,2.4rem)] font-normal leading-[0.98] tracking-[-0.02em] text-bone">
|
||||
{layer.title}
|
||||
</h3>
|
||||
<p className="mt-4 max-w-[34ch] text-[0.97rem] leading-relaxed text-bone/75">{layer.body}</p>
|
||||
<ul className="mt-5 flex flex-wrap gap-2">
|
||||
{layer.scope.map((t) => (
|
||||
<li key={t} className="rounded-full border border-bone/20 px-3 py-1 text-xs text-bone/70">
|
||||
{t}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export function LayerAccordion() {
|
||||
return (
|
||||
<div className="flex flex-col sm:h-[clamp(22rem,58vh,34rem)] sm:flex-row sm:overflow-hidden sm:border-y sm:border-bone/12">
|
||||
{layers.map((layer) => (
|
||||
<Slice key={layer.id} layer={layer} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Type-check**
|
||||
|
||||
Run: `cd app && npx tsc -b`
|
||||
Expected: PASS with no errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/components/LayerAccordion.tsx
|
||||
git commit -m "Add the five-layer services accordion"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Rewire the Services section
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/src/components/Services.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `LayerAccordion` from `./LayerAccordion`.
|
||||
- Produces: `export function Services(): JSX.Element` — unchanged export, unchanged `id="services"`. `WhyUs` is untouched.
|
||||
|
||||
- [ ] **Step 1: Replace the Services component**
|
||||
|
||||
In `app/src/components/Services.tsx`, replace everything from the top of the file down to the end of the `Services` function (that is, keep `export function WhyUs()` and everything after it exactly as it is) with:
|
||||
|
||||
```tsx
|
||||
import { ArrowUpRight } from 'lucide-react'
|
||||
import { pillars } from '@/data/content'
|
||||
import { LayerAccordion } from './LayerAccordion'
|
||||
import { Reveal, SectionLabel } from './Reveal'
|
||||
|
||||
export function Services() {
|
||||
return (
|
||||
<section id="services" className="scroll-mt-20 bg-ink py-24 sm:py-32 lg:py-40">
|
||||
<div className="container-x">
|
||||
<div className="flex flex-col justify-between gap-8 lg:flex-row lg:items-end">
|
||||
<Reveal>
|
||||
<SectionLabel index="01">Services</SectionLabel>
|
||||
<h2 className="display mt-8 max-w-[820px] text-[clamp(2.2rem,5vw,4.6rem)] font-light leading-[1.02]">
|
||||
Five layers. <span className="text-gold">None of them optional.</span>
|
||||
</h2>
|
||||
</Reveal>
|
||||
<Reveal delay={150}>
|
||||
<p className="max-w-[420px] leading-relaxed text-bone/60">
|
||||
This is the whole method, start to finish. Five layers on every site, in the same order, since 2007 — and it is not
|
||||
going to start now.
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<div className="mt-14 lg:mt-20">
|
||||
<LayerAccordion />
|
||||
<p className="mt-8 max-w-[520px] text-sm leading-relaxed text-bone/45">
|
||||
Civil design and residential development are also in-house.{' '}
|
||||
<a
|
||||
href="#contact"
|
||||
className="text-gold underline decoration-gold/40 underline-offset-4 transition-colors hover:text-bone"
|
||||
>
|
||||
Tell us what you need
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The old `useState`, `Plus`, `img`, `services` and `LoopVideo` imports are no longer needed by `Services`; `WhyUs` needs only `pillars`, `Reveal` and `SectionLabel`. Remove the imports that are no longer referenced and keep the `WhyUs` function byte-for-byte as it is.
|
||||
|
||||
- [ ] **Step 2: Confirm the old per-service anchors are gone**
|
||||
|
||||
Run: `cd /Users/david/DEVELOPMENT/MEGA-WEBSITE && grep -rn "service-" app/src || echo "no references"`
|
||||
Expected: `no references` — nothing outside `Services.tsx` ever linked to `#service-<id>`.
|
||||
|
||||
- [ ] **Step 3: Build and test**
|
||||
|
||||
Run: `cd app && npm run build && npm test`
|
||||
Expected: the build succeeds and all 12 unit tests still pass.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/components/Services.tsx
|
||||
git commit -m "Replace the services list with the five-layer accordion"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Verify in the browser
|
||||
|
||||
**Files:**
|
||||
- No file changes expected. Fix anything the checks surface, then re-run this task.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the site from Task 3.
|
||||
|
||||
- [ ] **Step 1: Start the dev server**
|
||||
|
||||
Run: `cd app && npm run dev` (background)
|
||||
Expected: listening on port 5173.
|
||||
|
||||
- [ ] **Step 2: Check the desktop accordion**
|
||||
|
||||
Open `http://localhost:5173/#services` in the in-app Browser. Confirm:
|
||||
- five slices, each collapsed to an equal width, each showing its vertical title;
|
||||
- hovering a slice widens it and brightens its photograph, and the panel fades in with kicker, title, description and chips;
|
||||
- moving the pointer away collapses it again.
|
||||
|
||||
Take a screenshot of a slice expanded.
|
||||
|
||||
- [ ] **Step 3: Check keyboard focus**
|
||||
|
||||
Tab into the accordion and confirm the focused slice expands exactly as it does on hover. Confirm the tab order walks the five slices.
|
||||
|
||||
- [ ] **Step 4: Check the mobile stack**
|
||||
|
||||
Use chrome-devtools MCP `emulate` with viewport `375x812x2,mobile,touch`. Confirm the slices stack vertically as full-width cards, the copy is visible without any interaction, and `document.documentElement.scrollWidth` equals the viewport width.
|
||||
|
||||
- [ ] **Step 5: Check the console and network**
|
||||
|
||||
Query the browser console for errors and the network log for failed requests. Expect no errors and no 404s — every `img/photos/*.webp` must resolve.
|
||||
|
||||
- [ ] **Step 6: Commit any fixes**
|
||||
|
||||
If the checks surfaced bugs, fix them, re-run `npm test` and `npm run build`, and commit:
|
||||
|
||||
```bash
|
||||
git add -A app/src
|
||||
git commit -m "Fix services accordion issues found in browser verification"
|
||||
```
|
||||
|
||||
If nothing needed fixing, skip this step.
|
||||
|
||||
---
|
||||
|
||||
## Self-review against the spec
|
||||
|
||||
| Spec requirement | Covered by |
|
||||
|---|---|
|
||||
| Exactly five slices, in build order | Task 1, Task 4 step 2 |
|
||||
| Hover expands and brightens | Task 2 (Tailwind hover variants), Task 4 step 2 |
|
||||
| Keyboard focus expands identically | Task 2 (`:focus-within`), Task 4 step 3 |
|
||||
| Kicker, title, description, chips on the panel | Task 2 |
|
||||
| Mobile stacks vertically, no overflow | Task 2 (layout classes), Task 4 step 4 |
|
||||
| Heading and intro | Task 3 |
|
||||
| "Also in-house" line linking to `#contact` | Task 3 |
|
||||
| Build and tests pass | Task 3 step 3, Task 4 |
|
||||
| No new dependency; `services` unchanged | Global Constraints, Task 1 |
|
||||
| `#services` anchor and quote dropdown intact | Task 3 (id kept), Global Constraints |
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
# MEGA Services — "Five layers" accordion
|
||||
|
||||
**Date:** 2026-11-03
|
||||
**Status:** approved (design), pending implementation
|
||||
**Reference:** Stackless Coffee "Ritual" section (`src/components/Ritual.jsx` + the `.acc` rules in `src/index.css`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Replace the current Services section — a sticky portrait photograph beside an expandable list of all seven services — with the horizontal accordion treatment from the Stackless Coffee site: five vertical slices that expand across the frame on hover or keyboard focus, each carrying its own photograph, kicker, title, one-line description and scope chips.
|
||||
|
||||
## 2. Decisions taken
|
||||
|
||||
| Decision | Choice |
|
||||
|---|---|
|
||||
| What becomes a slice | The **five site-building layers**: bulk earthworks, water & sewer, stormwater, roads & paving, electrical reticulation |
|
||||
| The current layout | The sticky photograph and the service list are **both replaced** by the accordion |
|
||||
| Mobile | The slices **stack vertically**, each a full-width card with the copy always visible |
|
||||
| Heading | **"Five layers. None of them optional."** |
|
||||
| Design & Residential Development | Kept as a quiet line under the accordion, not as slices |
|
||||
| Interaction | Pure CSS — `flex-grow` on hover, `:focus-within` for keyboard. No JavaScript state |
|
||||
|
||||
## 3. The reference mechanic
|
||||
|
||||
The Stackless accordion is pure CSS:
|
||||
|
||||
- The row is a flex container; each slice is `flex: 1 1 0` with `overflow: hidden`.
|
||||
- On hover (or focus) the slice's `flex-grow` transitions to `4.2`, so it takes roughly 41% of the row and its neighbours compress.
|
||||
- The photograph fills the slice, dimmed (`saturate(0.55) brightness(0.42)`) and scaled up slightly; on hover it brightens and settles.
|
||||
- A vertical title label (`writing-mode: vertical-rl`, rotated 180°) sits at the bottom of the collapsed slice and turns ember on hover.
|
||||
- A panel — kicker, title, body — sits over the slice with `opacity: 0` and a small downward offset; on hover it fades in and rises.
|
||||
|
||||
MEGA keeps the mechanic but swaps the ember accent for gold and the display weights for MEGA's editorial type.
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
### Files
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `app/src/data/content.ts` | Adds `Layer` type and a `layers` array derived from `services`. `services` itself is untouched. |
|
||||
| `app/src/components/LayerAccordion.tsx` | **New.** The accordion and its slices. |
|
||||
| `app/src/components/Services.tsx` | The Services section becomes header + accordion + the "also in-house" line. `WhyUs` is unchanged. |
|
||||
|
||||
### The five layers
|
||||
|
||||
| # | Kicker | Title | Source service | Photo |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Layer one | Bulk Earthworks | `earthworks` | `dji_0587` |
|
||||
| 2 | Layer two | Water & Sewer | `municipal` | `310a6423` |
|
||||
| 3 | Layer three | Stormwater | `stormwater` | `310a6304` |
|
||||
| 4 | Layer four | Roads & Paving | `roads` | `road-paving` |
|
||||
| 5 | Layer five | Electrical Reticulation | `electrical` | `310a6904` |
|
||||
|
||||
The order is the order the layers go into the ground, which is what makes the section read as a method rather than a list.
|
||||
|
||||
### Data derivation
|
||||
|
||||
`layers` is derived from `services` by id, not written out separately:
|
||||
|
||||
- `body` comes from the service's `short` field — currently unused data that was written for exactly this purpose.
|
||||
- `title` comes from the service title, with two overrides: `municipal` → "Water & Sewer" and `roads` → "Roads & Paving" (the slice titles are shorter and more concrete than the service names).
|
||||
- `scope` and `image` are taken from the service unchanged.
|
||||
|
||||
Deriving rather than duplicating means the accordion cannot drift away from the service data. `services` itself stays exactly as it is, because the quote form's service dropdown still uses all seven.
|
||||
|
||||
### Section copy
|
||||
|
||||
- Eyebrow: `01` + "Services" (the existing `SectionLabel`).
|
||||
- Heading: **"Five layers. None of them optional."** — "None of them optional." in gold.
|
||||
- Intro: *"This is the whole method, start to finish. Five layers on every site, in the same order, since 2007 — and it is not going to start now."* — "since 2007" is the AEB Construction founding date already in `company`.
|
||||
- Under the accordion: *"Civil design and residential development are also in-house."* with a link to `#contact`.
|
||||
|
||||
## 5. Layout
|
||||
|
||||
### Desktop (`sm` and up)
|
||||
|
||||
- Row: `flex`, height `clamp(22rem, 58vh, 34rem)`, `overflow: hidden`, hairline borders top and bottom.
|
||||
- Slice: `flex-1`, `min-w-0`, `overflow: hidden`, `transition-[flex-grow] 800ms ease-expo`; `hover:flex-grow-[4.2]` and `focus-within:flex-grow-[4.2]`.
|
||||
- Photograph: absolutely positioned, `background-size: cover`, `scale(1.08) saturate(.55) brightness(.42)`; on hover `scale(1.02) saturate(1) brightness(.72)`.
|
||||
- Scrim: `from-ink via-ink/55 to-ink/15`, top to bottom.
|
||||
- Label: absolute, full width at the bottom, `writing-mode: vertical-rl` + `rotate(180deg)`, display face at `clamp(1.1rem, 1.7vw, 1.6rem)`, with a text shadow; turns gold on hover.
|
||||
- Panel: absolute, full slice, content pushed to the bottom with `clamp(1.4rem, 3vw, 2.5rem)` padding. `opacity: 0` + `translateY(1.5rem)`; on hover/focus `opacity: 1` + `translateY(0)`.
|
||||
- Kicker: eyebrow, gold.
|
||||
- Title: display face, `clamp(1.5rem, 2.6vw, 2.4rem)`.
|
||||
- Body: `short`, `max-w-[34ch]`.
|
||||
- Scope chips: the service's scope tags.
|
||||
|
||||
### Mobile (below `sm`)
|
||||
|
||||
One markup tree, two layouts:
|
||||
|
||||
- The row becomes `flex-col` with no fixed height and no clipping.
|
||||
- Each slice is a column: the photograph is a `h-44` banner, and the panel sits below it in normal flow with the copy always visible.
|
||||
- The vertical label and the scrim are desktop-only.
|
||||
|
||||
## 6. Interaction and accessibility
|
||||
|
||||
- Each slice is focusable (`tabIndex={0}`) with an `aria-label` carrying the title. `:focus-within` reveals the panel, so keyboard users get the same reveal as hover — this is the reference's own approach.
|
||||
- The panel content stays in the accessibility tree at all times, so a screen reader reaches every layer's description and scope whether or not the panel is expanded.
|
||||
- Tapping a slice on a touch device focuses it, which expands it.
|
||||
- The site's existing global rule collapses every transition under `prefers-reduced-motion`, so the expansion snaps rather than animates. No extra work needed.
|
||||
|
||||
## 7. Out of scope
|
||||
|
||||
- The aerial clip on the Roads service (`/video/road-paving.mp4`). The accordion slices use still photographs; a landscape clip cropped into a narrow slice does not read. The asset stays in the repository and in the data.
|
||||
- Any change to `WhyUs`, `Company`, `Team`, `Contact` or the fly-through.
|
||||
- Any change to `services` — the quote form's dropdown still offers all seven.
|
||||
|
||||
## 8. Acceptance criteria
|
||||
|
||||
1. The Services section shows exactly five slices, in the order earthworks → water & sewer → stormwater → roads & paving → electrical.
|
||||
2. Hovering a slice expands it across the row and brightens its photograph; moving away collapses it again.
|
||||
3. Tabbing to a slice expands it identically to hovering.
|
||||
4. Each expanded slice shows its kicker, title, one-line description and scope chips.
|
||||
5. Below `sm` the slices stack vertically as full-width cards with the copy always visible and no horizontal overflow.
|
||||
6. The heading reads "Five layers. None of them optional." and the intro paragraph sits beside it on desktop.
|
||||
7. The "also in-house" line names civil design and residential development and links to `#contact`.
|
||||
8. `npm run build` passes and `npm test` still passes.
|
||||
9. No new runtime dependency, and the `services` array is unchanged.
|
||||
10. The `#services` anchor and the quote form's service dropdown still work.
|
||||
Loading…
Reference in a new issue