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






✅ 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⚠ --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.
League Gothic — display heading
Source Serif 4 — body copy, 1rem base, 1.75 line-height
Reenie Beanie — decorative accent
--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
sm 640px md 768px lg 1024px xl 1280px 2xl 1536px --radius-minimal (2px) is BANISHED: Bootstrap-esque uniform rounding fights the scrapbook/paper language. ✅ 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 (0var()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 reviewHeading level 1
Heading level 2
Heading level 3
Heading level 4
Heading level 5
Heading level 6
Paragraph + prose linkThis 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 elementsThis shows strong, emphasis, inline code, highlighted via mark, HTML, and small text — plus a key like Esc or Ctrl + K.
- Unordered item — teal bullet via
::marker - Second item with a prose link for good measure
- Nested:
- Child item one
- Child item two
- Ordered item one
- Ordered item two
- Ordered item three
Table"This is a blockquote rendered from bare markup. The cite that follows uses the heading face."
— Some attribution
| Cafe | Neighborhood | Wi-Fi |
|---|---|---|
| Roast8ry | Nimman | Yes |
| Akha Ama | Old City | Yes |
| Graph | Nimman | Yes |
- 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.
$ pnpm -F @cnxlocal/web build
280 pages
$ pnpm verify Figure + caption
<figure> with <figcaption> — italic, ink-mid, centred.What's inside this collapse?
Bordered surface with a heading-face summary and a teal disclosure marker. Opens to reveal whatever prose sits inside.
Image (just the base img rule)
✅ Done
body·h1–h6·p·a·p a·imgblockquote·table·hr·ul/olcode·pre·kbd·mark·small·abbrdl/dt/dd·figure·details·strong/em
⏳ Pending / deferred
- Form controls → §3
- Markdown block wrappers → §3
§3. Form controls
🟠 Awaiting reviewPlease enter a valid email address.
Looks good — we'll send confirmation here.
✅ Done
@tailwindcss/formsbase 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.is-loading (wrap content in a child so the spinner has something to hide) .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 ✅ Done
gold·teal·outline·ghostvariants- Sizes
sm·default·:disabled .is-loadingspinner · icon support · renders the same as<button>/<a>.text-link— button-shaped CTA link.btn--dark·.btn--lg·:focus-visiblegold 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-darkhover fills aligned to staging — Lily to confirm.
§5. Tags, badges, chips
🟠 Awaiting review✅ Done
.tag— standalone, teal border.card-tags+.card-tag— in-card filled ink chip;.card-tag--nomads/--expats/--visitorsadd 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 reviewFrom 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
This is a callout — used sparingly for important asides that shouldn't be confused with body copy. Jade tint, jade left-border.
— 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."
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-decorativeutility ·.kickersmall 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✅ Done
.alert·.alert-info·.alert-success·.alert-warn.skeleton+@keyframes shimmer.alert-error·prefers-reduced-motionskeleton guard — Astro additions
⏳ Pending / deferred
.alert-errordestructive 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
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-img-block.c-jade
.card-img-block.c-gold
🏷️ Benefit cards
Cafe-hop friendly
Walkable density, fast Wi-Fi, espresso under 80 baht.
Lower cost of living
Western comfort at 30–40% the cost.
Mountains on the doorstep
Doi Suthep is 20 minutes from the Old City moat.
Two-wheel commute
Scooter rental is 2,500฿/month; most things are inside a 4 km circle.
Cafes you'll actually work from
Hand-picked for WiFi, outlets, and espresso — not just photogenic.
Nature on the doorstep
Doi Suthep, the moat parks, Huay Tung Tao — all in arm's reach.
Schools that don't price you out
Three tiers across the city. We'll walk you through them.
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
.cardfamily (.card-lift,.card-img-block+.c-*, body/title/excerpt/meta/tags,.card-cta).polaroid(+-caption/-stamp) ·.postcard(+-stamp).benefit-cardfamily (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-title1.6rem vs staging 1.75rem;.card-meta0.7 vs 0.72rem — confirm.
§10. Site chrome
🟠 Awaiting review<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. <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-linkdiverge from staging for a11y/AA — KEPT;.pillar-cardlayout 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).
About CNXlocal
Contact and FAQs
Get in touch with our team for support, questions, or partnership opportunities.
Move with Confidence
Walks you through everything you need to plan a smooth, successful move.
.hero--ink (archive / search header). Shown at natural padding height. Ink band hero
📣 Full-bleed & band CTAs
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 ConsultationJoin the Chiang Mai Families group
5k+ expat families swapping notes on schools, housing, doctors, the works.
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. 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

Getting Around Chiang Mai: A Family Transportation Guide
Before kids, a motorbike was all we needed. These days we drive a Honda CRV and live in the suburbs. Your setup depends on where you land — but the details matter with kids in tow.



✅ 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-eyebrowflat, 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 reviewPage-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.
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 +.sectionrhythm +.section-innermeasure (--reading760 default · 1100 ·--wide1200) + optional<h1>+.body-proseslot. Mirrors stagingpage.php; used by privacy + terms..sectionprimitive +.section + .sectionadjacency · all--container-*tokens (byte-aligned to staging) · Tailwind v4 stock breakpoints.- Distinct templates sit on
BaseLayoutdirectly: 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-420is an Astro-only step.
§13. Landing page template
🟠 Awaiting review📌 Landing atoms — ported from staging §13, for review
.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.
Book a One-on-One Consultation
Navigate your family's move to Chiang Mai with confidence.
.card.border-t-4.border-accent + price + checklist + full-width gold button. Pins into the landing hero's right column on real pages. - 60-minute video call
- Tailored visa & housing advice
- Free Map Pack included
.card.tape-corner + gold icon bubble + copy + gold button Cut to the chase
This isn't just info dumping — we listen, advise, and answer questions blogs can't.
x-collapse panel. First row shown open. 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 🗺️
How to use the maps
A colour break with its own surface — instructional content that earns a stop in the scroll.
- Drop your email — the pack arrives within a minute.
- Open any map and tap "Follow" to save it to Google Maps.
- Pan around the neighborhoods you're considering.
- Compare amenity clusters before you commit.
bg-accent + centered white .card ticket + big h2 (teal eyebrow + teal button: the gold-surface standard) One hour. $100. Let's do this.
Your Thai adventure is one short call away. Money-back guarantee on every consultation.
Book Your Hour.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 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 Alpinex-collapsedisclosure (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.
Getting Around Chiang Mai: A Family Transportation Guide
.post-banner — any photo as a uniform wide band (~3.34:1, object-cover). Add the class to a body image.
.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 .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--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. .recommend — recommendation / affiliate aside (white card · gold icon badge · handwritten teal eyebrow · italic body · teal link). Recipe: swap icon + copy. 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. - Hot Desks
- Meeting Rooms
- Private Offices
- Co-Working
- Cafe
✅ 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__phototiles)
⏳ Pending / deferred
- §14 app.css alignments (awaiting review): body reading scale (16px/1.75 site-standard + scoped 1.15rem standfirst) ·
scroll-margin-top:6remon.body-proseheadings ·.hero--article45vh→60vh + lighter overlay ·.hero-title-cardh1 clamp + tokenized max-width - Reading-column 640-vs-760 decision is OVR-001-gated (see §2 note)
⚠ Debt / flagged divergences (KEPT)
pages/[slug]/index.astroL391/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--article + .hero-title-card narrowed into the left reading column (≈60%), beside the map panel (≈40%, placeholder)
.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--h2…h6 (defaults h3). Ported staging → app.css.
Nong Buak Haad Park
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
Urban convenience with a hip, modern vibe
👀 Walk these specimens
- Point-marker maps: Best Parks · Kid-Friendly Guide
- Area-polygon maps: Expat Families
✅ Done
.article-map-aside-page/-main/-body/-railgrid ·.cm-marker(+-hovered/-dim) marker states ·.map-aside-shellfamily- Map now renders + functions (Astro Leaflet): Mapbox
streets-v12tiles · jade wash on a pane UNDER polygons/markers (non-interactive) ·location-icon.pngmarkers (enlarge on select) · teal 0.2 → gold 0.4 polygons ·.cm-map-labelhover chip =.card-tagon 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-placecard 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-cardcallout (CNX_ARTICLE_REFmarker, §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
nameis 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.pngmarker 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:-1fix) — 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 reviewPolish 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.
<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.

30 Free (and Nearly Free) Things to Do in Chiang Mai with Kids
Thirty real ways to fill a day in Chiang Mai with kids without spending much — honest about which are truly free and which run a few baht, with practical notes and no padding.

A Comprehensive Guide to the Destination Thailand Visa (DTV) for Digital Nomads
Discover the new Destination Thailand Visa (DTV) for digital nomads and remote workers. Learn about this 5-year multiple-entry visa, its eligibility criteria, categories, and application process. Perfect for long-term stays in Thailand.

International Schools in Chiang Mai: A Guide for Expat Families
Close to 20 international schools, one genuinely hard decision. A Chiang Mai local's lay-of-the-land on curricula, fees, locations, and how to choose the school that actually fits your family.
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).
Walk in situ: /articles · /category/nomads · /author/lily-szabo · /neighborhoods · /search.
✅ Done
- Tracker §16 renders the REAL
<ArchiveListing>(livearticlesInCategory(..,'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/12auto-aspect + bodymd: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/--visitorsbrand-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-btna11y additions ·.articles-gridresponsive 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 localarchive.phpcentered-header draft is stale/unshipped — do not chase it.) - Search is Kris's Pagefind setup (
Search.tsx+ its own.hero--inkband) — left as-is. Astro-ahead of staging's WPsearch.php; do not rebuild.
§17. Articles listing
🟠 Awaiting reviewThe "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 reviewThe 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 MapPackForm → Tools & Applets scroll-row (.card.card-lift + hand-drawn arrows) → sign-off. Walk it in situ: homepage. There is no .benefits-grid on this page.
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. -
Moving with the family
Schools, neighborhoods, and the everyday logistics.
-
Working remote
Visas, cost of living, and cafes that get it.
.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. .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+LiveDataStripisland ·.pillar-carddispatch tiles ·.articles-grid· two photo-bands (one embeds a.polaroid) · watercolor map +.postcardrow ·.cta-band+ inlineMapPackForm· Tools & Applets.card-liftscroll-row.
⏳ Pending / deferred
- Whole homepage awaiting design + seal
⚠ Debt
- Spec atoms not yet in app.css:
.cta-newsletter,.cta-photo--profile,.section-header.
§19. Specialty tool pages
🔵 Deferred — beta good-enoughTwo 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 theCostOfLivingisland; 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,
/toolslanding. 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-enoughThe 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--color-highlight (jade); shared by the card AND its detail page Not sure which area fits you?
Answer a few quick questions and we'll match you to your top three.
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") .postcardcross-link cards (FontAwesome stamps) at the foot of /neighborhoods → live directories;.cta-bandfor map-pack / quiz / suggest-a-cafe (the band button drives the island by#suggest-cafehash).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 —assetImagenow passes own-origin paths straight through (they don't go via the media-map). DEV-only proxy insrc/middleware.tsfor the R2/transform paths. - Scraped facts in
web/src/data/directory-overrides.json(git); regen viascripts/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 indocs/neighborhood-ratings-rationale.md. Boundaries are editorial (hand-drawn), not administrative — the page says so.- Only Lily seals — stays 🟠 until reviewed