Substrate mirror — what the Astro foundation actually defines

Build Tracker

Every specimen below is styled by app.css alone — the same sealed classes the live pages use, so one CSS edit updates this catalog and the live site together. It's a mirror, never a hand-rolled copy; if something looks wrong here, the substrate is wrong. Read it top-down as a build-a-page guide: §1–§2 foundation (tokens + base elements) → §3–§11 the atom & component vocabulary (forms, buttons, tags, editorial, status, nav, cards, section patterns) → §12–§20 templates, which compose those atoms into each page type. An atom used on only one page is documented in that page's section. Nothing is sealed until you walk it and say so.

Item tags in the lists below — in Astro app.css · 🔜 sealed in staging, not yet ported · ⚰️ staging relic (was being retired — don't port).

⚠️ These notes record what an implementer believed staging looked like — they can be wrong (some were written from the STALE ~/Sites/cnxlocal mirror). The source of truth is DEPLOYED staging.cnxlocal.com — verify against the live rendered page + its output.css before trusting any entry here. See CLAUDE.md §"WordPress design reference".

Brand assets

🟠 Awaiting review
Long wordmark (horizontal — no mountain mark)
CNXlocal wordmark in dark ink

long · default (ink)Use on parchment / light surfaces. cnxlocal-logo-long.png

CNXlocal wordmark, gold CNX and teal local

long · colorHero / marketing surfaces where the brand needs to feel warm. Sparingly. cnxlocal-logo-long-color.png

CNXlocal wordmark in white

long · white reverseUse on --color-dark surfaces (footer, modals, photo overlays). cnxlocal-logo-long-white.png

Square mark (full identity — mountain + plane + wordmark)
CNXlocal full mark in dark ink

square · default (ink)Default identity on light surfaces. Favicon, share image, square avatar. cnxlocal-logo-square.png

CNXlocal full mark with gold mountain

square · color-1 (warm)Gold mountain + gold "CNX" + teal "local". Warm-leaning marketing surfaces. cnxlocal-logo-square-color1.png

CNXlocal full mark with teal mountain

square · color-2 (cool)Teal mountain + gold "CNX" + teal "local". Cool-leaning surfaces; cafe / location features. cnxlocal-logo-square-color2.png

CNXlocal full mark in white

square · white reverseHeader on photographic hero, mobile menu, dark modal headers. cnxlocal-logo-square-white.png

✅ Done

  • Long wordmark — 3 variants (ink, color, white reverse)
  • Square mark — 4 variants (ink, color-1, color-2, white reverse)
  • Each variant ships as PNG + WebP in /media/theme/images/logos/ (ported from staging)

⏳ Pending / deferred

  • Favicon / touch-icon set (favicon-32/192, apple-touch-icon) — verify against the square mark

⚠ Debt

  • No CSS class layer for logos — raw <img> assets (expected; not a gap)

§1. Tokens

🟠 Awaiting review
Brand colours — bg-* utilities from --color-* tokens (name · hex)
dark#00272b
bg#f5f0e8
primary#057080
primary-dark#044d57
primary-light#3ab0bf
accent#e8aa14
accent-dark#d09a0f
highlight#afe3c0

--color-primary (#057080), --color-primary-dark (#044d57) and --color-ink-light (#557073) are deliberately darkened from the sealed staging values for WCAG AA contrast — keep. --color-accent-dark reverted to the sealed #d09a0f.

Ink + surface
ink-mid#3a5a5e
ink-light#557073
white#ffffff
borderrgba(0,39,43,.1)
Status + Astro-only — error · focus · season palette
error#d64933
focus#3ab0bf (=primary-light)
season-cool=primary
season-hot=accent
season-wet#95daac (mint highlight)
Gray ramp (--color-gray-50 → 900)
50
100
200
300
400
500
600
700
800
900
Typography (font-heading / font-body / font-decorative)

League Gothic — display heading

Source Serif 4 — body copy, 1rem base, 1.75 line-height

Reenie Beanie — decorative accent

Micro type scale + tracking (Astro-only tokens)

--text-2xs 0.7rem · --text-3xs 0.65rem · --text-4xs 0.6rem · --text-h1-landing clamp(2.8rem, 6vw, 5rem) · --tracking-eyebrow 0.22em · --tracking-hero 0.28em

Container widths (max-w-* from --container-* tokens)
400
420
540
640
760 · reading
800
900
960
1000
1100 · default
1200 · wide
Breakpoints (Tailwind stock, mobile-first)
sm 640px md 768px lg 1024px xl 1280px 2xl 1536px
Radius — sharp (0) · card · organic. --radius-minimal (2px) is BANISHED: Bootstrap-esque uniform rounding fights the scrapbook/paper language.
sharp · 0
card
organic
Elevation — resting .card → hover .card-lift (--shadow-card / --shadow-lift)
--shadow-card
--shadow-lift (hover)
Motion — hover a chip to compare durations
fast · 0.15s base · 0.2s slow · 0.3s

✅ Done

  • Brand colours (8) + 10-step gray ramp · ink / surface / border · all labelled with hex
  • 3 font families · micro-type scale + tracking tokens · 11 container widths (400→1200) · 5 breakpoints
  • Radius (minimal/card/organic) · shadows (card/lift) · motion (fast/base/slow)
  • Astro-only additions surfaced: error · focus · season palette (cool/hot/wet)

⏳ Pending / deferred

  • ✓ a11y darkenings (primary / primary-dark / ink-light) + brand-based status/season palette accepted at seal (Lily, 2026-06-18)

⚠ Debt

  • Back-compat aliases (--color-brand-bg/ink/accent) removed in clean refactor — verified unreferenced (0 var() hits). Awaiting review.
  • Astro-only token additions (cross-ref): micro type-scale --text-2xs/3xs/4xs · --tracking-eyebrow / --tracking-hero · --container-420 — staging lacks these. 🟠 Awaiting review.

§2. Base elements

🟠 Awaiting review
Headings (bare markup, no classes)

Heading level 1

Heading level 2

Heading level 3

Heading level 4

Heading level 5
Heading level 6
Paragraph + prose link

This is a paragraph of body copy. It contains a prose link with the gold highlighter that grows on hover. The body face is Source Serif 4 at 1rem with a 1.75 line-height.

A second paragraph to confirm the margin-bottom rule, plus a second link for hover-state grins.

Inline elements

This shows strong, emphasis, inline code, highlighted via mark, HTML, and small text — plus a key like Esc or Ctrl + K.

Lists
  • Unordered item — teal bullet via ::marker
  • Second item with a prose link for good measure
  • Nested:
    • Child item one
    • Child item two
  1. Ordered item one
  2. Ordered item two
  3. Ordered item three
Blockquote

"This is a blockquote rendered from bare markup. The cite that follows uses the heading face."

— Some attribution
Table
CafeNeighborhoodWi-Fi
Roast8ryNimmanYes
Akha AmaOld CityYes
GraphNimmanYes
Definition list
Songthaew
Covered red pickup truck with two bench seats in the back — Chiang Mai's local shared taxi.
Tuk tuk
Three-wheeled open-air taxi. Iconic; not a practical daily option for families.
Code block (pre + code)
$ pnpm -F @cnxlocal/web build
280 pages
$ pnpm verify
Figure + caption
A bare <figure> with <figcaption> — italic, ink-mid, centred.
Details / summary
What's inside this collapse?

Bordered surface with a heading-face summary and a teal disclosure marker. Opens to reveal whatever prose sits inside.

Horizontal rule
Image (just the base img rule) A Chiang Mai street scene

✅ Done

  • body · h1–h6 · p · a · p a · img
  • blockquote · table · hr · ul/ol
  • code · pre · kbd · mark · small · abbr
  • dl/dt/dd · figure · details · strong/em

⏳ Pending / deferred

  • Form controls → §3
  • Markdown block wrappers → §3

§3. Form controls

🟠 Awaiting review
.form-input — standard data-entry field
.form-label-required — label + asterisk (input gets required + aria-required)
.form-input--sm — compact variant (Astro addition)
.form-check — checkbox + radio .form-inline — underline + ink submit
.form-email — shadow-lifted input + ink submit (layout + button: utilities) .form-paper — contact paper (class="card form-paper p-6 sm:p-10")
States — disabled, .is-error, .is-success

Please enter a valid email address.

Looks good — we'll send confirmation here.

Bare inputs (no DS class — preflight + forms-plugin reset only)

✅ Done

  • @tailwindcss/forms base reset
  • .form-field · .form-label · .form-label-required
  • .form-input · .form-check · .form-inline · .form-email
  • .form-paper · :disabled · .is-error · .is-success
  • .form-error · .form-help · .form-input--sm (Astro)

⏳ Pending / deferred

  • File input — no use case yet
  • Input with leading icon
  • Switch / toggle · range slider · date picker

⚠ Debt

  • Focus = a11y OUTLINE ring (WCAG 2.4.7) not staging's box-shadow ring — intentional Astro divergence, kept.

§4. Buttons & button-links

🟠 Awaiting review
gold
default link sm link
teal
default link sm link
outline
default link sm link
With leading icon — contextual actions (map, envelope, etc.) With trailing arrow — forward-action CTAs (continue, learn more, next) Disabled state
Loading state — .is-loading (wrap content in a child so the spinner has something to hide)
Loading
.text-link — button-shaped CTA link (not a real button) .btn--outline-dark — transparent/ink, fills dark on hover (shown on its real gold surface) .btn--dark · .btn--lg — Astro additions
ghost — same outlined style as outline, switched for dark surfaces

✅ Done

  • gold · teal · outline · ghost variants
  • Sizes sm · default · :disabled
  • .is-loading spinner · icon support · renders the same as <button> / <a>
  • .text-link — button-shaped CTA link
  • .btn--dark · .btn--lg · :focus-visible gold ring — Astro additions

⏳ Pending / deferred

  • Destructive variant
  • Icon-only square button
  • Button group / segmented control
  • CMS-emitted button parity → markdown elements layer

⚠ Debt

  • .btn--outline / --outline-dark hover fills aligned to staging — Lily to confirm.

§5. Tags, badges, chips

🟠 Awaiting review
.tag — standalone (teal border)
Wi-Fi Outdoor Quiet Vegan
.card-tags > .card-tag — in-card filled ink chip; nomads/expats/visitors umbrellas get brand colours (teal/gold/jade) and sort first
Nomads Expat Families Visitors Travel Food
.badge (+ --success / --warn / --error) — solid status chip (Astro addition)
Open Popular Closed
.filter-pill — gold-active directory chip (Astro addition)
.card-tag-on-dark — white border, white text; category chips on hero photos
Featured Expat Families Travel Food

✅ Done

  • .tag — standalone, teal border
  • .card-tags + .card-tag — in-card filled ink chip; .card-tag--nomads/--expats/--visitors add brand colours (teal/gold/jade), sorted first, all shown (live staging)
  • .card-tag-on-dark — on-photo chip (dark surface)
  • .badge (+ success/warn/error) · .filter-pill — Astro additions

⏳ Pending / deferred

  • Status badges (.badge-new, -popular, -closed)
  • Removable chip close button (.tag-x)

§6. Editorial accents

🟠 Awaiting review
Canonical heading stack — .eyebrow + h2 + .lead

From the field

Section heading

A lead paragraph sets up the section with larger, italic type than body. Used across the article archive, single posts, and most landing pages.

.eyebrow color variants — default (teal) and .text-accent (gold, dark-surface override)

Your insider guide to living in

Cafe directory

.callout (jade)

This is a callout — used sparingly for important asides that shouldn't be confused with body copy. Jade tint, jade left-border.

font-decorative (Reenie Beanie)

— from Lily's notebook

.kicker — small uppercase meta-label (Astro addition)

5 min read · Updated June

.testimonial — pull-quote card (quote + attribution); .testimonial-text + .testimonial-author

"I came for two weeks and stayed two years. The pace, the food, the cafes, and a community of remote workers that actually wants you around."

— Jamie, designer · Brooklyn → Nimman

.tape-corner — composable gold tape accent; composes onto any paper card with position:relative (+ -left / -right angle variants)

Pinned to the page

A gold tape strip composes onto any paper card holding position: relative — no new class needed on the host.

✅ Done

  • .eyebrow (responsive 1.2→1.5rem @640, flat — no tilt) · .lead
  • .callout (jade)
  • font-decorative utility · .kicker small meta-label (Astro)
  • .testimonial (+ -text/-author) · .tape-corner (+ -left/-right)

⏳ Pending / deferred

  • .pullquote — bare blockquote covers it
  • .figure-caption — bare figcaption covers it

§7. Status & feedback

🟠 Awaiting review
.alert-info
info An informational notice about something readers should know.
.alert-success
check_circle A success confirmation — the form was sent, the post was saved.
.alert-warn
warning A warning the user should notice before continuing.
.alert-error — destructive (Astro addition)
error An error message — something went wrong.
.skeleton (shimmer loader)

✅ Done

  • .alert · .alert-info · .alert-success · .alert-warn
  • .skeleton + @keyframes shimmer
  • .alert-error · prefers-reduced-motion skeleton guard — Astro additions

⏳ Pending / deferred

  • .alert-error destructive variant — now built (Astro)
  • Empty-state · 404 layout · toast / snackbar · inline spinner

⚠ Debt

  • Icons render via Material Icons here; live consumers (Search.tsx / Showtimes.tsx) use the FontAwesome equivalents.

§9. Cards

🟠 Awaiting review

🧱 Photo-card primitives

.polaroid — vertical photo-card primitive (max-width 340px)
landscape
— Doi Suthep, March
.postcard — landscape "photo + message" primitive
landscape
temple_buddhist
From Chiang Mai

Wish you were here.

A few lines about the place, the season, the cafe round the corner — the message side of a postcard, sized for ~25 words.

📰 Editorial & article surfaces

.card + .card-lift — base card primitive; riffed into article / benefit / offer cards. Image block in .c-teal / .c-jade / .c-gold.
image

.card-img-block.c-teal

.card · .card-lift · .card-body

local_cafe

.card-img-block.c-jade

map

.card-img-block.c-gold

🏷️ Benefit cards

.benefits-grid > .benefit-card — icon, vertical
wifi

Cafe-hop friendly

Walkable density, fast Wi-Fi, espresso under 80 baht.

savings

Lower cost of living

Western comfort at 30–40% the cost.

terrain

Mountains on the doorstep

Doi Suthep is 20 minutes from the Old City moat.

.benefit-card.benefit-horizontal — icon, horizontal
pedal_bike

Two-wheel commute

Scooter rental is 2,500฿/month; most things are inside a 4 km circle.

.benefit-card.benefit-photo — photo, vertical (title + body inside the polaroid)
local_cafe

Cafes you'll actually work from

Hand-picked for WiFi, outlets, and espresso — not just photogenic.

terrain

Nature on the doorstep

Doi Suthep, the moat parks, Huay Tung Tao — all in arm's reach.

school

Schools that don't price you out

Three tiers across the city. We'll walk you through them.

.benefit-card.benefit-photo.benefit-horizontal — photo, horizontal (composes .postcard)
restaurant
local_fire_department

Eat like a local for the price of coffee back home

Khao soi, boat noodles, papaya salad — every neighborhood has a stall doing one of them better than the place down the street. We'll point you at the right ones.

✅ Done

  • .card family (.card-lift, .card-img-block + .c-*, body/title/excerpt/meta/tags, .card-cta)
  • .polaroid (+ -caption/-stamp) · .postcard (+ -stamp)
  • .benefit-card family (icon + photo × vertical + horizontal)
  • Astro additions: .panel · .listing-card · .article-card--horizontal

⏳ Pending / deferred

  • polaroid-as-cafe-card → §19 · neighborhood card → §16 · archive featured-grid → §16
  • FLAG: featured .card-title 1.6rem vs staging 1.75rem; .card-meta 0.7 vs 0.72rem — confirm.

§10. Site chrome

🟠 Awaiting review
Site header — the REAL <SiteHeader> component (the exact one BaseLayout renders on every page), fed by the shared buildNavItems() from ~/lib/nav.ts. No hand-rolled copy — edit the nav in one place and it updates here too. Shown in-flow via the showcase prop (the live header is position:fixed); hover Articles / Map / Tools to open the real mega-menu. The transparent-over-hero state is this same element's scroll variant — walk it in situ on a hero page.
CNXlocal — Your Guide to Chiang Mai
Site footer — the REAL <SiteFooter> component (the exact one BaseLayout renders), incl. the Explore-column Search button that opens the live SearchModal (rendered just below so the search works here too). Footer nav is its own thing — topic columns, separate from the header nav.

✅ Done

  • Header — 64px nav, transparent → solid scroll-state, logo + links + gold CTA
  • Footer — 4-col grid + legal strip · .footer-link
  • Logos — header white wordmark, footer colour wordmark

⏳ Pending / deferred

  • Header band + mega disclosure + mobile drawer stay utility chains — reviewed in situ (hover Map/Tools on home)

⚠ Debt

  • Mega menu + scroll-state + mobile drawer are Astro-ahead chrome for Kris's functional menu (guardrail 1c) — KEPT.
  • .skip-link + .footer-link diverge from staging for a11y/AA — KEPT; .pillar-card layout RESOLVED — it is the homepage dispatch tiles (intent-routing); <sm it stacks icon + title above the body copy (was a 2-col icon|body reflow).
  • Footer column headings are a hand-rolled chain — candidate .footer-heading.

§11. Section patterns

🟠 Awaiting review

🏛️ Hero variants

The cross-template heroes live here: .hero--page (the standard 55vh page header — eyebrow + h1, optionally + .hero-sub / .hero-actions) and .hero--ink (the archive / search ink band). Single-use heroes are documented where they're used: home → §18, landing → §13, article → §14. Shown at representative heights (inline height — build-tracker is allowlisted for inline style, §function not drift).

.hero.hero--page — 55vh page-level (eyebrow + h1 only — minimum content)
The people behind

About CNXlocal

.hero.hero--page — with .hero-sub as last element
Drop us a line

Contact and FAQs

Get in touch with our team for support, questions, or partnership opportunities.

.hero.hero--page — with .hero-actions as last element
Free 10-day course

Move with Confidence

Walks you through everything you need to plan a smooth, successful move.

Ink-band hero — .hero--ink (archive / search header). Shown at natural padding height.
Tools

Ink band hero

📣 Full-bleed & band CTAs

.cta-photo — full-bleed dark photo CTA (.bt-fit-cta shrinks min-height)
One hour. Real answers.

Move to Chiang Mai with Confidence

Book a $100 one-hour consultation with a local expert who's helped families like yours find their footing.

Book Your Consultation
.cta-band — flat teal band (default), text left + action right

Join the Chiang Mai Families group

5k+ expat families swapping notes on schools, housing, doctors, the works.

groups Join the group
.cta-video — the video variant of the band CTAs: a full-bleed looping <video> under a bg-dark/60 overlay. Gold eyebrow (.text-accent); the h2 + .lead are auto-whited by the sealed dark-surface inversion (no per-element text-white). Used once, on the consultation landing.
Ready when you are

Put That Expertise to Work for Your Family

One focused hour — $100, money-back guaranteed. Leave with a clear, personalised plan for your family's move.

Book Now

🗂️ Featured-grid layout

.articles-grid + .article-card--featured (homepage Latest Articles)

✅ Done

  • .hero--page (baseline page header) + .hero--ink (archive / search). Single-use heroes documented at their pages: .hero--home → §18 · landing .hero min-h-screen → §13 · .hero--article → §14.
  • .hero-eyebrow flat, responsive 1.4→1.8rem @640 · .hero-sub · .hero-actions
  • .cta-photo + sub-parts · .cta-band + .cta-band-inner (--gold/--ink) · .steps-band
  • .articles-grid + .article-card--featured
  • Astro hero addition: .hero--ink

⏳ Pending / deferred

  • .section-header → §12 · .cta-newsletter → §13/§20 · .cta-photo--profile → §12/§20
  • .place-grid/.place-card → §18 · .pillar-card → §10/§20

⚠ Debt

  • ⚰️ Relics — do NOT re-import: .hero-landing (≠ Astro's .hero--landing), .brushstroke-cta, tear effect (.tear-*) — retired in staging 2026-06-03.

§12. Standard page template

🟠 Awaiting review

Page-template layering — pages aren't hand-coded from scratch; they're built from a shared base. BaseLayout is the universal chrome base every page wraps (<head> + SEO · SiteHeader · SiteFooter · <main> + skip-link). BasePage (layouts/BasePage.astro) sits on it as the standard content base for plain pages — fixed-header clearance + .section rhythm + .section-inner measure + optional <h1> + a .body-prose slot — mirroring staging's page.php. Richer templates (the post §14, the landings §13, hero + directory pages) sit on BaseLayout directly with their own structure.

BasePage — single column, reading measure (760), plain h1, prose. What /privacy-policy + /terms render.

Privacy Policy

A standfirst paragraph sets the page up at the reading measure.

A section heading

Body copy runs the site-standard reading column — the same prose the article body uses; links, lists and headings inherit the §2 base styling.

✅ Done

  • BaseLayout — universal chrome base (every page): <head> + SEO · SiteHeader (§10) · SiteFooter (§10) · <main> + skip-link.
  • BasePage (layouts/BasePage.astro) — standard content base for plain pages: header clearance + .section rhythm + .section-inner measure (--reading 760 default · 1100 · --wide 1200) + optional <h1> + .body-prose slot. Mirrors staging page.php; used by privacy + terms.
  • .section primitive + .section + .section adjacency · all --container-* tokens (byte-aligned to staging) · Tailwind v4 stock breakpoints.
  • Distinct templates sit on BaseLayout directly: hero pages (.hero--page/about, /contact) · directory pages (kids / coworking / neighborhoods) · the post (§14) · the landings (§13).

⏳ Pending / deferred

  • page.php-era patterns — adopt or retire per page
  • .section-header — promote to §11 once a callsite ships
  • .cta-photo--profile → §12/§20 · .journey-grid · .manifesto-section · .partner-section · .contact-strip

⚠ Debt

  • PDF atoms not in app.css: .section-header, .cta-photo--profile — port when a callsite needs them.
  • --container-420 is an Astro-only step.

§13. Landing page template

🟠 Awaiting review

📌 Landing atoms — ported from staging §13, for review

Landing hero — base .hero + min-h-screen (no .hero-- modifier): FULL-vibrance photo (no .hero-photo dim), custom gradient, 2-col with the offer card pinned right. Same composition as staging page-webpack-offer.php. Used across the landing pages.
One hour · $100 · Money-back

Book a One-on-One Consultation

Navigate your family's move to Chiang Mai with confidence.

Offer card — .card.border-t-4.border-accent + price + checklist + full-width gold button. Pins into the landing hero's right column on real pages.
$100
One hour · Video call · Money-back
  • check 60-minute video call
  • check Tailored visa & housing advice
  • check Free Map Pack included
Book Now arrow_forward
Inline pinned-CTA — .card.tape-corner + gold icon bubble + copy + gold button
headset_mic

Cut to the chase

This isn't just info dumping — we listen, advise, and answer questions blogs can't.

Book your hour arrow_forward
FAQ accordion — the live landing pattern: border-divided rows; each row is a full-width button (League Gothic uppercase question + a chevron that rotates 180° when open); the answer is an Alpine x-collapse panel. First row shown open.
Frequently asked

Questions

Yes — get in touch through our contact form and we’ll share the details.

.steps-band — full-bleed teal instructional band: phone mock + numbered steps + CTA (gold eyebrow; white heading/steps inherit)

Saved to your phone 🗺️

It's as simple as this

How to use the maps

A colour break with its own surface — instructional content that earns a stop in the scroll.

  1. Drop your email — the pack arrives within a minute.
  2. Open any map and tap "Follow" to save it to Google Maps.
  3. Pan around the neighborhoods you're considering.
  4. Compare amenity clusters before you commit.
Get the Free Maps arrow_forward
Final CTA — full-bleed bg-accent + centered white .card ticket + big h2 (teal eyebrow + teal button: the gold-surface standard)
Last call · don't scroll past this

One hour. $100. Let's do this.

Your Thai adventure is one short call away. Money-back guarantee on every consultation.

Book Your Hour arrow_forward
Newsletter widget — a .card shell; the form itself is a provider embed (MailerLite) on real pages. There is no dedicated .cta-newsletter DS class in staging — it's a card + embedded form.
Stay in the loop
Stay Updated

Sign up to get notified when we publish articles and updates.

— newsletter form embeds here —

✅ Done

  • Composes sealed substrate (§2 · §4 · §8 · §9 · §10 · §11 · §12) — no new atoms required
  • Ported specimens: offer card · inline pinned-CTA (.tape-corner) · FAQ disclosure · .steps-band · final gold-surface CTA · newsletter card shell
  • The landing hero (base .hero + min-h-screen + offer card) is shown in §11 hero variants
  • Walk in situ: /email-course · /map-pack-offer · /chiang-mai-for-families

⏳ Pending / deferred

  • FAQ shown in the live accordion style (border rows + rotating fa-chevron-down); on real pages it's an Alpine x-collapse disclosure (static here).
  • Newsletter form is a provider embed (not a DS class) — wires up on real pages

⚠ Debt / flagged divergences (KEPT)

    §14. Post — standard

    🟠 Awaiting review

    📄 Single-post components

    .hero.hero--article — single posts; photo + tag overlay + floating .hero-title-card (h1 + date · read · author). Shown shrunk via .bt-fit.
    Expat families How To

    Getting Around Chiang Mai: A Family Transportation Guide

    March 20, 2026 10 min read Lily Szabo
    .post-banner — any photo as a uniform wide band (~3.34:1, object-cover). Add the class to a body image.
    Chiang Mai banner specimen
    .post-banner.post-banner--titled — the h2 lives in the band (photo + ink overlay + overlaid white title)

    Coffee, Food & Experiences

    .post-banner--titled.post-banner--tall — taller crop (4:3). The three blog h2 styles: pure text · banner photo · tall photo.

    How We Hired Our Nanny

    .toc + .toc-title — sidebar TOC; active item highlighted by the scroll-spy (first row shown active)

    📰 Article footer components

    .breadcrumb (§8) — rendered after the article body Author bio — utilities + the sealed .polaroid avatar (§9, square crop) + .tagline (Reenie Beanie) role; sits on the parchment, not a card. 2026-06-27: componentized as AuthorBio.astro, now per-article (Lily / Kris / Zach Price from data/authors.json, photo from public/authors/<slug>.jpg with an initials fallback) and rendered on BOTH the standard and map_aside post templates with a subtle body→author divider.

    📐 Composition patterns (recipes) — adapt to your data, don't reinvent

    .quick-facts — at-a-glance facts panel (handwritten title · key/value rows). Scoped to .body-prose.
    Quick Facts
    Visa Duration5 years
    Stay Period180 days / entry
    Visa Fee~10,000 THB
    Apply100% online
    🟠 Awaiting review — .quick-facts--single modifier: drops the 2-column newspaper flow to a single full-width column so long, sentence-length values use the whole card width (the default cramps multi-line values). First: the international-schools guide.
    Quick Facts
    Typical annual tuition฿280,000–฿900,000 (~$8,200–$26,500) for full international schools; bilingual options start lower
    Biggest decision factorsCurriculum alignment with future uni plans, commute, budget, and waitlists
    Typical student body200–600 students, 25–40+ nationalities per school
    .recommend — recommendation / affiliate aside (white card · gold icon badge · handwritten teal eyebrow · italic body · teal link). Recipe: swap icon + copy.
    beach_access
    a tip from us

    If you want travel insurance, consider a SafetyWing plan through our affiliate link — budget cover through comprehensive remote-worker plans.

    .space-card — listing-with-specs (eyebrow labels · ✓/✗ availability checks · key/value rate footer). Recipe: swap/drop field groups per entry; keep the check-row + meta-row primitives.
    Amenities
    • Hot Desks
    • Meeting Rooms
    • Private Offices
    Services
    • Co-Working
    • Cafe
    Day Rate฿300
    Monthly฿3,000
    LocationNimman

    ✅ Done

    • .hero--article · .hero-title-card (+ -inner/-meta/-sep) · .post-banner (+ --titled/--tall) · .toc + .toc-title
    • Composes shared sealed atoms: .breadcrumb (§8) · .polaroid (§9) · .tagline (Reenie Beanie role)
    • Article-body recipes (ported staging → app.css): .quick-facts · .recommend · .space-card · .scrapbook-gallery (in-article photo gallery, BEM __photo tiles)

    ⏳ Pending / deferred

    • §14 app.css alignments (awaiting review): body reading scale (16px/1.75 site-standard + scoped 1.15rem standfirst) · scroll-margin-top:6rem on .body-prose headings · .hero--article 45vh→60vh + lighter overlay · .hero-title-card h1 clamp + tokenized max-width
    • Reading-column 640-vs-760 decision is OVR-001-gated (see §2 note)

    ⚠ Debt / flagged divergences (KEPT)

    • pages/[slug]/index.astro L391/L423 — max-w-[1068px]/[1004px] per-page overrides of --container-1200/§12 default 1100; promote to tokens or accept (Lily)

    §15. Post — map aside

    🟠 Awaiting review

    🗺️ The map column — fixed full-height panel (desktop)

    Derives from §14: same hero/prose/footer, narrowed into the left reading column beside a top-to-bottom map panel on the right. The map is Astro's Leaflet panel on Mapbox tiles — it now renders AND functions in situ: hover shows a name chip, click scrolls the article to that block (no link-out), with a light jade wash under the geometry. Spec below. 🟠 Conventions good-enough for launch; seal after.

    Hero in the map-aside context — §14's .hero--article + .hero-title-card narrowed into the left reading column (≈60%), beside the map panel (≈40%, placeholder)
    Parks Outdoors
    (Map)
    Point marker — small at rest, larger on focus; hover brings it to front + shows the name chip; click scrolls the article to that place (the map does not move). Resting · focused:
    map marker, resting (30px) map marker, focused (46px)
    Cursor hover-label (.cm-map-label) — .card-tag type on a white paper pill (ink text, not the filled chip) Nong Buak Haad Park Area polygon (neighborhood maps) — teal fill/outline at rest (--color-primary), gold on hover/active (--color-accent)

    📍 Place-listing card — recipe, the .cnx-place block

    One paper card: photo flush on top, hairline-divided meta footer (hours · price · location — gold icons, teal links), decorative .tagline subtitle below. Title heading level set per block via .cnx-place--h2h6 (defaults h3). Ported staging → app.css.

    Nong Buak Haad Park

    Nong Buak Haad Park
    schedule 5:00 AM – 9:00 PM
    sell Free
    place Old City

    Popular urban park, great for a morning walk

    🖼️ Polaroid photo group — recipe, composes .polaroid (§9)

    In-article photo rows get the .polaroid scrapbook frame (white mount, soft shadow, slight tilt) — never a generic corner-radius.

    ✍️ .tagline — decorative primitive

    One Reenie-Beanie line under a name (neighborhood, park); ink, ~1.5rem, never uppercase. NOT for descriptions.

    Urban convenience with a hip, modern vibe

    👀 Walk these specimens

    ✅ Done

    • .article-map-aside-page / -main / -body / -rail grid · .cm-marker (+ -hovered/-dim) marker states · .map-aside-shell family
    • Map now renders + functions (Astro Leaflet): Mapbox streets-v12 tiles · jade wash on a pane UNDER polygons/markers (non-interactive) · location-icon.png markers (enlarge on select) · teal 0.2 → gold 0.4 polygons · .cm-map-label hover chip = .card-tag on a paper pill (name only)
    • Map↔article sync: identity coupling (data-place-slug/data-hood-slug) — hover = name chip, click = scroll the article to that block (CLS re-align), no link-out from the map
    • Unified neighborhood block (data-driven AND transplant): heading (name only) → Reenie .tagline → large photo → highlights card if available
    • .cnx-place card recipe ported staging → app.css; meta footer now League-Gothic, one-line icon+value rows, links sit in the meta colour + darken to ink on hover (card-meta link convention)
    • Per-neighborhood directory links removed; an occasional .related-card callout (CNX_ARTICLE_REF marker, §5) links to a deep-dive guide where one exists

    ⏳ Pending / deferred (🟠 seal post-launch)

    • ContentMap basemap matrix (streets-v12 article · light-v11 directory · cafe · homepage relic) — decide whether jade + card-tag hover chips extend to directory/cafe maps, and unify the selected-marker (enlarged pin vs gold teardrop)
    • Neighborhood record name is verbose in the source (SEO title); cleaned in code for now — fix at the source post-launch (also tidies map tooltips)

    ⚠ Debt / flagged divergences (KEPT, not force-aligned)

    • location-icon.png marker asset ported from staging; shown at resting 30px · focused 46px
    • Split ratio 58.33/41.66 vs staging --map-col:40% (60/40) — flag
    • Map is sticky-below-nav vs staging FIXED-above-nav (would require touching Kris's nav, guardrail 1c) — KEPT
    • Mobile order is map-below-article vs staging map-above (optional order:-1 fix) — flag
    • Added .article-map-aside-body .cnx-place { scroll-margin-top:5rem } marker-jump offset · 1.75rem left-edge map-shadow gradient

    §16. Archive templates & search

    🟠 Awaiting review

    Polish pass (2026-06-27): capped at max-w-900 (1100 sprawled on large screens), excerpt clamped to 2 lines, and the horizontal-card grid is the shared browse-list treatment (.post-cards-container): 1-col → 2-col (≤md) → 1-col (≤480). At ≤480 the photo caps at a 200px banner with no excerpt. Same recipe as /articles "Latest" + search. See docs/lily-design-system.md.

    The WIRED archive template — this renders the REAL <ArchiveListing> fed a live 3-article slice, NOT a hand-mock, so it cannot drift from /category/expats/ · /category/guides/.

    Expats

    CNXlocal is your trusted guide to life in Chiang Mai.

    Template & nesting. The wired archive is ONE shared body — ArchiveListing.astro — consumed by every archive route (/category/<slug>/, /author/<slug>/, tag + date archives) and the search-results page; it mirrors DDEV's template-parts/posts-archive.php. Each route is a thin page wrapper (pages/category/[slug]/[...page].astro, …) that fetches + paginates, then NESTS <ArchiveListing> inside <BaseLayout>. Full nesting: BaseLayout → ArchiveListing → ArchiveCard (server wrapper, zero-JS) → ArticleCard.tsx. The card markup lives ONCE in ArticleCard.tsx (shared with Search) — edit the sealed design there, never re-implement it.

    Hero. Compact ink band .hero.hero--ink.hero-overlay + .hero-content<h1 class="hero-h1"><em>{title}</em></h1> (title italicised, build-time-curated via archiveTitle(slug)) + an optional .hero-sub line. Matches DEPLOYED staging /category/*. ⚠ The mirror's centered archive.php header is a STALE draft — do not chase it.

    Chips (.card-tags). Horizontal archive cards show ALL their categories — NO cap (vertical cards cap at 3 per §5). ORDER: the three umbrellas nomads → expats → visitors sort to the front (UMBRELLA_ORDER, stable); the rest keep source order. COLOUR: base .card-tag is the filled-ink chip; only the umbrellas add colour via .card-tag--{nomads|expats|visitors} = teal / gold / jade; every other category still gets a .card-tag--{slug} class but falls back to filled ink. Blanks + uncategorized dropped. Built by cleanCategories + cardTagClass + prettyCategory (lib/text.ts); each chip is its own link to /category/<slug>/.

    Excerpt. Authored excerpt when present, else a WP-style auto-excerpt (~55 words, deriveExcerpt(body_html, 55)) — most imported posts have an empty excerpt, so the card would otherwise be blank. Rendered italic + un-clamped.

    Card body + pagination. Image-left md:w-5/12 (auto-aspect) + body md:w-7/12, stacked on mobile; whole-card click (.card-stretch), no button. Read-time chip counts body prose + per-place write-ups. First card eager-loads, the rest lazy. .pagination/.page-btn renders only when >1 page (buildPagination) — the strip above is a 3-page demo (real hrefs on the live route).

    OPTIONAL featured-grid layout still under review (NOT the wired archive layout above)

    Walk in situ: /articles · /category/nomads · /author/lily-szabo · /neighborhoods · /search.

    ✅ Done

    • Tracker §16 renders the REAL <ArchiveListing> (live articlesInCategory(..,'expats').slice(0,3)) — no second copy of the markup, so this specimen can't drift from /category/expats/. Chips · hero · nesting documented in the notes above.
    • .articles-grid · .article-card--featured/--horizontal · §8 filters/pagination
    • Archive cards rebuilt to LIVE staging: full-width horizontal card (image-left md:w-5/12 auto-aspect + body md:w-7/12, stacked on mobile), italic un-clamped excerpt, 1.2rem title. Was wrongly capped at 760 + centered.
    • Category chips = LIVE staging: filled ink chip; .card-tag--nomads/--expats/--visitors brand-colour umbrellas (teal/gold/jade), sorted first, all shown (>3). Reverts the earlier mistaken ink-outline restyle — site-wide across §5/§9/§15/§16.
    • KEPT Astro additions (🟠 Awaiting review): Pagefind search (Search.tsx) · .filter-pill · .hero--ink/--compact · .pagination/.page-btn a11y additions · .articles-grid responsive elaboration · ArchiveCard / ArchiveListing / lib.archive.ts plumbing · EmptyState.tsx.

    ⏳ Pending / deferred

    • Neighborhood card · archive featured-grid layout review

    ⚠ Debt

    • Archive header = .hero--ink (ink band: h1 + sub) — matches deployed staging /category/*. (The local archive.php centered-header draft is stale/unshipped — do not chase it.)
    • Search is Kris's Pagefind setup (Search.tsx + its own .hero--ink band) — left as-is. Astro-ahead of staging's WP search.php; do not rebuild.

    §17. Articles listing

    🟠 Awaiting review

    The "Start Here" articles index (built June 24): hero → cornerstone .articles-grid (most-popular) → ink "Latest" band (full-width horizontal cards) → audience folder-tabs → topic .benefits-grid.topic-grid → closing photo-band CTA. Walk it in situ: /articles.

    Polish pass (2026-06-27): the featured .articles-grid now steps lead+2×2 (lg) → 3×2 (≤lg, with a 6th tablet-only .articles-grid-extra card) → 2×2 (≤md) → single column (≤480, where the photo caps at a fixed 200px banner and the excerpt is hidden). The ink "Latest" band now matches the archive browse-list treatment (.post-cards-container, max-w-900, 2-line excerpt clamp) and shows 4 articles so the 2-col band fills as a 2×2. Cards carry a user-icon byline — author photos live only on the end-of-article bio card. Full reference: docs/lily-design-system.md.

    .lane-tab + .lane-card + .lane-blurb + .btn.lane-cta — audience folder-tabs. Each lane's brand colour + readable ink is set by data-lane (expats=gold · nomads=teal · visitors=jade); the active tab carries its colour and overlaps the card it opens into. (Defined in app.css, not a page <style>.)

    For families putting down roots: schools, healthcare, neighborhoods, and the everyday logistics of running a household with kids in tow.

    See family guides →
    .benefits-grid.topic-grid — the §9 benefit-card (icon, vertical) in a denser 4-col grid; each card links to a category archive.

    ✅ Done

    • Hand-composed page on sealed atoms: cornerstone .articles-grid (featured lead + 2×2) · ink "Latest" full-width horizontal-card band · audience folder-tabs · topic .benefits-grid.topic-grid · closing photo-band CTA.
    • New page-scoped atoms (now in app.css, not a page <style>): .lane-tab / .lane-card / .lane-blurb / .btn.lane-cta / .topic-grid.
    • Editorial model: LANE_CURATION (hand-curated best-6 per lane in a relocation-journey order) + first-umbrella lane placement (category order decides the lane).

    ⏳ Pending / deferred

    • Whole page 🟠 awaiting design review + seal.

    ⚠ Debt

    • Lane folder-tab + topic-grid are articles-only atoms — on seal, confirm whether they generalise or stay page-scoped.

    §18. Homepage

    🟠 Awaiting review

    The homepage is a situational dispatcher (rebuilt June 24): .hero--home with the LiveDataStrip island (live time / weather / AQI / FX — not the static .cnx-ticker) → intent dispatch tiles (.pillar-card) → featured guides (.articles-grid) → consultation + about photo-bands (the About band embeds a .polaroid) → watercolor neighborhood map + .postcard directory row → teal .cta-band with inline MapPackFormTools & Applets scroll-row (.card.card-lift + hand-drawn arrows) → sign-off. Walk it in situ: homepage. There is no .benefits-grid on this page.

    .hero.hero--home — full viewport (100vh) homepage; dimmed photo + gradient, bottom-anchored.
    Your guide to

    Discover your dream life in Chiang Mai

    Explore curated maps and guides. Get expert advice from your CNXlocals.

    LiveDataStrip — the hero strip is a live-data island (time · weather · AQI · USD→THB), client-fetched on idle, so it can't render in this static catalog (description only). Replaces the static .cnx-ticker (now unused). Dispatch tiles — route by situation: .pillar-card (§10) + .pillar-link with a frameless, enlarged illustration in the .pillar-icon slot (live site uses an <img>; tracker shows a glyph) + a destination-cue line. Tools & Applets scroll-row — a horizontal snap-scroll of .card.card-lift tiles flanked by hand-drawn arrows: the #cnx-roughen-arrow SVG feTurbulence filter, an ink-wobble design-language element reused for scroll affordances. Directory postcards over the watercolor map — .postcard (§9) cards sized down + tilted, tucked over the right edge of the watercolor neighborhood map (page-specific composition; walk it in situ).

    ✅ Done

    • .hero--home + LiveDataStrip island · .pillar-card dispatch tiles · .articles-grid · two photo-bands (one embeds a .polaroid) · watercolor map + .postcard row · .cta-band + inline MapPackForm · Tools & Applets .card-lift scroll-row.

    ⏳ Pending / deferred

    • Whole homepage awaiting design + seal

    ⚠ Debt

    • Spec atoms not yet in app.css: .cta-newsletter, .cta-photo--profile, .section-header.
    Deferred for beta — good enough for launch, revisit post-launch

    §19. Specialty tool pages

    🔵 Deferred — beta good-enough
    Cinema substrate (transplanted) — .vtog · .date-pill
    Thu 19 Fri 20
    .season-strip — year season band, shared by /humidor/ + /planner/ (Kris)
    JanAprJulOctDec
    Cool
    Hot
    Wet
    Nov–Feb
    Mar–May
    Jun–Oct

    Two recipes (see lily-design-system.md → Tool pages). Funnel tools (Cost of Living · DTV Eligibility Checker · When to Visit · Neighborhood Quiz): short hero--ink + photo → lead → tool → "From the locals" h2 → RelatedGuides routing to the longform guides, so they don't cannibalise guide SEO. Standalone utilities (Showtimes · Area Converter): tool-first + .body-prose supporting content, no funnel. Only functional widget CSS stays page-specific (page-humidor.css, page-planner.css) — chrome, prose and tables come from the DS. Walk in situ: /showtimes · /humidor · /planner · /rai · /visa-calculator · /cost-of-living-calculator · /quiz/neighborhood.

    ✅ Done

    • Cinema/showtimes widget (Kris): .cnx-cinema-wrap · .vtog · .date-pill · .pills-scroll · timeline .tl-* · .poster-card
    • .cinema-seo-content/-article · .season-strip (+ --cool/--hot/--wet, shared humidor + planner)
    • Cost of Living Calculator ported from staging to its own page /cost-of-living-calculator/ (wraps the CostOfLiving island; the COL article links here); old /quiz/budget/ 301s to it.

    ⏳ Pending / deferred

    • Each tool reviewed on its own page (showtimes / humidor / planner / area-converter / visa / cost-of-living)
    • 🟠 Astro-only tools beyond staging — keep or cut at seal: DTV visa calc, /tools landing. The neighborhood quiz is now core — it's the funnel target for the neighborhood directory's quiz .cta-band (weights the same 8 rating dimensions; see §21).

    ⚠ Debt

    • Page-specific extras live in page-humidor.css / page-planner.css (intentional split, not drift)

    §20. Cafe directory

    🔵 Deferred — beta good-enough
    .cafes-grid + .cafes-grid-item (edge-to-edge tiles)
    Basecamp Trail Suthep
    The Baristro Suthep

    The current filter row + chrome live in §21. The live cafe directory adopts the unified .directory-filter-bar (the legacy .filter-menu is retired — see the rendered specimen in §21), the hero now carries a "Reviewed by Kris" Reenie-Beanie eyebrow plus a "Which cafes should I add next?" .cta-band (its button opens the SuggestCafeWidget form via #suggest-cafe), and the modal leads with the shared .cnx-place-meta at-a-glance line (neighborhood · rating · hours · address → Google Maps · website). The .cafes-grid tiles above + the full .cafe-modal-* suite stay valid. Walk it: /cafes.

    ✅ Done

    • .cafes-grid (+ item/img/meta/tag/name/hood)
    • .filter-menu/.filter-item · .cafes-filter-clear · full .cafe-modal-* suite

    ⏳ Pending / deferred

    • Cafe modal walked live (gallery / ratings / AQ vote)

    ⚠ Debt

    • PDF atoms NOT in app.css: .place-grid, .place-card

    §21. Directory system — consistency pass

    🟠 Awaiting review — shipped 2026-06-26; cross-directory design + content pass 2026-06-29
    .directory-filter-bar — the unified filter row (cafes · kids · coworking · neighborhoods)
    Sort
    .rank-meter — 0–5 score bars, fill = --color-highlight (jade); shared by the card AND its detail page
    Walkability5.0
    Cafés & culture3.5
    Green1.5
    Directory cross-link card — wide .card with the hand-drawn watercolor icons (live directories only)
    Around town

    Explore the directories

    .cta-band — teal capture / prompt band (quiz · map-pack · suggest-a-cafe)

    Not sure which area fits you?

    Answer a few quick questions and we'll match you to your top three.

    Take the neighborhood quiz →
    Reenie Beanie hero eyebrow — font-decorative + text-accent, over the dark hero
    Where to live in Chiang Mai

    One design kit across cafes/coworking/kids/neighborhoods: a shared modal shell + full-width filter band, the map-aside layout, jade rank-meters, Reenie Beanie hero eyebrows, .postcard cross-links and teal .cta-bands — all from sealed recipes. The neighborhood directory is the geographic spine; the place directories are hood-keyed layers that cross-link back to it. The full design-language + relationship map is now docs/directory-architecture-synthesis.md (read that first). The CMS strip is done — slug-keyed content layer (lib/content.ts), neighborhood assigned at import. Review in situ: /cafes · /coworking · /kids · /neighborhoods (+ any hood detail).

    Neighborhood map ↔ card model (2026-06-27): the map and the card list mirror each other and navigation is deliberate. Hover a polygon → one styled label; hover a card → its polygon highlights. Click a polygon → highlight both (GOLD: polygon fill + card ring-accent) and scroll the card into view. Click a card → highlight both and gently pan the polygon into focus (panTo, no zoom). Neither click navigates — "View more →" is the only navigation (the ListingCard body is a role="button"; the CTA is the lone link). Events: cnx:hood-hover / cnx:hood-select / cnx:hood-focus. See docs/lily-design-system.md.

    ✅ New sealed recipes

    • .directory-filter-bar (+.dfb-sort/-select/-filters/-clear) — white full-width band: Sort dropdown left, filters stretched equally, discreet ✕ clear
    • .rank-meter — score bars, fill = --color-highlight (brand jade, was gold — friendlier/more intuitive for a rating); ONE style shared by the neighborhood card AND its detail page; 8 dimensions from one overlay
    • Reenie Beanie hero eyebrows (font-decorative + text-accent): neighborhood index, every hood detail (the tagline IS the eyebrow), cafes ("Reviewed by Kris")
    • .postcard cross-link cards (FontAwesome stamps) at the foot of /neighborhoods → live directories; .cta-band for map-pack / quiz / suggest-a-cafe (the band button drives the island by #suggest-cafe hash)
    • .quick-facts--compact, .qf-actions, .crowdsource-band (jade air-vote), .body-prose--flat (default prose, no standfirst)
    • Modal facts reuse sealed .cnx-place-meta / .space-card / .recommend / .quick-facts

    ✅ Layout + modals

    • All 4: sticky full-height map; below-grid editorial squished into the content column; owner CTA full-width of that column
    • Hood DETAIL pages now use the map-aside layout too: that hood's polygon zoomed in the rail, editorial + jade ranks card + places-in-hood down a legible left column, breadcrumb under the hero
    • Modal at-a-glance line: neighborhood (→ guide) · rating · hours · address (→ Google Maps) · website/IG/call inline icon links — no buttons
    • Kind facts: coworking amenities+rates · cafes ratings+air vote+beans · kids "Good to know" insider card · map+neighborhood link
    • Neighborhood cards: Doi-Suthep hero, flush full-height photo, name on top, region chip, Reenie tagline, 8 ranking meters

    ⚠ Data status (on the content layer)

    • Photos serve same-origin from R2 (/media + /cdn-cgi/image); hand-added hood/owner photos use /img/... paths — assetImage now passes own-origin paths straight through (they don't go via the media-map). DEV-only proxy in src/middleware.ts for the R2/transform paths.
    • Scraped facts in web/src/data/directory-overrides.json (git); regen via scripts/scrape-directory-data.mjs
    • neighborhood-rankings.json = real curated 8-dim editorial scores (was a placeholder scaffold): expat / family / walkability / safety / affordable / cafes&culture / green / quiet — frank lows, text-aligned, safety generally-high with red-light/touristy dings; rationale in docs/neighborhood-ratings-rationale.md. Boundaries are editorial (hand-drawn), not administrative — the page says so.
    • Only Lily seals — stays 🟠 until reviewed