Skip to content
the system

One typeface, one accent, and the discipline to stop there.

Internal reference for the site buildout. Every page is assembled from the parts on this page — if a surface disagrees with this page, the surface is the defect.

Every specimen below is the production component, imported from src/components/v2/. Nothing here is a copy. Change a component and this page changes with it. Open usage under any specimen for the exact JSX. Written index: site/DESIGN-SYSTEM.md. Canon: brand/design.md v2.1, regenerated from this surface.

Sixteen sections. Section 16 is the complete token table, every name verbatim, and it is the one to bookmark. Section 13 is the component roster with props. Section 09 is the image base prompt every generated image on the site comes from. If you are about to build a page rather than look something up, start at how to build from this instead.

01 · color

Monochrome, plus one live thing.

#0A0B0B
ground·dark
#151717
card·dark
#222222
ink·hi
#4A4A48
ink·mid
#6B6B68
ink·dim
#8A8A86
eyebrow
#B0B0AD
muted ✕
#F5F5F5
ground
#FFFFFF
raised
Bjarmi on light — #547070
The only form of Bjarmi that carries words on paper. 4.90:1. #87A6A6 never carries text on light.
Bjarmi on dark — #87A6A6
The live thing. The only element in the system allowed to breathe.
Hairlines, not borders: rgba(0,0,0,.12) light · rgba(255,255,255,.12) darkOne tint: rgba(84,112,112,.06) — table headers onlyRetired v2.1: #729090 · #A9C6C6
02 · type

Schibsted Grotesk carries everything.

display · clamp(56px, 6.8vw, 96px) · 500 · lh 0.98 · ls −0.045em
Searching right now.
feature · clamp(44px, 4.4vw, 60px) · lh 1.02 · ls −0.04em
I’m Justin. This is the fix.
section · clamp(40px, 4vw, 52px) · lh 1.06 · ls −0.035em  |  card titles 19px / 21px · 500
Pick your lane.
body · 17px lede (520px) · 14.5px body · 13.5px small

Ledes run 17px on a 520px measure. Side notes run 15px on 340px. Long-form article bodies cap at 65ch.

Body 14.5px — captions, card copy, table cells.

the rules
Headings are always weight 500: contrast comes from size, never a lighter weight
Emphasis is the true italic: 2–5 words, once per headline, never a whole line
Sentence case everywhere a human reads a sentence, buttons included
Title Case appears nowhere. Uppercase only in mono labels
Italic never in Bjarmi, never a lighter weight, never a second family
03 · structure

The grid, the rhythm, the radii.

container
1280px
48px gutters · 24px under 760
section rhythm
150px
160 display moments · 90 mobile · header→content 56px
radii
12 · 8 · 999
12 everything · 8 nested controls · 999 pills. Nothing else.
eyebrow gap
20px
Eyebrow to heading, everywhere, no exceptions
Grounds: #F5F5F5 page, #FFFFFF alternating sections, dark reserved for momentsShadows: exactly two — the lifted field and the featured price cardCards: ghost hairline or solid dark — never grey fills, never colored left-borders
04 · controls

Buttons and the grader field.

light ground
Primary = ink fill · ghost = 0.2 hairline, 0.5 on hover · 50px page / 42px nav
usage
<Button href="/resources/website-grader/">Get website grade</Button>
<Button variant="ghost" href="/book/">Book a call</Button>
dark ground
Fills invert with the ground. Never gradients, never colored fills.
usage
<Button ground="dark" href="/resources/website-grader/">Get website grade</Button>
<Button variant="ghost" ground="dark" href="/book/">Book a call</Button>
the grader field

The site-wide conversion device. 56px light shell, 8px inner dark button — the only nested radius. On dark grounds it lifts with the field shadow. Every page ends with this ask.

05 · labels & marks

The mono language.

eyebrow anatomy
05 · services
9.5px · 0.24em · uppercase · lowercase words · NN · label · #8A8A86 on light, #87A6A6 on dark · 20px above its heading
usage
<Eyebrow number="05">services</Eyebrow>
the ✕ / ✓ system
Sold, then ghosted
Same-day reply, personally
Grey ✕ names pains, Bjarmi ✓ answers them. ✕ never appears outside a problem context.
usage
<CheckList mark="cross" items={['Sold, then ghosted']} />
<CheckList items={['Same-day reply, personally']} />
live marks
4 of 6 client slots filled★★★★★ 5.0 on googleread the build →
numbered rows
W1Audit and plan, in writing
W2Build — pages, campaigns, follow-up
01Mono index + 28px column for steps, terms, timelines
06 · cards

Card families. Hover them.

presence
$1,000/mo

Your foundation, run properly.

Website build and hosting
Local SEO
growthbest value
$3,000/mo

The whole demand engine. Featured tier: dark card, −8px lift, the system’s second shadow.

Everything in Presence
Paid ads management*
*ad spend billed separately
custom
Let’s talk

Non-price tiers set the phrase at the price scale.

the four card familiescase · tool · blog · pricing
Case cards carry one stat and no more — the case page carries the rest. Pricing runs two ghost cards flanking one dark featured card, and the featured card is one of the system’s only two shadows.
usage
<CaseCard tag="jhai · ai system" title="…" stat="↑ 12× content output" statSpark
          href="/work/case-study-01/" />
<ToolCard icon={<Link size={22} strokeWidth={1.5} color="var(--accent)" />} title="Website grader"
          description="…" cta="try it" href="/resources/website-grader/" />
<BlogCard date="jul 17, 2026" title="…" href="/blog/post-01/" />
<PricingCard featured tag="best value" name={tiers[1].name} price={tiers[1].price} per="/mo"
             line="…" items={['Everything in Presence']} note="*ad spend billed separately" />
07 · patterns

Bands, tables, rows.

300+
businesses served
$50M+
revenue generated
13+
years in digital
40+
industries

The CTA band: statement left, grader right. One per page, after a proof moment.

Free, in minutes. Same-day reply from Justin, personally.
comparison tablecentrepiece of a comparison page · nowhere else
How working with Justin compares with a typical agency.
what you getThe othersJustin
Same-day reply, personallyNoYes
Who does the workA rotating account teamJustin, every time
usage
<ComparisonTable
  rowHeading="what you get"
  caption="How working with Justin compares with a typical agency."
  columns={[{ label: 'The others' }, { label: 'Justin', highlight: true }]}
  rows={[
    { label: 'Same-day reply, personally', values: [false, true] },
    { label: 'Who does the work', values: ['A rotating account team', 'Justin, every time'] },
  ]}
/>
FAQ rows use native details, hairline-ruled
Answers in plain English on a 640px measure. Question weight 500, 17px.
A second row, closed
Rows stack on a hairline. Nothing about the row is coloured until it is open.
pattern usage
One dark CTA moment maximum between the hero and the close. The comparison table is the centrepiece of a comparison page and appears nowhere else.
usage
<StatTile value="300+" label="businesses served" divider={false} />
<CTABand title="Your website is losing customers you never meet." action="/resources/website-grader/" />
<FAQItem question="How much does it cost?">Retainers start at {tiers[0].price} a month, scoped in writing.</FAQItem>
08 · photography

Black and white, hard light, one subject.

Plate treatment example
The plate: grayscale(1) contrast(1.1), bottom scrim, 5% grain, words in the shadow.
Cutout treatment examplethe cutout
image slotthe grey plate that stands in for photography
Drop: agent console frame
Drop: case study hero
Plate treatment, grayscale and contrast raised
usage
<ImageSlot ratio="16/10" placeholder="Drop: agent console frame" />
<ImageSlot ratio="16/10" src="/v2/photos/plaza-plate.jpg" alt="Plaza" grayscale contrast />
Never: color photography, stock, illustration-as-photoCutouts: dark wardrobe on light grounds; clip at section edges, fade inside the clipEmpty slots: grey plates until real photography exists
09 · image system

One base prompt. The subject is the work.

Higgsfield MCP, nano-banana model. Every featured image on the site is generated from the block below, and nothing else is added to it.

the base prompt · copy this whole
Black and white photograph. A bit of grain. Architectural framing. Editorial.
Photographic realism, as though we hired our own photographer to take high-concept pictures.

Subject: <the subject of the section this image sits in>

That is the entire prompt. The only thing you write is the subject line, and you take it from the section the image sits in. Everything a person would normally reach for next is deliberately absent, and section 09 exists partly to stop it being added back.

fixed · every image, no exceptions
  • Black and white.
  • A bit of grain.
  • Architectural framing.
  • Editorial register.
  • Photographic realism.
  • Subject drawn from the section it sits in.

These six are the system. They are what makes 63 separately generated images read as one set.

left to the model · never specified
  • Scene.
  • Composition.
  • Camera position.
  • Light direction.
  • Lens.
  • Depth.
  • Texture.

These seven are the variance. Naming one of them narrows the output for no gain, and naming several collapses it. If you can infer it from the subject and the reference, do not write it.

the subject says what the section sayscorrect
dentist page · a dentist working
speed to lead · a call being answered

A dentist page shows a dental practice, or a dentist working, shot artistically. A speed-to-lead guide shows a phone being answered. The image is briefed from the section it sits in, one image at a time, and it is the only thing in the prompt that changes.

an image chosen for moodavoid
abstract texture
generic desk scene

Decorative is the failure. A moody abstract that could sit on any page sits correctly on none of them, and 63 of those read as stock even when nothing was bought. If you cannot name what the image is of in the words of the section, the subject line is not finished.

the reference frame

Justin’s own photography, at tasks/2026-08-10-wireframe-v2/claude-design-drop/extracted/uploads/claude-design-kit/photos/. Six frames. Name the look and the subject when you point at them, and nothing else. Never name an era, a genre, a magazine or a decade: those are abstractions, and a model given an abstraction returns the average of it. The six frames themselves stay reserved for person pages, About and author bios (D-13). They are the calibration, not the supply.

Tool: Higgsfield MCP, nano-banana model, chosen on costApproval: six samples across different post types go to the CEO first, and the remaining 57 generate only against the approved system (D-19). One wrong system costs 63 renders, which is why this is the one place a lane waitsNever: colour, stock, illustration-as-photo, or an image that would work equally well on another page
10 · motion

One curve. Nothing bounces.

the hover · try it
image 1.02 · arrow 4px →
the curve
cubic-bezier(0.2, 0, 0, 1)
200ms color hovers · 400ms small moves · 700ms reveals and image scales. Reveals fade up 24–32px, staggered ~90ms.
alive vs still
One breathing element per screen, always bjarmi
Marquees drift linearly (logos 70s). Parallax ≤12%. No springs, no bounces, no scroll hijack. prefers-reduced-motion kills everything.
reveal · scroll it into viewdata-rv

Fade up 28px over 0.7s on the one curve. The hide is written in JavaScript and only for elements already below 90 percent of the viewport, so a crawler, a reader with JS off, and the very first paint all get the fully visible page. No reveal can cost an indexed word.

stagger · four bars, 90ms apartdata-rv="0" … data-rv="3"

The index is the delay multiplier, not an order. Four siblings numbered 0 to 3 land 90ms apart within their band. Numbering restarts per band, never across the page.

count · runs once, on entrydata-count="300" data-count-suffix="+"

900ms, ease-out cubic, 30ms tick. The server renders the final figure, so the number is in the HTML whether or not the script runs. Use data-count-in="<selector>" when the figure is inside a shared component you do not own, and the annotation stays on the band.

300+
parallax · capped at 12 percentdata-plx

The plate is sized past its frame and the frame clips it: top -12 percent, height 124 percent, inside an overflow: hidden parent. Deeper than 12 percent reads as a trick, so 12 percent is the ceiling and not a default.

11 · doctrine

The rules about what never happens.

like things are rowscorrect
Local SEO
Google ads
Web design

Eight services are eight hairline rows. Repetition is the tell: the moment a pattern repeats, the box comes off. A container appears only when one thing stands alone (a featured case, the terms card, the best-value tier).

a set of boxed cardsavoid
Local SEO
Google ads
Web design

Identical items in identical boxes is agency-template grammar. Also never: grey-filled cards, colored left-borders, icon-in-circle chips, emoji.

boundaries are grounds or aircorrect

Sections meet as a change of ground (#F5F5F5 ↔ #FFFFFF ↔ dark) or 150px of air. No full-width rules at section edges, no tinted divider bands. Hairlines live INSIDE sections: on rows, tables, terms strips.

media is contained, never bledcorrect

No image or video touches the browser edge; a wide image is a wide container at the brand radius. Exception, stated once: the homepage hero video is the single full-bleed surface on the site. Video adds four rules: muted, looping, no controls, poster + painted fallback.

Documentation obeys the same doctrine: right and wrong get an identical hairline frame and a mono label. The word is the only difference — never a green border, a red border, a coloured tag or a tinted card.
12 · chrome

Header and footer, fixed for every page.

4 of 6 client slots filled · taking 2 this quarter · ★★★★★ 5.0 on google
ServicesIndustriesWorkPricingResourcesAbout(725) 324-7571Get website grade
search
websites & ads
All 33 services →

Capacity strip above the bar (dark, always). Bar: transparent-dark over the hero → frosted light after scroll. Megamenu: near-solid light panel, categorized columns, “All →” hairline row. Footer: giant wordmark (rises on arrival), 6 link columns, legal strip, signature + badges + skyline, “made with love in las vegas”.

The bar above is a facsimile on purpose: Header, Megamenu and MobileNav are sticky, client-side and scroll-reactive, and a sticky header rendered inside a specimen plate would misrepresent its own behaviour. The live header is the one at the top of this page. Source: src/components/v2/chrome/.

13 · components

Every part, and what it takes.

63 entries under src/components/v2/. 19 of them render live somewhere on this page; every other row names why it cannot.

Reach for a part in this order: a core primitive, then a card, then a page part, then a band. If none of them fits, the answer is almost never a new component. It is a row.

D-24, binding. Every stateful or interactive primitive comes from shadcn, themed with the v2 tokens. Navigation menu, sheet, dialog, tabs, form, input, select, tooltip, popover, carousel, separator, badge, skeleton. If shadcn has it, we use it, and no new hand-rolled interactive component is authored, ever. Tailwind utilities are the styling layer and inline style objects come out rather than go in. The token names stay verbatim: shadcn is themed with our tokens, our tokens are never bent to shadcn defaults. lucide-react is the icon set, and no icon SVG is hand-drawn inside a component file.

shadcn primitives · pending W1-0

Zero files in this build import @/components/ui today. shadcn v4, Tailwind v4, @base-ui/react, lucide-react, class-variance-authority and tailwind-merge are all installed and components.json is configured, and none of it is used yet. That is the true state of the build, and it is printed here rather than described as finished, because a style guide that claims a roster it does not have is the defect this whole surface exists to end. Lane W1-0 does the conversion and hands its roster back as data; the table appears here when it lands, with no edit to this page.

iconslucide strokes at 1.5, never hand-drawn
One family, one stroke weight, one size token per context. Mixing filled and outline at the same level is the tell that an icon set was assembled rather than chosen.
usage
import { Icon } from '@/components/v2/sections/icons';

<Icon name="search" size={22} />
<Icon name="wrench" size={22} color="var(--accent)" />
the one platform exception

FAQItem uses a native <details> element and keeps it. It opens with JavaScript disabled, it is keyboard operable for free, and its content is findable by in-page search, all of which a shadcn accordion has to re-earn. This is the one case where the platform primitive beats the library one, and it is written down here so nobody “fixes” it later. D-24, stated once, on purpose.

proof decklive import · the deck every one of the 190 pages mounts

Trusted by UFC, Caesars Entertainment, City of Las Vegas, Supercuts

Justin's expertise, responsiveness, and genuine investment in our success have been evident throughout this process. We are truly grateful.
Dr. Linda Silvestri & Dr. Angela Silvestri-Elmore, Co-authors, Saunders Pyramid to Success · 14 years · 300+ Las Vegas businesses · 5-star rated
Identical on all 190 pages, with no conditionals and no per-vertical figures (D-10, I-2). The figures are not props, so no page can vary them: 14 years, 300+ businesses, 40+ industries, plus the rating and the BBB mark. No agent count appears anywhere, because our own files carry six different numbers for it and not one of them is sourced.
Rendered with wall={false} here. The trusted-by wall it normally carries is LogoWall, a marquee, and a marquee inside a specimen plate reads as a bug rather than a behaviour. That is the same reason the roster below lists LogoWall as documented rather than live, and rendering it here anyway would have contradicted the row.
usage
<ProofDeck row={row} content={content} />
// wall={false} when the template already renders its own trusted-by wall
coreThe eight primitives every page is assembled from. Reach for these first, always.
componentpropson this page
Buttoncore/Button.tsxEvery call to action. Renders an anchor when given href, a button otherwise.variant?: 'primary' | 'ghost' · ground?: 'light' | 'dark' · size?: 'md' | 'nav' | 'lg' · href?: string · children: ReactNoderendered livesee it →
CheckListcore/CheckList.tsxThe mark system. Grey cross names a pain, Bjarmi check answers it.items: ReactNode[] · mark?: 'check' | 'cross' · ground?: 'light' | 'dark' · size?: 'md' | 'lg' · style?: CSSPropertiesrendered livesee it →
Eyebrowcore/Eyebrow.tsxThe mono label above a heading. One recipe, no variations.number?: string · ground?: 'light' | 'dark' · accent?: boolean · children: ReactNode · style?: CSSPropertiesrendered livesee it →
GraderFieldcore/GraderField.tsxThe site-wide conversion device. Every page ends with this ask.ground?: 'light' | 'dark' · cta?: string · action?: string · subline?: ReactNode · placeholder?: string · inputLabel?: string · style?: CSSPropertiesrendered livesee it →
ImageSlotcore/ImageSlot.tsxThe grey plate that stands in for photography, and the frame real photography lands in.placeholder?: string · src?: string · alt?: string · ratio?: string · shape?: 'rounded' | 'rect' | 'circle' · grayscale?: boolean · contrast?: boolean · width?: string · style?: CSSPropertiesrendered livesee it →
SectionHeadercore/SectionHeader.tsxEyebrow, heading and side note, at the one gap. Every section on the site opens with this.number?: string · label: string · title: ReactNode · side?: ReactNode · scale?: 'section' | 'feature' | 'display' · ground?: 'light' | 'dark' · style?: CSSPropertiesrendered livesee it →
StatTilecore/StatTile.tsxOne figure and its mono caption, with an optional left hairline.value: string · label: string · divider?: boolean · ground?: 'light' | 'dark' · style?: CSSPropertiesrendered livesee it →
Icon · Sparkline · SocialIconsections/icons.tsxThe icon layer. Lucide strokes at 1.5, plus the sparkline that rides a case-card stat.Icon: name: string · size?: number · color?: string | Sparkline: variant?: number | SocialIcon: id: stringrendered livesee it →
cardsFour families and no fifth. If a new thing needs a card, it is one of these four or it is a row.
componentpropson this page
CaseCardcards/CaseCard.tsxA case study, carrying exactly one stat. The case page carries the rest.href? · tag: string · title: ReactNode · stat? · statSpark?: boolean · cta? · image? · imagePlaceholder? · width? · style?rendered livesee it →
ToolCardcards/ToolCard.tsxA tool or resource. White ghost card, 22px Lucide icon, one-line description, mono CTA.href? · icon?: ReactNode · title: string · description: string · cta? · style?rendered livesee it →
BlogCardcards/BlogCard.tsxA post. Hairline top rule, mono date, 21px title. No box.href? · date: string · title: string · image? · imagePlaceholder? · style?rendered livesee it →
PricingCardcards/PricingCard.tsxA tier. Two ghost cards flank one dark featured card, and the featured card carries one of the system two shadows.name: string · price: string · per? · line: string · items: ReactNode[] · featured?: boolean · tag? · note? · ctaHref? · ctaLabel? · style?rendered livesee it →
page partsComposable blocks a template reaches for. Each one is layout plus primitives, never a re-implemented control.
componentpropson this page
CTABandpage/CTABand.tsxStatement left, grader right. One dark CTA moment per page, after a proof moment.title?: ReactNode · subline?: ReactNode · action? · cta? · style?rendered livesee it →
ComparisonTablepage/ComparisonTable.tsxThe cross and check table. The centrepiece of a comparison page and nowhere else.rowHeading? · columns: ComparisonColumn[] · rows: ComparisonRow[] · caption? · yesLabel? · noLabel? · style?rendered livesee it →
FAQItempage/FAQItem.tsxOne question row on a hairline. Native details, and that is deliberate. See the note in section 13.question: string · children: ReactNode · open?: boolean · style?rendered livesee it →
DemoFramepage/DemoFrame.tsxA working demo inside contained browser chrome, with mono spec rows beneath.url: string · live?: boolean · liveLabel? · ratio? · placeholder? · meta?: ReactNode[] · maxWidth? · children? · style?rendered livesee it →
ProofDeckpage/ProofDeck.tsxThe proof wall. Identical on all 190 pages: 14 years, 300+ businesses, 40+ industries, plus the rating and the BBB mark.row: PageRecord · content: ContentFile | null · wall?: boolean. The figures are locked at D-10 and are not props, so no page can vary them.rendered livesee it →
chromeThe header and footer that wrap every route. Three of the four cannot be rendered honestly inside a specimen plate, and the page says which and why rather than faking it.
componentpropson this page
Footerchrome/Footer.tsxGiant wordmark, six link columns, legal strip, signature, badges, skyline.columns?: FooterColumn[] · phone? · email? · legalLinks?: FooterLink[] · badges?: FooterBadge[] · copyright?rendered livesee it →
Headerchrome/Header.tsxCapacity strip, bar, phone, CTA pair. Transparent over a hero, frosted after scroll.nav?: NavItem[] · phone? · phoneHref? · ctaPrimary?: HeaderCta · ctaSecondary?: HeaderCta · announcement?: HeaderAnnouncement | null · transparentOverHero?: boolean · homeHref?documentedSticky, client-side and scroll-reactive. A sticky header rendered inside a specimen plate misrepresents its own behaviour. The live one is at the top of this page.
Megamenuchrome/Megamenu.tsxThe near-solid light panel under a nav item: categorised columns and an "All →" hairline row.panel: MegamenuPanel · onPointerEnter? · onPointerLeave? · id? · labelledBy?documentedOnly exists as a child of Header, positioned against the bar. Detached from the bar it has no geometry to render into.
MobileNavchrome/MobileNav.tsxThe full-screen sheet under 900px, opened from the bar.open: boolean · onClose: () => void · nav: NavItem[] · phone · phoneHref · ctaPrimary · ctaSecondarydocumentedRenders full-viewport by definition. Open it in a specimen and it eats the page. Resize this window under 900px to see the real one.
motionOne client component for the whole site. Every section stays a server component and opts in with a data attribute.
componentpropson this page
MotionRuntimemotion/MotionRuntime.tsxThe scroll-driven half of the motion layer. Mounted once in the root layout, double-mount guarded.none. Sections opt in with data-rv, data-plx, data-count and data-feed.rendered livesee it →
homepage bandsTwenty-three bands, each taking one typed slice of content/home.json. They are compositions of the parts above, which is why the page documents the parts and lists the bands.
componentpropson this page
Herosections/Hero.tsxThe one full-bleed surface on the site.content: HeroContentdocumentedTakes a full content slice and renders at viewport scale. Its parts are documented individually above.
Problemsections/Problem.tsxThe diagnosis band. Capped at one per page (I-4).content: ProblemContentdocumentedContent-bound band. See the service-page recipe in section 14.
Answersections/Answer.tsxThe operator answer, with the one bespoke drift on #answer-figure.content: AnswerContentdocumentedContent-bound band.
Servicessections/Services.tsxEight services as eight hairline rows. The doctrine example in section 11.content: ServicesContentdocumentedContent-bound band. Its row grammar is rendered live in section 11.
Worksections/Work.tsxThe case rail.content: WorkContentdocumentedContent-bound band. Built from CaseCard.
Engagesections/Engage.tsxThe pricing band.content: EngageContentdocumentedContent-bound band. Built from PricingCard, which is live in section 06.
Processsections/Process.tsxThe numbered-row ladder.content: ProcessContentdocumentedContent-bound band. Its numbered rows are rendered live in section 05.
Operatorsections/Operator.tsxThe person band: portrait plate and fact rows.content: OperatorContent · phone: string · phoneHref: stringdocumentedContent-bound band.
Aesirsections/Aesir.tsxThe system band.content: AesirContentdocumentedContent-bound band.
Comparesections/Compare.tsxThe comparison band. Wraps ComparisonTable.content: CompareContentdocumentedContent-bound band. ComparisonTable is live in section 07.
Questionssections/Questions.tsxThe FAQ band. Wraps FAQItem.content: QuestionsContentdocumentedContent-bound band. FAQItem is live in section 07.
Closesections/Close.tsxThe closing display statement and the last grader ask.content: CloseContentdocumentedContent-bound band.
CtaBandSectionsections/CtaBandSection.tsxThe band wrapper around CTABand.content: CtaBandContentdocumentedContent-bound band. CTABand is live in section 07.
AnchorQuotesections/AnchorQuote.tsxOne quote at feature scale.content: QuoteContentdocumentedContent-bound band.
InterstitialPlatesections/InterstitialPlate.tsxA contained full-width plate between bands.content: InterstitialContentdocumentedContent-bound band. Built from the plate treatment in section 08.
LogoWallsections/LogoWall.tsxThe client marquee, drifting linearly at 70s.logos: LogoEntry[] · label: stringdocumentedA marquee inside a narrow specimen plate reads as a bug rather than a behaviour.
ReviewRailsections/ReviewRail.tsxThe review rail and its rating mark.content: ProofContentdocumentedContent-bound band.
RecordBandsections/RecordBand.tsxThe four-figure stat strip. Built from StatTile with data-count.stats: StatEntry[]documentedIts parts are live in section 07 and its count-up is demonstrated in section 10.
Tickersections/Ticker.tsxThe single-line drift strip.text: stringdocumentedSame marquee reason as LogoWall.
Toolssections/Tools.tsxThe tool-card grid.content: ToolsContentdocumentedContent-bound band. ToolCard is live in section 06.
Writingsections/Writing.tsxThe three-post rail.content: WritingContentdocumentedContent-bound band. BlogCard is live in section 06.
TechStacksections/TechStack.tsxThe stack strip.content: TechContentdocumentedContent-bound band.
IndexRailsections/IndexRail.tsxThe fixed section rail, coloured by scroll position and never moved.entries: RailEntry[]documentedPosition-fixed against the viewport. Inside a plate it has nothing to track.
Container · Section · Grain · HomeStylessections/shell.tsxThe 1280 container, the ground grammar, the one texture, and the stylesheet the folds read from.Section: id? · label? · ground?: 'paper' | 'white' | 'ink' · children · style? · className?documentedThis documentation surface renders its own container and grain, because a doc page is not a marketing band and nesting a Section inside one would double the rhythm. The three grounds these produce are rendered in section 11 and the grain is on the dark plate in section 04.
templatesSix route templates and their parts. Each takes a row from site-data/pages.json plus its content file, so none of them can render without a real page behind it. The recipes in section 14 are how you choose between them.
componentpropson this page
ServiceDetailTemplatetemplates/service/ServiceDetailTemplate.tsx33 service pages.row: PageRecord · content: ContentFile | nulldocumentedRoute-level template. Needs a real pages.json row.
ServiceCategoryTemplatetemplates/service/ServiceCategoryTemplate.tsxThe six service category landers.row: PageRecord · content: ContentFile | nulldocumentedRoute-level template.
IndustryTemplatetemplates/industry/IndustryTemplate.tsx24 industry pages.row: PageRecord · content: ContentFile | nulldocumentedRoute-level template.
IndustryCategoryTemplatetemplates/industry/IndustryCategoryTemplate.tsxThe industry category landers.row: PageRecord · content: ContentFile | nulldocumentedRoute-level template.
ArticleTemplatetemplates/longform/ArticleTemplate.tsxBlog posts and guides.row: PageRecord · content: ContentFile | nulldocumentedRoute-level template.
ComparisonTemplatetemplates/longform/ComparisonTemplate.tsxComparison pages, where the cross and check table is the centrepiece.row: PageRecord · content: ContentFile | nulldocumentedRoute-level template.
CaseStudyTemplatetemplates/work/CaseStudyTemplate.tsx38 case studies.row: PageRecord · content: ContentFile | nulldocumentedRoute-level template.
WorkIndexTemplatetemplates/work/WorkIndexTemplate.tsxThe editorial work index: hairline rows and a fixed preview panel.row: PageRecord · content: ContentFile | nulldocumentedRoute-level template. Its row and preview grammar is rendered in section 14.
ChildLaneDirectorytemplates/service/ChildLaneDirectory.tsxThe hairline directory under a category page.consumed by the service templatesdocumentedTemplate part.
ServiceHerotemplates/service/ServiceHero.tsxThe short service hero with the grader beside it.consumed by the service templatesdocumentedTemplate part.
service/parts.tsxtemplates/service/parts.tsx17 bands: ZoneHeader, Prose, ContentPlate, ItemGrid, StepLadder, RowList, ProofBand, RecordStrip, PriceAnchor, ReviewBand, FaqStack, MidCta, CloseBand, LogoWallBand, BandMedia, ContentBand, ServiceStyles.each takes its own slice of the service content filedocumentedTemplate parts. Every one is a composition of the primitives above.
industry/bands.tsxtemplates/industry/bands.tsx16 bands: IndustryHero, WhyBand, LanesBand, ChecklistBand, InclusionsBand, ProofBand, ProcessBand, TimelineBand, PricingBand, QuestionsBand, ProseBand, ChildDirectory, AboutBand, Prose, QuietLink, ProofRows.each takes its own slice of the industry content filedocumentedTemplate parts.
industry/compose.tsxtemplates/industry/compose.tsxFold, MidCta, Reviews and PageClose: the shared industry page spine.consumed by the industry templatesdocumentedTemplate parts.
longform/parts.tsxtemplates/longform/parts.tsx10 parts: StatementLine, Plate, LongFormHero, ContentsRail, ComparisonBlock, Movement, FigureRows, RelatedRail, OperatorNote, CloseBand.LongFormHero: row · kind · eyebrow · statement · subline · primary? · secondary? · scale?: 'display' | 'feature'documentedTemplate parts.
work/parts.tsxtemplates/work/parts.tsx10 parts plus the GRADER constant: MonoTag, Crosshead, Prose, Plate, GalleryPlate, StatBand, FactRows, LinkRows, WorkClose, WorkStyles.consumed by the work templatesdocumentedTemplate parts.
work/narrative.tsxtemplates/work/narrative.tsxNarrativeSection: the 65ch case-study body with mono crossheads.consumed by CaseStudyTemplatedocumentedTemplate part.
styles modulestemplates/*/styles.tsxIndustryStyles, LongFormStyles and ServiceStyles: the per-family fold rules, kept out of the band files so a band stays declarative.nonedocumentedStyle-only modules. They render a stylesheet, not a visual.
Before you author a component: check this table, then check shadcn. Two out of three “new” components are a row with a different nameIf you do author one: it composes primitives and holds layout. A presentational band never re-implements a controlA new component appears here by adding a row to src/app/design-system/_doc/roster.ts, and its specimen by importing it into this page. Both, or it is undocumented
14 · page recipes

Thirteen archetypes cover all 190 pages.

Every route in site-data/pages.json resolves to one of these. Compose from the parts above; never restyle one.

service page · ×33
Short hero + grader → problem strip (✕, lane-specific) → what I do (✓ grid) → proof (case + stats) → steps → pricing anchor → FAQ → close.
industry page · ×25
Service skeleton; section 3 becomes “the lanes that matter for [industry]” (tool cards → services); proof swaps to industry review + case.
category lander · hubs
/services/, /industries/, guide hubs, the skills library. Directory pattern: SectionHeader per group + hairline row-lists, counts in mono, chip filters on top. The quietest page type — rows, never card walls.
work index · galleries
Editorial index: hairline ROWS (title + mono tag + one stat), fixed preview panel on the right that swaps on row hover, mono chip filters. 38+ items stay scannable.
case study
Tag + display headline → contained plate → 3 sparkline stats → 65ch narrative with mono crossheads → next-case rows + CTA band. Galleries of shots ride as contained plates in the narrative.
comparison page
The ✕/✓ table IS the centerpiece: short hero → full comparison table (tinted header, dark Justin column, Bjarmi hairline flag) → prose sections unpacking each row → CTA band.
blog post
Mono date + section headline → featured plate → 65ch body, italic-voice pull-quotes → 3 related cards + CTA band.
resource / tool
Functional-first: headline + the working tool above the fold; guides read like posts with a tool-card rail.
demo / showcase
Framed live: the demo RUNS inside a contained browser-chrome plate (12px radius, hairline chrome bar with dots + URL) → mono spec rows below (stack, build time, what it does) → “how it was built” case link.
pricing page
Homepage tiers expanded + a scope BUILDER: pick lanes (mono chips), the price floor recomputes live ($1,000 base + per-lane). Terms strip, tier ✕/✓ table, FAQ. The builder is the one interactive moment.
about · the operator
Operator dossier: portrait plate → fact rows (Lucide + fact) → 13-year timeline as numbered mono rows → the Aesir → reviews + badges. No letter-prose; evidence.
contact
Minimal: the same-day promise stated large, grader + consult pair, NAP block, map plate (b&w). Nothing else.
location / geo
Service skeleton + local proof (map plate, local reviews, local cases). Never thin duplicated copy.
the editorial index, renderedgalleries · hubs · work
all · 38ai systems · 12websites · 15
The 63-agent marketing systemai system · ↑ 12×
Valhalla client portalportal · 100% logged
The spec-site factorywebsites · 3× close
fixed preview · swaps on row hover
the demo frame, rendereddemo · showcase archetype
demo.justinharris.ai/graderlive
the live tool runs here
stack · claude + n8nbuild · 3 dayshow it was built →
usage
<DemoFrame
  url="demo.justinharris.ai/grader"
  meta={['stack · claude + n8n', 'build · 3 days', 'how it was built →']}
/>
Hard rules: one dark CTA moment between hero and closeevery page ends with the grader askno invented stats — TK markersindexes are rows + mono chips, never card walls
15 · everything at once

A section built from the parts above and nothing else.

03 · local seo

Own the map pack before they scroll.

46% of searches are local. The three names in the map pack take the call.

Google Business Profile, rebuilt and maintained
Review velocity system: asked, answered, reported
Service pages built around real search demand

Eyebrow recipe, section scale, italic cap, side-note measure, ✓ list, button pair, contained plate, hairline discipline — every value on this page, nothing invented. This is the bar for all 190 pages.

16 · token reference

Every name, verbatim.

69 distinct names at :root, plus 6 re-declared under 760px. 75 declarations, one file: src/styles/v2-theme.css.

Every swatch, rule and specimen on this page is painted from one of these. If a surface disagrees with this table, the surface is the defect.

The chips are previews, painted with var(--token) at render time. The value beside each one is authored text, transcribed from the stylesheet. They are deliberately two separate facts, so a divergence between them shows up on this page as a visible mismatch rather than sitting quietly in a document nobody re-reads. There is no Tailwind @theme block and no --color-* alias set: if a utility layer is ever wanted it belongs in globals.css as aliases pointing at these names, never as a second vocabulary.

ink and paperThe monochrome base. Nine steps, and no tenth. Every grey on every surface is one of these.
previewtokenvaluerole
--ink-950#0a0b0bnear-black: dark grounds, dark cards
--ink-900#151717elevated dark card on a dark ground
--ink-800#222222primary text on light
--ink-600#4a4a48body text on light
--ink-500#6b6b68secondary text on light
--ink-400#8a8a86eyebrows, muted labels
--ink-300#b0b0addisabled and muted marks on light
--paper-0#ffffffwhite section ground
--paper-50#f5f5f5default page ground
text on darkThree steps, used only over an ink ground. Never inverted from the light ramp.
previewtokenvaluerole
--dark-text-1#e8e8e8headings and primary copy on dark
--dark-text-2#b8b8b6body copy on dark
--dark-text-3#a0a0a0side notes and captions on dark
bjarmi, the one accentTwo sides only, ink on light and glow on dark. It is never a surface colour. The name is Bjarmi, everywhere, in every surface and every document.
previewtokenvaluerole
--bjarmi-ink#547070the accent on light grounds. The only form that carries words on paper, at 4.90:1
--bjarmi-glow#87a6a6the accent on dark grounds. The live thing
--bjarmi-tintrgba(84, 112, 112, 0.06)the one tint in the system: comparison-table headers, nothing else
hairlinesAlpha, not hex, so a rule rides whatever ground it lands on. There are no solid borders in this system.
previewtokenvaluerole
--line-lightrgba(0, 0, 0, 0.12)the standard hairline on a light ground
--line-light-softrgba(0, 0, 0, 0.06)ghost card edges on light
--line-darkrgba(255, 255, 255, 0.12)the standard hairline on a dark ground
--line-dark-softrgba(255, 255, 255, 0.08)ghost card edges on dark
semantic aliasesThe layer a component should reach for. A section that needs body copy asks for --text-body, not for --ink-600, so a value can move once and land everywhere.
previewtokenvaluerole
--text-headingvar(--ink-800)every heading on a light ground
--text-bodyvar(--ink-600)every paragraph on a light ground
--text-secondaryvar(--ink-500)side notes, captions, table cells
--text-eyebrowvar(--ink-400)the mono eyebrow on light
--surface-pagevar(--paper-50)the default page ground
--surface-raisedvar(--paper-0)the alternating white section
--surface-darkvar(--ink-950)the dark moment, reserved
--accentvar(--bjarmi-ink)Bjarmi on light. What a component asks for by default
--accent-on-darkvar(--bjarmi-glow)Bjarmi on dark
shadowsTwo, and only two. Everything else in this system is flat. A third shadow is a defect, not a variation.
previewtokenvaluerole
--shadow-field0 10px 40px rgba(10, 11, 11, 0.35)the grader field, lifted off a dark ground
--shadow-lift0 24px 48px rgba(10, 11, 11, 0.22)the featured price card, and nothing else
facesTwo families. Schibsted Grotesk carries display, headings, body and UI. IBM Plex Mono carries eyebrows, annotations, stat captions, row numbers and table headers, and nothing else.
previewtokenvaluerole
--font-sans'Schibsted Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serifeverything a human reads as a sentence
--font-mono'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospacelabels only. Never a paragraph
the display scaleFour fluid steps and two card sizes. Heading weight is always 500: contrast comes from size, never from a heavier weight.
previewtokenvaluerole
--type-heroclamp(56px, 6.8vw, 96px)hero h1. lh 1.02, ls -0.045em, w 500
--type-displayclamp(56px, 6.6vw, 96px)page-scale statements: the problem band, the close. lh 0.98 to 1
--type-featureclamp(44px, 4.4vw, 60px)feature sections: the answer, the operator. lh 1.02, ls -0.04em
--type-sectionclamp(40px, 4vw, 52px)the standard section h2. lh 1.06, ls -0.035em
--type-card-title19pxcard h3. w 500, ls -0.015em
--type-card-title-lg21pxcase and blog card h3. lh 1.25 to 1.3
body and monoTwo measures only: a 520px lede and a 340px side note. Long-form article bodies cap at 65ch.
previewtokenvaluerole
--type-lede17pxthe lede under a heading. lh 1.65
--type-body14.5pxcaptions, card copy, table cells. lh 1.6 to 1.65
--type-small13.5pxthe smallest running copy. lh 1.55 to 1.6
--type-caption13pxplate captions and figure notes
--type-eyebrow9.5pxthe mono eyebrow. ls 0.24em, uppercase, always this recipe
--type-micro10pxmicro labels and dates. ls 0.2 to 0.24em, uppercase
--measure-lede520pxthe lede measure
--measure-side340pxthe side-note measure
--weight-heading500every heading, at every scale. There is no 600 and no 700 display
--eyebrow-gap20pxeyebrow to heading, everywhere, no exceptions
structureOne container, one gutter, one section rhythm. A component never carries a breakpoint of its own.
previewtokenvaluerole
--container1280pxthe one content width
--gutter48pxthe container inset. Steps to 24px under 760px
--section-y150pxstandard section padding. Steps to 90px under 760px
--section-y-display160pxdisplay moments: the problem band, the close
--header-offset120pxscroll-margin-top, so an anchored section clears the sticky bar
--gap-heading-block56pxsection header to its content
--gap-grid48pxcolumn gaps inside a section grid
--gap-card20pxcard grid gaps. Pricing runs 24px
--gap-inline12pxbutton rows and chip rows
radii and hit targetsThree radii, and nothing else. A hand-authored corner is a defect.
previewtokenvaluerole
--radius-card12pxcards, buttons, photo plates, panels
--radius-inner8pxa button nested inside a 12px field. The only nested radius
--radius-pill999pxthe capacity pill and tags
--button-h50pxstandalone page CTAs
--button-h-nav42pxthe header CTA
--field-h56pxthe grader field shell
motionOne curve for everything. No spring, no bounce, no parallax deeper than 12 percent.
previewtokenvaluerole
--easecubic-bezier(0.2, 0, 0, 1)every transition on every surface
--dur-fast200mscolour and border hovers
--dur-med400msarrow slides and small moves
--dur-slow700msreveals and image scale hovers
--hover-img-scale1.02an image inside a clipped card
--hover-arrow-shift4pxhow far the arrow slides right
under 760pxThe same six names, re-declared on a narrower ground. This is why no component in the system carries a breakpoint of its own. Narrow this window past 760px and the values above change under you.
tokenvaluewas
--gutter24pxwas 48px
--section-y90pxwas 150px
--section-y-display96pxwas 160px
--gap-heading-block36pxwas 56px
--gap-grid32pxwas 48px
--header-offset96pxwas 120px
eight files, two familiesServed from /v2/fonts/. No @import, no runtime request to a third-party origin on first paint. The sans is variable, so one file carries 400 through 600 and there is no separate 500 download. The subsets and unicode-range splits match what Google Fonts serves, so a latin-only page downloads two files, not eight. Both faces are OFL.
specimenfilefamilystyleweightsubset
schibsted-grotesk-var-latin.woff2Schibsted Grotesknormal400 600latin
schibsted-grotesk-var-latin-ext.woff2Schibsted Grotesknormal400 600latin-ext
schibsted-grotesk-var-italic-latin.woff2Schibsted Groteskitalic400 600latin
schibsted-grotesk-var-italic-latin-ext.woff2Schibsted Groteskitalic400 600latin-ext
ibm-plex-mono-400-latin.woff2IBM Plex Mononormal400latin
ibm-plex-mono-400-latin-ext.woff2IBM Plex Mononormal400latin-ext
ibm-plex-mono-500-latin.woff2IBM Plex Mononormal500latin
ibm-plex-mono-500-latin-ext.woff2IBM Plex Mononormal500latin-ext
Values may move. Names may not. a rename is 200 silent rendering failures, not a refactorOne authored layer: src/styles/v2-theme.css. A hex, size or radius written anywhere else is a defectOne motion invariant, carried with the tokens: prefers-reduced-motion stops every animation and transition on every surface, so no component has to remember to honour it
justin harris digital marketing · design system v2.1 · internalcanon: design.md · index: site/DESIGN-SYSTEM.md