# BRIEF — LiNK Real Estate Marketplace, public surface prototype

    brand:        LiNK Real Estate Marketplace
    domain:       linkrem.com  (marketplace takes the root; investor site moves to /investors)
    scope:        public / anonymous surface only — no authenticated app screens
    conversion:   start a property search
    handoff:      Drupal SDC via drupal-ddev-cpanel
    mode:         light only (no dark toggle — comps are light, client has seen them)
    fidelity:     follow the approved comps closely; flag improvements, don't silently apply
    date:         2026-08-05

Context skill: `link-context` — brand tokens, 16-component contract, page inventory,
client facts, resolved design contradictions. Project rules: `LiNK-Fable/CLAUDE.md`.

## Stream 1 — User preferences

**Brand.** LiNK Real Estate Marketplace. Consumer-to-consumer real estate — buyers and
sellers transact directly, no realtors, no commissions. Vetted service providers (closing
agents, attorneys, lenders, inspectors, contractors, photographers) supply every
professional service inside the platform.

**Audience.** Three, all on the public surface: buyers who want to browse without an
agent; sellers weighing whether they can do this themselves; service providers looking
for work. Launch market Richmond VA, then Virginia, then state by state.

**Tone.** Plain-spoken, reassuring, concrete. The visitor's real fear is *"can I actually
do this without an agent, or will I screw up the biggest transaction of my life?"* — copy
answers that, it doesn't celebrate the platform. Not luxury, not startup-clever.

**Colors.** Sampled from the live build at `garza.link` (2026-08-05), which supersedes the
2024 style-guide swatches where they differ. Monochromatic blue.

| Token | Hex |
|---|---|
| `--color-navy-darkest` | `#002457` |
| `--color-navy` | `#05446B` |
| `--color-navy-footer` | `#07466F` |
| `--color-navy-mid` | `#1F6088` |
| `--color-blue` | `#358DC7` |

**Fonts.** **Poppins** (300–700), Oswald for accents. Confirmed canonical 2026-08-05 —
supersedes the Roboto + Libre Franklin in the 2024 style guide. See
`designs/STYLE-GUIDE-ADDENDUM.md`.

**Reference designs.** The client's own approved comps, not third-party sites:
`link-frontpage-v2e.jpg` (homepage) and `link-buyer_single-listing-v3a.jpg` (listing detail).

## Stream 2 — Website preferences

**Pages (11).** Public/anonymous only. Authenticated app screens — dashboards, bid flow,
seller listing form, messages, compare — are explicitly out of scope this round.

| # | File | Page |
|---|---|---|
| 1 | `index.html` | Home |
| 2 | `buy.html` | Buy a Home |
| 3 | `sell.html` | Sell My Home |
| 4 | `search.html` | Property search results |
| 5 | `listing.html` | Single listing detail (sample) |
| 6 | `services.html` | Find Services — provider search |
| 7 | `provider.html` | Provider profile (sample) |
| 8 | `how-it-works.html` | How it works / education — **the SEO workhorse** |
| 9 | `about.html` | About / Our Mission |
| 10 | `faq.html` | FAQ |
| 11 | `contact.html` | Contact LiNK |

**Conversion goal.** Start a property search. Every primary CTA leads there; registration
is the secondary ask, gated at transact-time per the Login spec ("All users must be able
to browse the site without a login").

**Components.** Every section maps to a tab 13-SDC component — names and props in the
`link-context` skill. Public surface exercises: `listing-card`, `facet-sidebar`,
`map-panel`, `photo-gallery`, `provider-card`, `rating-stars`, `schools-nearby-list`,
`status-badge`, `empty-state`, `bid-panel` (read-only), `doc-list`.

**Assets.** Logo: confirm current version — style guide shows the chain+LiNK mark, comps
show a boxed `LiNK / REAL ESTATE MARKETPLACE` lockup, and `logo-v6-temp-20250620.png`
(2025-06-20) is newest on disk. Photography: Adobe Stock comps are watermarked — every
photo is an image-slot placeholder in the prototype.

## Stream 3 — Existing content

**linkrem.com is live** — Drupal 11, last modified 2026-07-24. It is an **investor
information site**, not the marketplace: Executive Summary, Market Research, Revenue
Forecasts, Capital Raise Rounds. Different audience entirely. 9 URLs in its sitemap.

Real copy salvaged from it (verbatim, reusable):
- *"A New Era - Consumer to Consumer Real Estate Transactions Without Realtor Commissions or Fees!"*
- *"Why Is the Real Estate Industry Ripe for Change?"*
- *"LiNK's Solution to High Commissions"*

Client functional specs — authoritative for behavior, in
`LiNK-Fable/discovery/Christine-*-material/`: Compare Properties, Bid, FAQ, Contact,
Login, Profile, Service Provider Onboarding / Offering / Rating, Seller Listing
Collection, Admin.

### Bugs found on the live site (fix independently of this project)

1. `<title>` renders as `| LiNK Investor Information` — empty site-name token leaves a
   leading pipe.
2. `/node/6`, `/node/7`, `/node/8`, `/node/20` have **no path aliases** — raw node IDs are
   the public URLs.
3. No meta description on the homepage.

### Missing client specs

- **Buyer Search** — schedule sheet says provided 2026-03-09; not on disk, and no comp
  exists either. The one genuine hole in the public surface. Built from tab 13-SDC props,
  the database spec, and the Compare page doc; every assumption flagged for Christian.
- **Property Viewing** — schedule says provided 2026-03-25, not on disk, but the v3a
  listing comp covers it.

## Round 13 — the log-in page had no page of its own (2026-08-12)

### What was wrong

`login.html` carried sign-in **and** registration as two tabs of one card, and the site
header pointed **both** "Log in" and "Sign Up" at it. So "Sign Up" opened the sign-in tab,
and the sign-in form itself sat 942px down a 6,989px specification document, under three
SSO buttons. Asked for a log-in page, the prototype offered a spec essay with a sign-up
form inside it.

### Two pages now

| Page | Carries |
|---|---|
| `login.html` | Log in, and every state that belongs to logging in: MFA, the five error states, account recovery, remembered devices and login history, session security, accessibility, mobile. |
| `register.html` | **New.** Create an account: validated logins, the form, the password meter, the security questions, the terms, then what happens next. |

The shared pieces (hero, auth panel, support rail, the role-routing section) are one
definition used by both, so neither can drift from the other's wording.

- **The credential form comes first** on `login.html`, with the four validated logins under
  an "or continue with" rule. Three SSO buttons above the email field is what made the page
  read as a wall rather than a log-in form.
- **Registration gained the same four validated logins.** An account creatable with Google
  has to be creatable with Google.
- **Header:** "Log in" → `login.html`, "Sign Up" → `register.html`, on desktop and in the
  mobile drawer, which previously had no sign-up entry at all.
- The tab machinery is gone from the markup. `account.js` `authTabs()` early-returns when
  no tabs are present, so it needed no change; the password toggle and strength meter run
  on both pages unchanged.

### Verified

Site-wide internal link check: **0 broken page or anchor links** across 57 pages (this
caught `login.html#mfa` from the new page — the MFA section had an `aria-labelledby` but
no `id`). No duplicate ids, no dangling `label[for]`, no console errors on either page.
Password meter and show/hide toggle exercised on `register.html`; `verify-fields.py` still
PASS at 225/225.

### Indexing: register is in, login stays out

Confirmed with Frederico and applied. `register.html` is indexable and in the sitemap (30
URLs, up from 29); `login.html` and `profile.html` keep `noindex, nofollow` and keep their
robots.txt Disallow entries. A sign-up page is a public destination people search for by
name; a sign-in form is not.

The robots meta was hardcoded in this generator's `SHELL`, which is why every page it wrote
was noindex whether that made sense or not. It is now a per-page value (`NOINDEX` /
`INDEXED`) chosen in the `PAGES` dict beside the slug. `make-sitemap.py` needed no change —
it already derives the sitemap from each page's own robots meta and canonical, so the two
signals cannot disagree.

`register.html` carries **BreadcrumbList only**. No Organization block: the NAP is still
unverified (SEO-PLAN line 4), and asserting an organization on a sign-up form is not worth
the risk. Its entry is now in the SEO plan's page→query map and per-page detail.

## Round 12 — conformed to build manual v6 (2026-08-12)

The client issued `link_build_manual_6_20260812.xlsx`. This round makes the prototype
provably match it, and makes it impossible to drift again silently.

### The problem: nothing connected the two artefacts

An audit of the 225 fields the manual defines against the built HTML found **8
referenced** — and even those only by coincidence of wording. The seller's listing screen
exposed 15 of the manual's 108 property fields, the manual's new
`field_prop_market_status` had never arrived, and no page showed the nine-step listing
wizard that manual tab 1-Modules specifies as a custom module.

### Three new files close it

| File | What it does |
|---|---|
| `make-field-map.py` | Parses the manual `.xlsx` (stdlib only — no openpyxl on this machine) into a literal catalogue. Asserts the rules it must not lose while parsing prose. |
| `link_fields.py` | **Generated.** Every field, value list, vocabulary, flag, other-type and SDC row in the manual. Page generators render from this. |
| `verify-fields.py` | Fails if any manual field reaches no page, if the HTML invents a field the manual does not define, or if a v6 invariant is broken. |

A field counts as referenced only when a page carries its machine name in `data-field`
(display) or `name` (form control). Prose does not count — "Price" appears on nine pages
and proves nothing about `field_prop_price`.

    python3 make-field-map.py     # after any new manual revision
    python3 verify-fields.py      # PASS / FAIL, exit status 1 on failure

**Result: 225/225 = 100%.**

### The seller listing wizard, which did not exist

`dashboard-seller-listing-wizard.html` — nine steps, all 108 property fields, rendered
from `link_fields.py` rather than hand-authored, so a manual revision moves it. Widgets
follow the manual's Field Type column: term references become checkbox groups off the real
vocabulary, `List (text)` fields carry the manual's own allowed values, paragraph
references become add-another repeaters of the bundle's own fields, Smart Date becomes a
weekly availability grid.

Resumable is the point (manual 1-Modules row 36): a resume banner, per-step state,
per-step validation, and a save-and-continue footer on every step. `field_group` alone
cannot express that, which is why v6 demoted it.

Steps 1–3 arrive pre-filled from `listing_data.py` and 4–9 empty — a draft mid-completion,
which is what a resumable wizard looks like on return.

### Market status is not moderation state

The single largest correctness change in v6, and the prototype had them conflated in one
"status". They are now separate everywhere: `field_prop_market_status` (coming soon → for
sale → under contract → sold → off market, Search-API-indexed, seller-controlled, five
filter pills on My Listings) and the editorial moderation state (draft → needs review →
published, a one-time LiNK decision, shown as a second badge). `verify-fields.py` asserts
both columns survive. The Overview's "listings for sale" count now reads market status,
so a listing awaiting review no longer counts as live.

### Fields that reached a page for the first time

- **Bid** — status (all nine tokens, one documented colour map), seller-viewed stamp with
  unviewed rows tinted, and a counter-offer panel writing `counter_price` / `counter_terms`
  as a revision of the buyer's offer rather than a second bid. Appliances-to-convey now
  offers the 11-term Appliances vocabulary instead of washer / dryer / refrigerator.
- **Buyer profile** — budget minimum and maximum as two figures (the single "price band"
  select could not express `$310k–$455k` and had no field to store into; the band survives
  as a preset that fills them), preferred property types from the vocabulary, preferred
  locations, both alert opt-ins.
- **Seller profile** — "show contact info to buyers", which the Bid page reads before it
  shows or withholds a seller's name, plus the two channels a seller may accept.
- **Provider** — account standing (verified / subscription / advertising tier: staff- and
  Stripe-written, read-only to the provider), licence number, and the packages and
  credentials blocks rebuilt as the paragraph bundles the manual models.
- **Concierge tasks** — the `concierge_task` bundle as a staff entry panel on the admin
  screen. Time spent and internal notes stay staff-side (Rule 9).

### Listing detail and compare, expanded to the manual's contract

`listing_data.systems()` derives the full disclosed record — size and layout, systems and
condition, plumbing/electrical/structure, what it costs to run, location — deterministically
from the primary fields, the same way lot size and roof age already were. Detail pages went
from 9 facts to 32; the compare table from 15 rows to 43, which is what 13-SDC means by
"ALL systems detail". Both read one derivation, so they cannot disagree.

### Also

- The styleguide's `step-tabs` demo shows nine steps, not five — a component that must
  survive nine labels should be reviewed at nine labels.
- `make-bid-page.py`, `make-provider-pages.py` and `make-account-pages.py` helpers gained
  an optional `field=` parameter rather than having attributes pasted at each call site.

### Verified

`verify-fields.py` PASS. Browser-checked: My Listings filter pills still filter against the
renamed market statuses; the offers table's two new columns scroll inside their own
container with no page-level overflow; the wizard renders 252 controls with no duplicate
ids, no dangling `label[for]`, no dangling `aria-labelledby`, and no console errors;
paragraph repeaters render every field of their bundle.

**Not verified here:** this preview pane returns blank captures below the fold on tall
pages (it does the same on the pre-existing styleguide), so the wizard was checked through
the DOM rather than by eye past step 1. **Worth one scroll-through in Chrome.**

## Round 11 — dead code removed, header logo shrinks on scroll (2026-08-05)

### Duplicate showings definition deleted

`make-dashboards.py` held two `PAGES["dashboard-seller-showings.html"]` blocks; round 5's
v2a silently superseded the v1 one, which had already cost one wasted edit. The v1 block
(EVENTS, `cal_cells()`, CAL_ROWS and the page definition — 117 lines) is gone. `GRID` was
kept, because the live v2a calendar iterates it.

**Proven dead:** the page was regenerated after deletion and diffed against the previous
output — byte-identical.

### Header logo shrinks once the page scrolls

At rest the 215×121 logo overhangs the 53px bar, matching garza.link. Once scrolled it
shrinks to 78×43.7 and centres, so it is fully contained by the header and never covers
the content being scrolled past.

Driven by an **IntersectionObserver** on a sentinel above the sticky header rather than a
scroll listener: it fires only on the two transitions that matter, needs no rAF throttling,
and does no work during the rest of the scroll.

**Three attempts at the width, and why the first two failed** — worth knowing before
touching this again:

1. An unlayered `.site-header.is-scrolled [data-site-logo] { width: 78px }` **lost the
   cascade** to the `.w-[215px]` utility on the element.
2. Routing it through a custom property (`w-[var(--logo-w)]`) failed differently: the
   Tailwind **browser build never generated that utility**, so no width rule matched at all.
3. What works: the img carries **no width utility whatsoever**, and `extras.css` owns the
   width outright — base, both breakpoints, and the scrolled state. Nothing to compete with.

### Asset cache-busting

`css/extras.css` and `js/main.js` are now versioned (`?v=5`) in every page and in all three
generators. Stale CSS/JS masked real changes repeatedly during this build; bump the number
when either file changes.

### ⚠ Not verifiable in the preview pane

The scroll *trigger* could not be exercised here: this pane stopped dispatching scroll
events and running IntersectionObserver callbacks, and CSS transitions never advanced
(`window.scrollY` reached 700 with **zero** scroll events fired). What was verified is the
state change itself — forcing `is-scrolled` with the transition disabled gives 215px → 78px,
height 43.7px, `fullyInside: true`. **Confirm the scroll behaviour in Chrome.**

## Round 10 — every filter live (2026-08-05)

All six filter UIs in the prototype now work. Five are driven by one generic engine,
`linkListFilter()`; `search.html` keeps `linkFacetedSearch()` because it needs
OR-within-group, exact-vs-threshold matching and live facet counts that the generic one
deliberately does not do.

| Page | Controls | Verified |
|---|---|---|
| `search.html` | 5 facet groups, sort, **new** free-text search | text "church hill" → 3; + beds=3 → 1; facet counts follow the text query; clear paths correct; sort orders hold |
| `services.html` | category select, **new** text search, clear | 6 → category 1 → text 1 → no-match 0 with empty state → reset 6 |
| `dashboard-seller-listings.html` | status pills | All 8 · Active 7 · Pending 1 · Sold 0 · Draft 0, empty state on the zeros |
| `dashboard-seller-showings.html` | saved-property checkboxes → calendar; property search → rail | 6 events → 608 W. 20th only 3; rail search "main" → 1; the two lists filter independently |
| `dashboard-buyer.html` | price band, bedrooms, neighborhood → Saved Homes | 3 → Church Hill 1 → beds 3 → 2 → +price 1 → no-match 0 |
| `showing-request.html` | date picker → availability | 33 rows total, 11 per day, each day its own availability pattern |

### Engine design

Everything is data attributes, so it converts to one `Drupal.behaviors` entry:
`[data-filter]` scope · `[data-filter-item]` rows carrying `data-<key>` · `[data-filter-key]`
controls (select, text, checkbox, or aria-pressed button group) · `[data-filter-count]` ·
`[data-filter-empty]` · `[data-filter-reset]` · `[data-filter-sort]`. **OR within a key,
AND across keys.** An empty control value means "no constraint", so "All" options are just
`value=""`.

Two capabilities were added while wiring the showings page: checkbox groups that OR
together, and `data-filter-scope="name"` so a control can drive a scope it does not sit
inside — the calendar's property checkboxes live in the sidebar next to it.

### Three real bugs this surfaced

1. **Seller status vocabulary didn't match its own filter.** The pills read
   Draft/Active/Pending/Sold while the cards carried marketplace wording — For Sale /
   Under Contract / Sold — so *every pill returned zero*. Both now derive from the
   canonical `state` in `listing_data.py`: seller pages show moderation vocabulary, public
   pages show marketplace vocabulary, and the two can't drift.
2. **A scope was stealing another scope's controls.** The calendar checkboxes sit inside
   the saved-properties aside, so that aside was filtering its own list by a key its items
   don't have — hiding all four. Controls that name a scope now belong only to it.
3. **I patched dead code.** `make-dashboards.py` holds two definitions of
   `dashboard-seller-showings.html`; round 5's v2a supersedes the first. The first edit
   went to the superseded copy and changed nothing on the page. ⚠ That duplicate is still
   in the file — worth deleting before it catches someone again.

## Round 9 — aligned to the LIVE build at garza.link (2026-08-05)

`https://garza.link/` is a **live Drupal 11 build of LiNK**, in maintenance mode (503 to
anonymous visitors; authenticated users bypass). It supersedes the 2024 comps for anything
the two disagree on, and the prototype homepage is now aligned to it.

### Changed to match

| | Was | Now |
|---|---|---|
| Typeface | Roboto + Libre Franklin | **Poppins** (live loads Poppins, Oswald, "Ginter") |
| Heading / button navy | `#06446C` | `#05446B` |
| Footer ground | `#06446C` | `#07466F` (own token, `--color-navy-footer`) |
| Page ground | `#F4F7F9` | `#F6F8FC` |
| Hero | eyebrow line + statement | statement only, 58px/600, no eyebrow |
| Hero tabs | underlined active | wide-spaced, weight-only active state |
| Search placeholder | "Search by city, ZIP, or address" | "Search..." |
| Promo band | bordered box, small heading, text "LiNK" | full-width, 46px heading, real logo, large pill "Learn More", **no exclamation mark** |
| Header logo | 56px inline | 118px, overhanging the bar into the hero |
| Header nav order | Buy · Sell · Client Services | **Buy · Client Services · Sell** |
| Sign-up button | pill, "Sign up" | square, "Sign Up" |
| 4-up strip | title case, no dividers | uppercase labels, vertical dividers |
| Band buttons | navy uppercase pills | dark square buttons, title case |
| Footer | 6 About links, 6 social icons | live link sets, ruled headings, **no social icons** |
| Footer bottom bar | © line only | © line + "Designed & built by Garza Web Design" |
| Homepage FAQ | present | **removed for parity** — `faq.html` still carries all 7 questions and the FAQPage schema, so nothing is lost site-wide |

### Measured parity, not eyeballed

The first pass at this was compared by eye — a 1483px live site against an 800px preview
pane, which is why several things looked right and weren't. The homepage was then matched
by **measuring the same properties on both at 1512px** and diffing until the numbers
converged. Final state:

| Metric | Live | Prototype | Δ |
|---|---|---|---|
| Hero block bottom | 756 | 756 | 0 |
| Headline top / left / width | 425 / 76 / 756 | 425 / 76 / 756 | 0 / 0 / 0 |
| First tab left | 241 | 243 | 2 |
| Search field left / width | 281 / 345 | 282 / 345 | 1 / 0 |
| Image gate height | 557 | 557 | 0 |
| Logo width | 215 | 215 | 0 |
| Header height | 51 | 53 | 2 |
| Footer height | 481 | 472 | −9 |

**Max deviation 9px.** Key structural findings that produced it:

- The live hero is a **`hero_full_screen` block**: full-bleed, *no* max-width container,
  text column exactly 50% of viewport starting 76px from the edge. The prototype had it
  inside the 1200px centred container, which put the headline at left 188 instead of 76.
- The live hero content is **bottom-aligned**, ending 12px above the hero's lower edge —
  not vertically centred as its `align-items-center` class suggests.
- Hero block internals are 319px: headline 146 · gap 30 · tab row 60 · gap 29 · search 54.
- **Live's header is `position:absolute` and overlays the hero**, so its hero starts at
  y=0. Ours is in normal flow (sticky — better UX, and making it absolute would break the
  other 38 pages), so the hero is shortened by the header height instead. Header + hero
  together now occupy the same 756px, putting every element at the same absolute position.
- Hero tabs are 22.68px **weight 400**, not bold, and the tab row and search field are
  **centred within the text column** while the headline is left-aligned.

### Header logo — the bar must not grow to fit it

Live: the header bar is **51px** tall and the logo is **215×121**, so it *overhangs* into
the hero. The prototype originally let the logo set the header height, which grew the white
bar to ~130px and made the logo read small by comparison. The logo is now
`position:absolute`, centred, outside the flex flow — the bar keeps its own height and the
logo hangs over the hero. A fixed-width spacer keeps the right-hand nav clear of it.

### The image gate, measured (second pass)

The first attempt got the *behaviour* right and the *appearance* wrong. Measured against
live and corrected:

| | Live | Was | Now |
|---|---|---|---|
| Width | 1414, inset ~48px each side | full-bleed | 1416, inset 48 |
| Dark overlay | **none** | `navy-darkest/55` | none |
| Column dividers | **none** | light borders | none |
| Card | 232px, vertically centred in 557 | stretched, top-padded | 233px, centred |
| Icon block | 79px | 46px | 79px |
| Title | 25.7px / 600 / uppercase | 26px | 25.7px |
| Description | 18.1px / 27.2 lh, max-width 245 | 15px, full width | 18.1 / 27.2 / 245 |
| Hover | image swap only | image swap **+ bg tint** | image swap only |

**Live has no scrim over the photographs — they are simply dark.** The gate images are
therefore darkened at generation (`ImageEnhance.Brightness`, 0.42–0.62 depending on the
source) rather than covered with an overlay. Max deviation now 2px on every metric.

Icons were hand-drawn and initially unreadable — and a scripted replacement put them in the
wrong cells because the regex restarted at position 0 each pass, overwriting cell 1 four
times. They are now anchored per `<li>` and asserted: house-with-$ / house-with-door /
wrench / megaphone.

### The 4-up band is an image gate, not a static strip

The live block is `complex_image_gate`, and it behaves: four columns share a **stack of
four background layers**, and hovering a column cross-fades *that column's* image in.
Column 1's image is the resting state. Confirmed by hovering column 3 on the live site and
watching its layer flip from `visibility:hidden` to visible.

Also structural, and previously wrong here: **each column is a single link** wrapping icon,
title and description. The prototype had a separate "Start selling →" / "Start buying →"
link per column, which the live build does not have. Those are gone.

Implemented in `extras.css` with `:has()` — four paired hover states express badly as
utilities, and this way a decorative effect needs no JS. `:focus-within` is included so the
gate responds to keyboard, which the live build does not do.

### ⚠ Three things the live site does that the prototype deliberately does NOT copy

1. **No `<h1>`.** The live homepage renders the statement as an `<h2>` and has no `<h1>`
   at all. The prototype keeps exactly one `<h1>` — the same statement, styled identically
   — because a missing h1 is a defect, not a design choice. Side effect: the h1 no longer
   carries the exact-match "for sale by owner" phrase, which now lives in the title, meta
   description and H2s.
2. **No SEO head whatsoever.** Live has no canonical, no meta description, no Open Graph
   tags and no JSON-LD. The prototype keeps all of them. Everything in `SEO-PLAN.md` is
   currently unimplemented on the live build.
3. **The live theme's own block vocabulary** differs from build-manual tab 13-SDC. Drupal
   block types on the live site include: Hero full screen, Hero short screen, Full-width
   CTA, Featured with Icons, Image text row, Simple/Tall image text row, Jumbotron, Card
   block, Simple/Complex image gate. Content types include Property, Offer, Service,
   Guide, Employee, plus Paragraphs for Comparison Item, Concierge Task, Contingency,
   Improvement, Open House, Price Change and Property Document. **That is the real
   information architecture** and it should be reconciled with tab 13-SDC before the
   Drupal phase — they are two different naming systems for overlapping things.

### Typeface conflict — RESOLVED 2026-08-05

**Poppins is canonical**, confirmed by the client. Roboto + Libre Franklin in the 2024
style guide is superseded. Updated to match: build-manual tab `13-SDC` (backup at
`link_build_manual_5_20260725.bak-before-poppins-20260805.xlsx`), `LiNK-Fable/CLAUDE.md`,
the `link-context` skill, and `css/tokens.css`.

⚠ **The style guide itself has not been reissued** — the PDF/AI/PSD still show Roboto and
Libre Franklin, and the typography panel needs an Illustrator edit to `.ai` plus a re-export,
which cannot be scripted. A dated errata sits beside them at
`designs/STYLE-GUIDE-ADDENDUM.md` and is authoritative until the guide is reissued.

## Round 8 — save/favorite hearts (2026-08-05)

The heart on every search card is a real toggle: click it and it fills **red**
(`#D8323F`), click again and it clears.

- state is `aria-pressed`, not a class, so screen readers announce it
- `aria-label` flips between "Save {address} to your favorites" and "Remove {address}
  from your favorites"
- a short scale-pop fires on each press (disabled under `prefers-reduced-motion` by the
  existing global rule)
- **state persists and is shared** — favourite a home on the search grid and its own
  detail page shows it saved, where the "Save / favorite" action is the same control and
  its label reads "Saved to favorites"

### A new token, deliberately not an existing one

`--color-favorite: #D8323F` was added to `css/tokens.css` rather than reusing
`--color-status-alert`. An error state and a favourited home must never read as the same
signal. The style guide is monochromatic blue, so this is an accent outside the brand
palette — confined to the heart affordance and nothing else. Worth a nod from the client,
since it is the one non-brand colour in the system besides the status map.

### For the Drupal phase

Favorites become the **Flag module**, per Rule 4 — one lightweight table, never a node per
save. `linkFavorites()` is keyed by address only because the prototype has no entity IDs;
the real implementation flags the `property_listing` node.

## Round 7 — gallery lightbox (2026-08-05)

Listing photographs are now clickable. Click the hero (or its magnifier affordance) and
the photo opens full-size over a dimmed backdrop, with previous/next, a caption, and an
"N of 4" counter.

### Built on native `<dialog>`, not a hand-rolled overlay

`showModal()` supplies focus trapping, the top layer, inertness of the page behind, and
Escape-to-close **from the platform**. Those are exactly the parts a bespoke lightbox
usually gets wrong, and a Colorbox-style jQuery plugin would be a dependency the Drupal
theme then has to carry. On top of the native behavior:

- previous/next buttons and **Left/Right arrow keys**, wrapping at both ends
- click the backdrop to dismiss
- focus returns to the element that opened the viewer
- the hero stays in sync with wherever you navigated to before closing

One `<dialog>` per listing page, emitted as real markup by `make-listings.py` rather than
injected by JS — so it maps to an SDC like every other section.

### Thumbnails now actually work

They previously only changed a text label; the hero image never changed. They now swap the
hero, set `aria-current`, and update the stage caption. The lightbox opens at whichever
image is currently showing, not always the first.

### A false caption, caught and fixed

The thumbnail rail was labelled **Floor plan / Photos / Video walkthrough** after the comp
— but the placeholder images are all photographs, so the viewer read "Floor plan" over a
photo of a sofa. Labels are now **Photograph 1–4**, with an on-page note that a real
listing also carries floor plan, video walkthrough, virtual tour and drone media types.
Those thumbnails are omitted rather than faked over stock imagery.

**Note on resolution:** the CC0 source images are 960px wide, so there is no larger
derivative to serve the lightbox — it displays the same file, capped at 78vh. Real
photography would get proper derivatives per Rule 7 (never render originals).

## Round 6 — a page per property (2026-08-05)

`listing.html` is gone. In its place, **18 real detail pages**, one per property —
`listing-1214-rothsay-cir.html` and so on. **51 pages total, verify-clean.**

**2026-08-06 expansion (12 pages + live maps).** Leaflet+OSM maps went live on search,
all 18 listing pages, and the seller listing-edit screen. Then the client-spec gap sweep
added: `login.html` + `profile.html` (Christine 7-20-26 specs), `bid.html` (Bid Page spec,
figures derive from `listing_data.py`), `provider-onboarding.html` + `provider-rating.html`
(onboarding + rating specs, DR-8 shown in both states), six `dashboard-buyer-*.html` tabs
(no spec — mirrors seller side, proposed content flagged), `styleguide.html` (all 16
tab-13-SDC components, internal), and `contact.html` rebuilt to the 7-20-26 Contact spec.
**Every page is now generated (2026-08-06).** `index.html` and `how-it-works.html`
were the last two hand-authored pages; index could not be converted at all while it was
the source `sync-shared.py` read the public header and footer from. All three chrome
blocks now live in `chrome/` partials, and sync reported `0 page(s) updated` against
each extraction — the proof they changed no output.

Their marketing prose lives in `bodies/*.body`, lifted VERBATIM: the regenerated pages
differ from the originals by one trailing blank line. The customer-hero voice work was
not rewritten. The guide's commission example now derives from a real listing, so the
worked figure cannot drift from a price the rest of the prototype quotes.

**Data-page conversion (2026-08-06).** The three data-dense hand-authored pages —
`dashboard-buyer.html`, `dashboard-seller.html`, `compare.html` — are now generated,
every figure derived from `listing_data.py`. This purged six phantom addresses that
existed in no data file (608 W 20th St, 1100/1011 E. Main St, 123 Boulevard Ave,
512 Eleanor Ave, 1612 New Yorker St) and several impossible figures: a $374,999 offer
on a non-existent home, a seller portfolio of "4 active / $6,001,123" against 7 active
worth $2,973,900, and a compare column showing a real address over invented data.

The lesson worth keeping: **three of the phantoms shipped on GENERATED pages.** A
generator that hardcodes a string is no safer than a hand-typed page. What prevents
drift is derivation — reading the value from the single source — not generation itself.

`chrome/appheader.html` was extracted so `sync-shared.py` no longer reads chrome out of
a page; that dependency was why the seller dashboard had to stay hand-typed. Only
`index.html` and `how-it-works.html` remain hand-authored, both prose-heavy with few
derived figures.

**Developer index + version control (2026-08-06).** `developer.html` lists every page
grouped by surface, generated by scanning the folder so it cannot drift; noindex and out
of the sitemap. The prototype moved to `~/dev/ai/claude/link-prototype` and is now under
git — the first commit is the full 54-page build.

**Concierge dashboards (2026-08-06).** Two surfaces closed the concierge gap, which
until now had only the public tiers page:

* `dashboard-admin-concierge.html` — LiNK **staff** view, built from Christine's Admin
  spec §Concierge Services Management. Every column a–o, all five filters, and the
  four-role refund chain (Customer Service initiates → Concierge Manager validates →
  Finance approves → System Admin executes). That chain IS the interface, because her
  control model is strict: no one approves their own work, financial actions need dual
  approval. Only the current actor's action is offered; the rest are disabled with the
  reason shown. ⚠ This is a **design reference for a Drupal back-office view, not a page
  to hand-build** — Views/VBO/RBAC already do the table, filters and permissions. What
  genuinely needs building is the dual-approval state machine and its audit writes.
  Christine's full admin spec is 24 pages across 11 sections and 10 roles; ten sections
  remain unbuilt and should be scoped separately from the public marketplace.
* `dashboard-seller-concierge.html` — the **customer** half, added as an 8th seller tab.
  **No client spec exists** for it; it mirrors the admin record showing only
  customer-visible fields. Rule 9 enforced: cost-impact-per-task, internal notes and
  unapproved-work flags are staff-only and deliberately absent (verified in-browser).

**Canvas SDC + SEO hardening (same day).** The prototype is now built to the Drupal 11.4
Canvas rules, and the `website-prototype-builder` skill *requires* them — `verify.py` fails
the build on each. What changed: 590 lines of inline JS moved into seven per-page files
(`js/bid.js`, `js/account.js`, …), mapping 1:1 onto per-component `*.libraries.yml`;
`main.js` gained a `once()` guard because Drupal re-runs behaviors on every AJAX insert;
`css/extras.css` moved into `@layer link-extras` so a global rule can no longer outrank a
component's own utilities; every `component:` marker got a real element root; `faq_block()`
stopped hardcoding `id="faq-h"` (rendering it twice was producing a duplicate id — exactly
the multi-instance failure Canvas hits). `components.json` records props with JSON Schema
types, which parameters are SLOTS rather than props, and which of the 79 markers are real
components vs page sections.

SEO closed to the same standard: `twitter:card` + `og:image` on all 51 pages, JSON-LD on
every indexable page, `alt` on every image, continuous heading levels, and `sitemap.xml` +
`robots.txt` **generated** by `make-sitemap.py` from each page's own `noindex` and
canonical — so the sitemap cannot contradict the pages, and `verify.py` fails either
direction of that mismatch.

**Component reconciliation (same day).** `step-tabs`, `rating-stars`, `message-thread`
and `alert-toast` had been authored twice — by the page generators and by the styleguide,
in parallel, neither aware of the other. They now live in **`components.py`**, imported by
every generator that emits them, so the styleguide demo *is* the page markup. Decided per
component on the merits: step-tabs took provider-onboarding's underline row (2 of 3
authorings, and state is now attribute-driven so one markup serves both the clickable and
the server-rendered wizard); rating-stars took provider-rating's proportional glyphs (the
styleguide showed five full stars for a 4.6 — a defect); message-thread took
dashboard-buyer-messages' bubble (timestamp on the page ground, better contrast);
alert-toast took the styleguide's four-variant map, widened with `dismissible` and a `role`
override so bid.html's tip and inline error stopped being bespoke panels.

New generators: `make-account-pages.py`, `make-bid-page.py`, `make-provider-pages.py`,
`make-buyer-dashboards.py`, `make-styleguide.py`. Same regeneration flow: run generators,
then `sync-shared.py` once.

### Why one page each, not one template

A marketplace's listing pages *are* its long-tail search surface. Sharing head tags across
them would make them a single page as far as a crawler is concerned. Each page now carries
its own `<title>`, meta description, canonical, `og:url`, `og:image` (its own photograph)
and a 3-level `BreadcrumbList`. Titles are kept under 60 characters by putting the zip in
the description rather than the title — the longest address, 1204 Westover Hills Blvd,
lands at 55.

**No `Residence` or `Offer` schema.** Schema inherits the never-invent rule and this is
sample data; breadcrumbs only.

### One source of truth

`listing_data.py` holds the property set and is imported by **all three** generators, so
the search grid, the detail pages, and the seller dashboards cannot disagree about a
property. Secondary facts — lot size, year built, price per sq ft, property tax, HOA,
roof age, saves, bids, schools — are **derived deterministically** from the primary fields
rather than typed by hand:

- property tax at Richmond's ≈$0.86 per $100 assessed
- bid count scaled from views and days listed; top bid just under asking, each earlier bid
  stepping down
- schools mapped per neighborhood from a VDOE/NCES-shaped table (never GreatSchools)
- HOA only on condos and townhouses

Regeneration is reproducible, and it stays obvious that no real property record is involved.

### Each page carries

Breadcrumb · gallery with per-property hero and three thumbnails · days/views/saves ·
read-only bid panel (or "bidding is closed" when sold) · Leaflet map panel · a
nine-field property-details table · neighborhood schools · docs · sticky request form
and primary actions · a commission-savings panel computed from that property's price ·
and **"Other homes in {neighborhood}"**, which resolves to real sibling listings.

### Data consistency fixed along the way

The seller dashboards previously used invented addresses (1338 Elm St, 3306 Garfield Dr,
2900 Gemini St) that existed nowhere in the marketplace. My Listings and the Offers table
are now derived from the real set, so every address in the prototype resolves to a page
that exists. Hand-authored references on `compare.html` and `dashboard-buyer.html` were
retargeted the same way.

Verified: all 18 search cards link to 18 distinct pages, all returning 200.

## Round 5 — two screens I had missed (2026-08-05)

**Correction to the round-3 note.** It claimed every dashboard comp on disk had been built.
That was wrong — two existed that were never built. Found by cross-referencing against
`https://garzawebdesign.com/link-mockups-v3`, the client-facing index of all 16 hosted
mockups. The hosted files match Dropbox exactly, so nothing already built was based on the
wrong material; the page simply exposed the gap. **22 pages now.**

| Page | Comp |
|---|---|
| `dashboard-seller-listing-edit.html` | `link-seller_single-listing-v2.jpg` (2026-04-10) — the seller's editing view, counterpart to the public `listing.html` |
| `dashboard-seller-showings.html` **rebuilt** | `link-SellerBuyer_showings-tab-v2a.jpg` (2026-04-10) — supersedes the `_showings-tab-v1` the first version used |

### Showings v2a — DR-1 made visible

The v2a comp is materially richer than v1: a saved-properties rail with per-property
Schedule / Request info / Reschedule actions, an Accepted/Proposed key, accept-decline
controls on proposed times, and — most usefully — **an explicit double-booking warning**:
a red-outlined day holding two 11:30 am showings on different properties.

That cell is the exact case DR-1 exists for, so the page carries a note that in production
the `link_booking` constraint plus the core lock service rejects the second booking **at
save time**, regardless of entry point — the calendar warning is the symptom, not the
enforcement. Rendering verified: 6 weeks / 42 cells, events on days 6, 13, 17, 30, conflict
on 17 with two entries and the warning banner.

The v1 comp's wrong lead-in dates (`27, 28, 29, 30, 31` — April has no 31st) repeat in
v2a. The corrected Apr 26–30 lead-in is used again.

### Edit Listing

Promote / Pause toggles, Save / Cancel, a "Draft saved" badge, property-details form,
10-tile media gallery with Add photo, overview fields (key highlights, unique features,
key upgrades, neighborhood highlights, preferred closing timeframe), financial details,
docs, service providers, scheduled showings, pending requests, offers.

Two comp notes: the Map View shows **Google Maps** — replaced with Leaflet + OpenStreetMap
per the resolved decision — and the comp's section numbering runs 1, 2, 3 then restarts at
3 for Scheduled Showings. Renumbered 1–5 here.

"Edit listing" on every My Listings card now links here.

## Round 4 — 18 listings + live faceted search (2026-08-05)

`search.html` now carries **18 sample listings**, each with a distinct CC0 photograph, and
the filters and sort are **functional**, not decorative.

### Listing data

Eighteen Richmond properties across seven real neighborhoods (The Fan, Church Hill, Museum
District, Ginter Park, Westover Hills, Scott's Addition, Midlothian), three property types,
$232,500–$825,000, with beds, baths, sq ft, days on market, and view counts. Every field is
a `data-` attribute on the `listing-card`, which is what the JS filters against:

    data-price data-beds data-baths data-sqft data-type data-hood data-days data-views

That mapping is deliberate — in Drupal these become the Search API indexed fields, so the
prototype's filter logic documents the index the Drupal phase has to build.

### What works

- **Filters** — price band, bedrooms, bathrooms, property type, neighborhood.
  Multiple selections within a group are OR; across groups they are AND.
- **Bedrooms and bathrooms match differently, on purpose.** Bedrooms are exact counts
  — `1 · 2 · 3 · 4 · 5+` — where only the trailing `+` means "or more". Bathrooms stay
  cumulative thresholds — `1+ · 2+ · 3+ · 4+` — where the loosest selected threshold wins.
  There is no exact "5" bedrooms option: with 5 the highest count in the set it returned
  the same homes as "5+", so the two were indistinguishable. Both rules are commented in
  `linkFacetedSearch()`; the Drupal build needs the same split (a Search API integer field
  faceted as exact values vs. as ranges).
- **Live facet counts** update as you filter, and **a facet's own group is excluded from
  its own count** — which is why selecting "3+ beds" doesn't zero out the other bed
  options. This is how Search API + Facets actually behaves; matching it here means the
  Drupal build has a working reference rather than a guess.
- **Zero-result facets disable and dim** rather than disappearing.
- **Sort** — newest (fewest days listed), price ascending, price descending, most viewed,
  largest. Sorting applies to the filtered set.
- **Result count** and **empty state** are driven by the same render pass.
- **Clear all filters** resets everything.

Verified in-browser: 18 → Church Hill 3 → +3 beds 1 → Condo + 4 beds 0 (empty state shown,
18 facets disabled) → cleared back to 18. All five sort orders confirmed against the data.

The behavior is `linkFacetedSearch()` in `js/main.js` — one named function, keyed off
`data-results`, so it costs nothing on the 20 pages that don't have a result grid, and it
converts to a single `Drupal.behaviors` entry later.

## Imagery — provenance and licensing (2026-08-05)

Two sources, both clean. **Nothing watermarked or unlicensed is in this prototype.**

### 1. Client's own assets — `__LiNKrem.com/designs/images/building site/`

| File in `img/` | Source |
|---|---|
| `hero-home.jpg` | `hero images/LiNK-real-estate-hero.jpg` — the exact shot in the homepage comp |
| `logo.jpg`, `og.jpg` | `logos/linkrem-logo-500w.jpg` |
| `favicon.jpg` | `tall images/linkrem-favicon.jpg` |
| `band-selling.jpg` | `tall images/zero-percent-commissions-calculator.jpg` |
| `band-buying.jpg` | `tall images/real-estate-neighborhood-rio-rancho-web.jpg` |

This resolves the earlier `[ VERIFY: logo version ]` flag — the double-box
`LiNK / REAL ESTATE MARKETPLACE` lockup is the current mark.

### 2. CC0 stock — StockSnap.io, found via the Openverse API

Property and interior photography: **`listing-1.jpg` … `listing-18.jpg`** (one distinct
exterior per listing), `listing-hero.jpg`, `gallery-1..6.jpg`, `pathways.jpg`,
`band-services.jpg`.

**License: CC0** (public domain dedication) — free for commercial use, no attribution
required, no redistribution restriction. Safe for the live site as well as the prototype,
though real Richmond property photography should replace it before launch.

### ⚠ What is deliberately NOT used

Both Adobe folders — `images/AdobeStock/` (36 files) and `mockups/stock photos/` (12) —
contain **only `_Preview` files**: watermarked, unlicensed comps. They must not be used in
a client presentation or in production. If you want the specific images the comps were
designed around, the Adobe IDs are in the filenames and need licensing first.

### Generated placeholders (not photography)

- `provider-1..6.png` — navy lettermark tiles (BC, OD, FH, JR, RP, SS). The six sample
  providers are fictional, so a real logo would mean inventing a brand. Lettermarks are
  honest and on-palette.
- `map-placeholder.jpg` — a neutral grid. Map slots get their own placeholder so a
  building photo never masquerades as a map. Real maps are Leaflet + OpenStreetMap.
- `ad-slot.jpg` — the footer ad inventory box.

Every image still sits inside a drag-and-drop `image-slot`, so anything here can be
replaced in the browser during a client walkthrough. Filled slots carry `is-filled`,
which suppresses the striped placeholder.

## Placeholders — nothing fake ships

- `[ VERIFY: phone ]` — no published phone number
- `[ VERIFY: address ]` — no street address; **excluded from schema entirely**
- `[ VERIFY: logo version ]` — three candidates, see Assets
- All photography — image-slot placeholders
- Listing data on `listing.html` / `search.html` is sample data, visibly labelled
- Provider names, ratings, review counts — sample, visibly labelled

Per the never-invent rule: no invented phone, address, team member, testimonial, review
count, or statistic. `[ VERIFY ]` items stay out of JSON-LD.

## How to run and maintain this prototype

**View it.** Pages reference `css/extras.css` and `img/*`, so serve the folder rather
than double-clicking a file:

    python3 -m http.server 8412 --directory prototype

**Two generator scripts. Run them, never hand-edit what they produce.**

| Script | What it does |
|---|---|
| `make-pages.py` | Regenerates the 9 shell-based pages. `index.html` (chrome source) and `how-it-works.html` (long-form) are excluded. |
| `sync-shared.py` | Stamps `css/tokens.css` into every page's inline `<style>`, copies the header/footer from `index.html` into all pages, and sets `aria-current="page"` per page. |

Order matters: `python3 make-pages.py && python3 sync-shared.py`.

**Field conformance (added Round 12).** `link_fields.py` is generated from the client's
build manual and is the source of truth for every field name, value list and vocabulary in
the prototype. After a new manual revision:

    python3 make-field-map.py          # regenerates link_fields.py from the newest .xlsx
    python3 make-listing-wizard.py     # and any other generator the change touches
    python3 sync-shared.py
    python3 verify-fields.py           # must print PASS

`verify-fields.py` fails on a manual field that reaches no page, on a field name in the
HTML that the manual does not define, and on a broken v6 invariant (market status vs
moderation state, nine wizard steps, the resume affordance, the DR-8 review threshold).

**Why tokens are inline on every page.** Tailwind v4's *browser* build is used so the
prototype needs no compile step, and it resolves only `@import "tailwindcss"` — anything
else fails hard:

    Error: The browser build does not support @import for "./css/tokens.css"

So `css/tokens.css` stays the single source of truth (drupal-ddev-cpanel lifts it verbatim
into `src/input.css`) and `sync-shared.py` copies it into each page. It also strips the
literal `@import "tailwindcss";` line from the inline copy only — the browser build tries
to *fetch* that as a relative URL, 404ing on every page load. The line stays in
`tokens.css`, where the real Tailwind CLI build needs it.

**Two traps already hit and fixed — don't reintroduce them.** `css/extras.css` is an
unlayered plain stylesheet, so anything declared there outranks *every* Tailwind utility:

- `img { height: auto }` there silently stretched every `object-cover` image to its
  natural aspect ratio. Preflight already sets it, inside a layer where utilities win.
- `.image-slot { position: relative }` there overrode the `absolute` utility and pulled
  the hero background out of `position:absolute` and into normal flow, doubling the hero's
  height. Positioning now comes from utilities in the markup.

Keep `extras.css` to things utilities genuinely cannot express: `@keyframes`, `::before`
art, third-party widget overrides.

**No Tailwind Plus blocks were used** — `~/tailwind-plus-blocks/` does not exist on this
machine, so every section is hand-built to the same conventions. Saving blocks there would
speed up future rounds.

## Verification

`scripts/verify.py` — **0 failures, 0 warnings** across all 11 pages (2026-08-05).

| Page | Visible words |
|---|---|
| how-it-works.html | **2,121** — target was 2,000+, set from the Opendoor/Redfin teardown |
| buy.html | 714 |
| index.html | 693 |
| faq.html | 624 |
| sell.html | 544 |
| search.html | 460 |
| listing.html | 427 |
| about.html | 426 |
| services.html | 369 |
| contact.html | 366 |
| provider.html | 287 |

Browser-checked: mobile breakpoint (burger nav, single-column cards), token resolution
(`#06446C` computed), listing gallery, bid table, schools list, no failing network
requests, no console errors.

## Round 2 — authenticated screens (2026-08-05)

Three signed-in pages added. All carry `<meta name="robots" content="noindex, nofollow">`
— dashboards must never be indexed — and none carry JSON-LD, deliberately.

| Page | Source | Components exercised |
|---|---|---|
| `dashboard-seller.html` | comp `link-seller_overview-tab-v1.jpg` | `portfolio-table`, `status-badge`, `alert-list`, `showing-calendar`, `dashboard-stat-card` |
| `dashboard-buyer.html` | comp `link-buyer_overview-tab-v2.jpg` | `progress-tracker`, `saved-homes`, `mortgage-calculator`, `alert-list`, `showing-calendar`, `offers-activity` |
| `compare.html` | Christine's *Compare Properties Page* spec (2026-04-23) | `compare-table` (sticky first column **and** header, max 4), `alert-settings` |

**Chrome is now two variants.** Public pages carry `chrome:header` (sourced from
`index.html`); signed-in pages carry `chrome:appheader` (sourced from
`dashboard-seller.html`) — logo, utility nav, Edit Profile, Logout. The per-role tab row
is **not** synced: seller tabs are Overview/Notifications/My Listings/Offers/Showings/
Insights/Settings, buyer tabs swap My Listings for Messages. Footer is shared by all 14.

**The mortgage calculator is real, not a mockup.** Standard amortization in `main.js`,
with the zero-rate division guarded. At the comp's inputs ($400,000 value, $80,000 down,
5.00%, 30 years, $2,700 tax, $1,500 insurance) it returns **$2,067.83/month — the exact
figure printed in the comp**, which is a useful cross-check that the comp's math and ours
agree. The loan-breakdown donut is a `conic-gradient` ring driven by CSS custom properties
the calculator sets; no charting library, no hand-drawn SVG.

Status colors in the comps (Active green, Pending amber, Sold grey) already matched the
`status-badge` token map, so no new palette was introduced — tab 13-SDC's "ONE canonical
color map for ALL statuses" holds.

## Round 3 — remaining dashboards (2026-08-05)

Seven more pages, generated by `make-dashboards.py`. **21 pages total, verify-clean.**

| Page | Source comp |
|---|---|
| `dashboard-seller-notifications.html` | `link-seller_notifications-tab-v1.jpg` |
| `dashboard-seller-listings.html` | `link-seller_my-listings-tab-v1.jpg` |
| `dashboard-seller-offers.html` | `link-seller_offers-tab-v1.jpg` |
| `dashboard-seller-showings.html` | `link-seller_showings-tab-v1.jpg` |
| `dashboard-seller-profile.html` | `link-seller_settings-tab-v1.jpg` |
| `showing-request.html` | `link-SellerBuyer_showingRequest-tab-v2a.jpg` |
| `concierge.html` | `concierge-service-selection-page-v2.jpg` — **public chrome**, not a dashboard |

The seller tab row is now fully wired. **Insights** stays `#` — no comp exists for it.

### Three comp discrepancies found, all flagged on-page

1. **The showings calendar lead-in dates are wrong in the comp.** It shows
   `27, 28, 29, 30, 31` before May 1 — but April has no 31st. May 1 2026 is a Friday, so
   the correct lead-in is Apr 26–30. **The prototype uses the correct dates**, which is why
   its first row differs from the comp by one day.
2. **The settings comp labels its tab "Profile"**; the other five comps label the same tab
   "Settings". The prototype uses "Settings" in the tab row and "My Profile" as the page
   title, matching the majority.
3. **The showing-request comp uses a navy tab bar** where the other five use white. White
   is used, being the majority convention.

### Notes on specific pages

- **Showing request** implements DR-1 visibly: booked slots render as blocked rather than
  selectable, with an on-page note that the real enforcement is the `link_booking`
  constraint plus the core lock service at save time — not merely a hidden form option.
  Sentry and buyer instructions are separate blocks; the comp is explicit that each party
  sees only their own message.
- **Concierge** carries real prices from the client's comp ($500 / $2,500–3,000 / $5,000),
  flagged on-page for confirmation because pricing is a commercial commitment. Payment-brand
  marks are image-slots — never hand-drawn, since each brand has usage rules.
- **Profile** uses `[ SAMPLE: … ]` for every personal field. The comp's placeholder data was
  Star Trek names and a `starfleet.gov` address; none of it was carried over.

### Still out of scope

Seller 9-step listing form (`step-tabs` — the largest remaining piece), bid submission
flow, messaging (`message-thread`), Insights tab, provider dashboards, admin.

**No comps exist for any of these.** All 16 mockups listed at
`https://garzawebdesign.com/link-mockups-v3` are now built (older versions of the same
screen — `single-listing-v2a`/`v3`, `overview-tab`, `single-listing-v1`/`v1a` — are
superseded by the latest of each). The remaining work needs either new designs or a
decision to build from the database spec and tab 13-SDC props, the way `search.html` was.

## Out of scope this round

Seller and buyer dashboards, bid submission flow, seller 9-step listing form, messaging,
showings calendar, concierge selection, admin. Comps exist for most — a later round.
