# Flux showcase — build notes

The public component gallery and roadmap at **life.pondocean.co/show**.

This file is the honest record behind that site: what was counted, what was
reproduced, where the code and the documentation disagreed, and what broke.
It ships with the site on purpose. A gallery that cannot say what it got wrong
is a brochure.

Built 2026-07-16 against Flux **v0.6.0** (`src/lib/changelog.ts`).

---

## What this is

A **static reproduction** of Flux, generated from Flux's own source. No live
app, no login, no network, no framework. Every page is plain HTML that renders
from the filesystem.

- **17 surfaces** — full 1280×800 frames, one per place a user can be
- **98 component cards** covering **78 component files** (every one)
- **9 categories**, grouped by the job a component does, not the folder it sits in
- **34 roadmap items** across the product's own six funnel stages
- **194 search records**, parsed out of the rendered markup

The rule the whole thing runs on: **the shipped code is the authority.** Not
the docs, not the brief, not the changelog. Where they disagreed, the code won
and the disagreement is written down below.

## Counts, and how to check them

```bash
node scripts/build-search-index.mjs   # 194 records; warns if cards != expected
node scripts/check-coverage.mjs       # 78/78 files covered + hub counts match
node scripts/lint-showcase.mjs        # the failure modes that still render "fine"
node scripts/check-public-safety.mjs  # refuses to publish a known leak
node scripts/capture-screenshots.mjs  # 17 frames + 98 cards, validated
node scripts/serve-showcase.mjs       # search and roadmap need http
```

**98 cards vs 78 files is not a discrepancy.** Several files export more than
one UI-bearing component that earns its own card (`notes-board.tsx` alone
contributes QuickCapture, ColorPicker, ChecklistEditor, NoteCard and
ArchivedCard). Card count catches a card you meant to build and forgot. It does
**not** catch a file nobody covered, which is the question that actually
matters, so `check-coverage.mjs` reconciles every `.tsx` against the cards'
`.src` paths and must stay at 78/78. It found the one gap in this build:
`life-priorities/priority-manager.tsx` had no card. It caught a second one
later, the moment the changelog rewrite added `technical-breakdown.tsx`.

Four files are covered despite rendering nothing useful, and are labelled
rather than dropped, so both numbers reconcile:

| File | Why it is still on the site |
|---|---|
| `shared/theme-provider.tsx` | 9-line next-themes pass-through, renders no UI |
| `shared/sw-register.tsx` | `return null`; registers the service worker |
| `circular-view.tsx` (root) | Byte-identical duplicate of the calendar's. **0 importers.** |
| `life-priorities/priority-manager.tsx` | Superseded by an inline version on the page. **0 importers.** |

## The design system, taken from the code

Nothing here was invented or improved.

| Token | Value | Source |
|---|---|---|
| primary | `#673AB6` | `globals.css` `hsl(262 52% 47%)` |
| accent | `#009485` | `globals.css` `hsl(174 100% 29%)` |
| background / card | `#F5F5F5` / `#FFFFFF` | `globals.css` |
| border | `#E5E5E5` | `globals.css` |
| chart 1-5 | `#673AB6 #009485 #274754 #E8C468 #F4A462` | `globals.css` |
| radius | `8 / 6 / 4` px | `--radius: 0.5rem`, md = −2px, sm = −4px |
| type | **Inter**, weights 400-700 | `src/app/layout.tsx` |
| mono | plain `monospace` | `tailwind.config.ts` `font-code` |
| icons | **Lucide**, 107 unique | 58 files import `lucide-react` |

**Icons are the product's, not lookalikes.** `scripts/extract-lucide.mjs` reads
the geometry out of the installed `lucide-react` package and emits it as inline
`<svg>` — which is also exactly what lucide-react renders. No path was drawn by
hand.

**Absence is a finding.** Flux is icon-library-native where its sibling Tabatha
ships no icon library and uses emoji throughout. Each showcase follows its own
product. That is why the two sites look nothing alike.

**Inter is self-hosted here.** Flux loads it from the Google Fonts CDN via a raw
`<link>`. This site's CSP is `font-src 'self'` (no CDNs, by design), so the same
latin subset is vendored into `assets/fonts/`. Same font, same weights, no third
party.

---

## Discrepancies found

Every one of these is a place where Flux's code and Flux's documentation
disagree. **The code is what users get.** Nothing below was fixed; this is a
render, and a drive-by fix buries the finding.

### The big one: the app's honesty mechanism is now dishonest

`src/lib/route-truth.ts` drives the Prototype/Beta banners. It is **stale in
both directions**, which makes it the most misleading file in the repo.

**It calls seven working surfaces fake.** All seven are Supabase-wired; the
seed arrays were deliberately removed and the source files still carry the
commits proving it. The app's own changelog contradicts its own banner.

| Route | What the banner says | What the code does |
|---|---|---|
| `/goals` | "Goals are seeded sample data." | Real `goals` table |
| `/systems` | "Systems are seeded sample data." | Real `systems` table |
| `/frictions` | "Frictions are seeded sample data." | Real `frictions` table |
| `/choices` | "Choices are seeded sample data." | Real `choices` table |
| `/insights` | "Charts display mock data." | Real `day_rollups` |
| `/environments` | "Environment data is hardcoded." | Real `environments` table |
| `/audit-log` | "Shows static sample history." | Real `sync_log` + `agent_actions` |
| `/integrations` | "Connect flows are not functional." | 4 providers genuinely work |
| `/conversations` | "AI processing not yet functional." | It runs and persists |

v0.5.0's changelog: *"Goals, Systems, Frictions, Choices, Environments… now save
to your account (no more demo data)"*. v0.6.0's: *"Audit Log is real: actual
sync and agent activity, no more sample rows."*

**And it does not know five routes exist.** `/tabatha`, `/people`,
`/context-inbox`, `/admin` and `/management/choose-your-hard` have no entry, so
`getRouteConfig` falls through to a default that stamps amber **"Prototype —
This route has not been evaluated"** on four fully-shipped surfaces.

Reproduced exactly as shipped on every frame. A stale honesty label is worse
than none, which is why "Labels that tell the truth about each page" is on the
public roadmap.

**Four of the banner's six variants are unreachable.** `broken`, `hidden` and
the `fix-before-launch` disposition are declared and used by zero routes.
`reference` is declared by four routes, all of which set `showBadge: false`.
Only `shallow` (9 routes) and `partial` (5) ever paint.

### The brand colours in the comments are not the brand colours on screen

`globals.css` annotates `--primary` as `#673AB7` and `--accent` as `#009688`,
and `blueprint.md` repeats both. The HSL it actually ships resolves to
**`#673AB6`** and **`#009485`**. The comments are rounded by one step. The
showcase uses the pixels.

### Everything else

| # | Discrepancy |
|---|---|
| 1 | `docs/route-truth-matrix.md` disagrees with `route-truth.ts` *and* with the code. It will make you understate ~7 working surfaces. Do not source from it. |
| 2 | `docs/features.md` marks **all 37 features ✅ Shipped** while `launch-checklist.md` has **16 of 29 boxes open**. Neither is trustworthy alone. |
| 3 | `APP_VERSION` is `0.6.0`; `package.json` says `0.1.0`. |
| 3a | **The prototype release dates were fabricated.** 0.1.0-0.1.2 claimed 2024-07-29/30/31. Git says that work happened on 2025-08-13: wrong by roughly a year. Corrected, and the history backfilled. |
| 3b | **Nine months of real work had no release at all.** The changelog jumped from the 0.1.x prototype to 0.2.0, skipping 74 commits across 2025-10 to 2026-05 — including the entire meaning layer (goals, frictions, choices, environments, conversations) and the move off Firebase. Reconstructed from git as 0.1.2-0.1.6. |
| 4 | `docs/CHANGELOG.md` had an "Unreleased — Admin panel" section that `changelog.ts` never got, so the app and its own docs disagreed about what shipped. That doc is now **generated** from `changelog.ts`, so the drift cannot recur. |
| 5 | `/management/docs` and `/management/api-data` document a **Firebase/Firestore** app. Flux is Supabase. They describe collections that do not exist. |
| 6 | `docs/flux-current-state-inventory.md` is ~6 weeks stale and predates four releases. |
| 7 | Nav says **"Goals"**; the page title says **"Goals & Systems"** — and `/systems` is a separate route in the same nav group. |
| 8 | Sidebar says **"API & Data"**; route-truth says **"API/Data"**. Same path. |
| 9 | Entry's stage union is `focused`; Tabatha's `FunnelStage` is `focus`. Two models, one letter apart. |
| 10 | AssemblyAI is ✅ at 0.3.0, but transcription is unverified end to end and `TRN1` is still open. |
| 11 | ElevenLabs and Codex are described as part of the live backbone in two docs. Neither is built. |
| 12 | `BetaBanner` renders **above** the header, not below: `layout.tsx` puts it before `{children}`, and every page's `AppHeader` is inside children. |
| 13 | `OverlapSettings` has **5** InfoTooltips, not 4. |
| 14 | `neuroType` is a `Select`, not a radio group; the option labelled **"Autism Spectrum"** stores `Autism`. |

---

## Real bugs surfaced

Forcing every component into every state is an unusually thorough test. It
found these. **None were fixed** — a showcase branch that also carries product
fixes is a branch nobody can review. They are reproduced as shipped and
labelled on the card so they do not read as showcase defects.

### Data loss and silent failure

1. **The Journal's New Entry dialog throws your text away.** Save is
   `disabled={!title || !content}` (raw truthiness) but the handler tests
   `.trim()`, and Save is wrapped in `DialogClose`. A single-space title enables
   the button, the handler rejects it with a toast, and the dialog closes
   anyway. The entry is unsaved and unrecoverable.
2. **The AI check-in claims to have saved.** It toasts *"Your task and schedule
   have been updated"* when nothing is written anywhere. The answer exists only
   in the result panel and closing the dialog discards it.
3. **Admin's Settings and Stats tabs stick on "Loading…" forever after a failed
   read.** Both gate the loading line on a value only ever set on success.
4. **A broken load is indistinguishable from an empty one** in three admin tabs
   once the toast expires: *"No admin actions recorded yet."* on a log that
   failed to load.
5. **A 500 from OpenAI or AssemblyAI renders the row green.** `live-status.tsx`
   branches to `error` only when `fetch` itself throws; every HTTP status maps
   to `ready` or `unconfigured`. An outage reads *"AI processing ready"*. Only
   Asana surfaces `HTTP {status}`.

### Things that look like controls but are not

6. **The Now page's time dials only fire a toast.** *"Time Adjusted — The entry
   time has been shifted."* The entry does not move.
7. **The Now page's duration stepper is decorative.** `duration` is captured and
   never used in `handleAddEntry`; every entry is created unscheduled at
   `00:00–00:00`.
8. **"Use Voice Input" has no click handler.** Pure decoration.
9. **Audio's "Upload audio" has an inert `disabled`.** `<Button asChild disabled>`
   wraps a `<span>`, and a span cannot be disabled, so it never dims and never
   blocks — while the Record button beside it dims correctly.
10. **FocusView's Pause does not pause.** The timer recomputes from wall-clock
    `end`, so the display freezes while the deadline keeps coming.
11. **PlannerSettings' master switch is decorative.** Turning off "Enable Plan
    Locking" leaves both dependent controls fully interactive.
12. **A `Loader2` with no `animate-spin`** on "Process now": a spinner that never
    spins.

### Layout and rendering

13. **The Now card's Focus button covers its own end time.** Measured: the 40px
    button spans x=967–1007 and the time label x=939–1007. Every NOW card with a
    focus action hides when it ends.
14. **Every sub-45-minute entry clips its own title.** `isShort` hides the tags
    and time *below* `md` and shows them at desktop, but block height depends on
    duration, not viewport width. A 30-minute block is ~48px at every width, so
    the small screen gets the clean render and the desktop gets the collision.
15. **The calendar cannot show a container and its children at once.** Show
    Shadows ON filters out every container; OFF drops every child. There is no
    setting that shows both, and the first line of `getVisibleEntries()` is dead
    code overwritten in both branches.
16. **Deleted planned entries render twice, with duplicate React keys.**
    `deletedEntries` is a subset of `plannedEntries`, then `AgendaView` renders
    `[...entries, ...deletedEntries]`.
17. **`h-full` is inert on every page root.** `SidebarInset` is `min-h-svh` only,
    never a definite height, so no `flex-1 overflow-auto` main in Flux ever
    scrolls internally. The document scrolls instead.
18. **Tabatha's stage tints erase the card surface.** `cn()`/tailwind-merge lets
    `bg-blue-500/5` *replace* Card's `bg-card`, so every focus item sits on a 5%
    tint over grey rather than on white.
19. **Integration catalog titles clip.** `CardTitle` is `text-2xl` in a
    4-column grid; "Monday.com" runs past the card edge and is cut by
    `overflow-hidden`.
20. **The clock spinner's hand does not pivot at the clock's centre.** It sweeps
    a circle centred 24px below the dial and its tip leaves the face every
    rotation. The `transform-origin: center` in `globals.css` is dead: an inline
    `transformOrigin: 'bottom'` always wins.
21. **Friction priorities 1 and 2 are indistinguishable**, and P2's
    `text-yellow-500` sets the whole card's text colour against a pale yellow
    card.
22. **The settings accordion draws a stray line under every card**, and six of
    seven groups are below the fold on load.

### Reachability and correctness

23. **Three routes are unreachable from mobile.** `mobile-drawer.tsx` and
    `mobile-drawer-settings.tsx` hold byte-identical stale 17-item lists that
    omit `/tabatha`, `/people` and `/context-inbox`. Not pinnable, not in the
    overflow grid, no "more" link.
24. **Two neurotype presets break the view they select.** `defaultView` is a
    4-way union but CalendarSettings offers only Week/Day. Autism and Dyslexia
    set `agenda`, so those users see the trigger render its placeholder *"Select
    rule..."* forever.
25. **Neurotype presets leak.** Only `Default` touches all six keys, so
    switching Autism → Anxiety leaves `defaultView: 'agenda'` behind. Returning
    to Default restores *shipped* defaults, not the user's prior choices.
26. **The birthday reminder branch is unreachable.** `refreshReminders()` only
    ever writes `reason: 'cadence'`; the Cake icon never renders, and
    `personDetails.birthday` is collected and drives nothing.
27. **"Confirm & Add All" is a no-op on an existing conversation.** It toasts
    *"The items have been added to your system"* and creates nothing. Only the
    `isNew` path persists.
28. **"Upload Audio" on the conversation detail page has no `onClick`.**
29. **Exhausted and inactive invite codes look identical**, and the action
    button reads a different field than the badge: an exhausted code still
    offers "Deactivate".
30. **`VariableKindCard`'s empty state ignores the filter** — keyed on unfiltered
    length, so filtering everything out leaves a blank row and no message.
31. **"End of day" has no scheduler.** Committed audio processes only when
    someone loads the page.
32. **Raw backend error text reaches the UI in four of five admin tabs.**
33. **`IntegrationCard`'s "Coming soon" is invisible to keyboard and screen
    readers** — `pointer-events-none`, `aria-hidden`, nothing focusable.
34. **`NoteCard`'s "Saved" is a 750ms timer**, not a write acknowledgement.
35. **The New Entry dialog keeps yesterday's date across midnight** — `entryDate`
    resets only after a successful save.

### Hardcoded where it should not be

36. **The sidebar's footer user block never calls `useUser()`** — hardcoded
    "User" / "user@flux.com" / a placehold.co avatar, two rows below a header
    that can sign the real account out.
37. **UserProfileSettings' name, email and avatar are fake** and Save Changes
    only toasts. The neuro/planning/nightly settings on the same card are real.
38. **The AI check-in sends fixtures.** `taskDetails: "Working on Flux project,
    calendar view component."` on every call, regardless of your actual day.
39. **All four onboarding steps render the same stub** — `<p>Step {n} Content</p>`
    — yet Finish writes `onboardingCompleted: true`.

---

## Faithful vs. approximated

**Verbatim from the source:** every visible product string. Headings,
descriptions, placeholders, button labels, tab names, empty-state copy, toast
text, badge labels, honesty strings. Copied out of the components, never
paraphrased. Product strings keep their em-dashes (*"Saved — not committed"*);
the site's own prose has none.

**Real, not staged:** all colours and radii resolve from `globals.css` and
`tailwind.config.ts`. All icon geometry comes from the installed `lucide-react`.
The type is the product's Inter. The sidebar in all 17 frames is generated from
the real nav arrays by `scripts/inject-shell.mjs`, so it cannot drift between
frames.

**Deliberately reproduced quirks** (these look like showcase bugs and are not):

- The Now page's add-entry card uses `rounded-xl` (12px), which is **not** on
  the app's radius scale (8/6/4). It hardcodes its own corner.
- The app header's two icon buttons are **different sizes**: `SidebarTrigger` is
  `size="icon"` overridden to `h-7 w-7` (28px); `ThemeToggle` is a plain
  `size="icon"` (40px).
- The notes board and the friction priorities name **Tailwind's stock palette**
  directly (`bg-red-500/10`, `yellow-400/10`). There is no Flux token to point
  at, so those hexes are literal. The board's teal is `#2dd4bf`, not Flux's
  `--accent` `#009485`: that component escapes the design system, and its card
  says so.
- The probe icons' emerald and amber are hardcoded in `live-status.tsx`, not
  tokens.

**Composed or staged, and disclosed:**

- **All demo data is invented.** Generic labels, invented people (Priya
  Raghavan, Tomás Adeyemi, Rita Okonkwo…), `@example.com` addresses (RFC 2606,
  unassignable), `+44 7700 900xxx` and `+1 555 01xx` reserved fictional numbers.
  No real person, client, customer or teammate appears anywhere.
- **Numbers are internally consistent.** Rollup stat cards equal the last bar
  pair; good-day scores were computed with the component's own formula; the
  circular view's slices sum to their stated minutes; admin per-user activity
  sums to the stat tiles.
- **Charts are static SVG, not Recharts.** Series, fills, corner radii, axis
  strokes and legends are faithful; tick placement may drift a pixel from a live
  render. Tooltips and hover cursors are not reproduced.
- **Probe states are staged** so all four `ProbeState` branches appear in one
  capture. A real page shows whatever the probes returned.
- **One catalog card is staged at `:hover`** so the "Coming soon" overlay is
  visible at rest.
- **`broken` and `hidden` banner bodies use the fallback string** *"This route
  has not been evaluated."* because no route supplies a sentence for those
  statuses. No product copy was invented.
- Frames use `overflow: hidden` to stand in for the viewport edge. Where a page
  runs past the fold here, it runs past the fold in the product.

---

## The Admin pages, and the gate

`admin.html` and `components-admin.html` sit behind a passphrase prompt
(`assets/gate.js`).

**That gate is obscurity, not security, and should not be mistaken for it.**
The pages are static files: the markup reaches the browser before the prompt
does, and the passphrase is readable in the source. It stops a casual visitor
stumbling in. It stops nothing else.

That is acceptable here for exactly one reason: **there is no real data behind
it.** Every email, invite code, count, timestamp and audit row on those pages is
invented. No RPC name, error token, env var or migration number is reproduced.
Nothing there is worth defeating the gate for.

If real data ever lands on those pages, `gate.js` is not the control you need.
Move them behind server-side auth.

The gate no-ops on `file://`, because capture navigates that way and a gate that
hid the cards would hand the capture script a folder of blank PNGs.

---

## Public safety

The repo's docs contain a production IP, a production hostname, three real
people's names, ~30 Asana workspace GIDs, hosting topology, credential
architecture, and an unannounced internal project codename.

**None of it is on this site**, and "we were careful" is not a control:
`scripts/check-public-safety.mjs` is. It scans every publishable file against 17
rules and exits non-zero on a hit. Wire it into CI beside
`build-search-index.mjs --check`.

**The roadmap is hand-curated and never piped from the internal tracker.** Not
filtered, not tagged-public. Every design where publication is the default takes
exactly one mislabelled task to publish something you cannot recall. A curated
file inverts it: nothing is public until a human writes it in, and every change
is a reviewable diff. The friction is the feature.

`roadmap.json` carries no dates, and the page says so. `resolved` items name the
version they shipped in, because "shipped" with no version is unfalsifiable.
`roadblocked` items say what they are blocked *on*, because "blocked" alone
reads as abandoned.

---

## The roadmap's columns

The six columns are **Flux's own progress funnel** (`src/lib/tabatha/types.ts`,
`FunnelStage`), in the order the funnel actually flows: Unsorted → To Do → In
Focus → Addressing → Roadblocked → Resolved.

Not invented marketing tiers. "Now / Next / Later" is a tier system designed to
avoid saying anything falsifiable. Using the product's own model is both more
honest and more interesting to read: it is *why* this board has a Roadblocked
column, and why two items sit in it saying exactly what they are waiting on.

The tags are Flux's own status legend from `docs/features.md` (Shipped / Built,
has bugs / Backend exists, no UI / Prioritized for next sprint / Planned /
Future). They are tags rather than columns so a card can say both where it sits
in the funnel and how finished it is — which is how "Two-way sync with Google
Calendar" can be Resolved *and* carry a caveat.

---

## Bugs in this build, and the traps behind them

Logged because each one rendered "fine" and passed a green check. Every rule in
`lint-showcase.mjs` exists because of one of these.

1. **A CSS reset outranked every component rule.** `.fx button` scores `0,1,1`
   and beat `.fx-btn--default` at `0,1,0`, so `background:none` silently
   stripped the fill off every filled button and the border off every outline
   button. Nothing threw. Fixed with `:where()`, which zeroes the reset's
   specificity.
2. **Tokens scoped to `.fx` made components invisible.** Frames carry `.fx` on
   the wrapper; category pages put components in `.vbody`, which had no `.fx`
   ancestor, so `var(--chart-2)` resolved to nothing and entry blocks rendered
   with no background at all. Invisible, not broken. Tokens moved to `:root`.
3. **Two stylesheets both defined `--muted`** — mine as text (`#6E6E78`), the
   product's shadcn as a background (`#EBEBEB`) — and flux-ui.css loads second,
   so every label on the site went pale. Renamed to `--ink-3`; a namespace
   warning now sits at the top of `site.css`.
4. **`<section class="sec" id="…">` indexed zero sections.** The builder's regex
   needed `id` immediately after `<section`. No error; search just quietly lost
   every section. Fixed in the *tool*, so attribute order can never cause it
   again.
5. **A Tailwind class inside a CSS comment closed the comment.** A wildcard with
   a slash-opacity suffix contains `*/`, which killed the rules after it. The
   frame still captured at 1280×800; only reading the PNG caught it. The lint
   script tripped the same trap while being written.
6. **Grid `stretch` shipped PNGs padded with dead white.** Capture clips each
   card by its bounding box, so a short card in a tall row exported 1439px of
   PNG for ~350px of content. Fixed with `align-items: start`.
7. **The capture profile raced across processes.** `cdpRun` resets to 1 every
   run, so concurrent invocations both reached for `.cap-profile-cdp-1` and the
   second died with `EPERM` — which looks like a page fault and is not. Three
   agents lost time to it. The pid is now in the profile name.
8. **Contrast was asserted, not measured.** The placeholder colour measured
   4.49:1 against `--panel`, just under AA, while a comment claimed 4.6. Every
   colour pair on the site is now computed, not eyeballed.

---

## Structure

```
showcase/
├── index.html                 hub: 17 surface cards (live iframes), 9 categories
├── <surface>.html             17 frames, each a .shot at exactly 1280×800
├── components-<category>.html 9 category pages, 97 cards
├── roadmap.html               kanban over the product's own funnel
├── roadmap.json               CURATED, public-safe, hand-written
├── search-index.json          GENERATED — never hand-edit
├── _headers                   CSP and cache policy
└── assets/
    ├── site.css               site chrome (light editorial instrument)
    ├── flux-ui.css            faithful reproduction of Flux's UI layer
    ├── site.js                shared search + feedback (project-agnostic)
    ├── gate.js                the Admin passphrase prompt (obscurity only)
    ├── lucide-icons.json      107 icons, real geometry from lucide-react
    └── fonts/                 Inter latin subset, self-hosted for the CSP
```

`scripts/` holds the build and its guards. `functions/api/feedback.js` files a
card's bug/feature report to a **public intake board**; with no token configured
it returns 501 and the client degrades honestly. Flux has no public issue
tracker, so `showcase-repo` is deliberately empty and no GitHub fallback is
offered.

## Design direction

**Light editorial instrument**: paper ground, hairline rules, flush-left
editorial type, Flux's real violet locked as the single accent, and its teal
reserved for data and status only. Motion is deliberately near-zero: this is a
reference document, and the frames are captured pristine.

It looks nothing like the Tabatha showcase, which is dark pop-art and
emoji-native. That is the point. **Tabatha watches attention and focus; Flux
interprets the whole life system.** Two products that refuse to become each
other should not have interchangeable galleries. The two sites link to each
other from the nav and the footer.

---

## Deploying to life.pondocean.co/show

The requirement, stated first: **a git-connected static host that redeploys on
push.** Cloudflare Pages is the recommendation (DNS, host, TLS and the feedback
Function are one product). Netlify is equivalent.

The site publishes under a **subpath**, `/show`, matching
`tabatha.pondocean.co/show`. Two things follow, and both are worth checking
before the first deploy:

1. **Mount the output at `/show`.** Every link inside the site is relative, so
   the pages work at any prefix without modification. The build just needs to
   place `showcase/` at `show/` in the output directory, e.g. a build command of
   `mkdir -p dist/show && cp -r showcase/* dist/show/` with output `dist`. The
   `_headers` file lists both `/…` and `/show/…` shapes so its rules bite either
   way, but note Pages reads `_headers` from the **output root**, so it must end
   up at `dist/_headers`, not `dist/show/_headers`.
2. **Check the feedback path.** Every page declares
   `<meta name="showcase-api" content="/api/feedback">`, which is root-absolute
   and therefore resolves to `life.pondocean.co/api/feedback` regardless of the
   `/show` prefix. That matches a Pages Function at repo-root `functions/api/`.
   If the deploy nests the function under the prefix instead, change the meta in
   all 11 pages to a relative `api/feedback`.

**Ship it before the backend exists.** With no `ASANA_TOKEN` configured the
endpoint returns 501 and the client degrades honestly. Wire the token whenever;
the ordering is deliberate, and it is also why an expired token later degrades
instead of breaking.

If a feedback backend is configured, point `ASANA_PROJECT` at a **dedicated
public intake board**, never the internal development board.

## Maintenance

The site is generated from the codebase, so it must redeploy on push or it
becomes the exact artifact this method exists to prevent: an
authoritative-looking page confidently showing UI the product no longer has.

Wire into `prebuild` or CI:

```bash
node scripts/build-search-index.mjs --check   # fails if the index is stale
node scripts/inject-shell.mjs --check         # fails if a frame's shell drifted
node scripts/check-coverage.mjs               # fails if a file has no card
node scripts/lint-showcase.mjs                # fails on silent-render bugs
node scripts/check-public-safety.mjs          # fails on a known leak
```

When a release lands: move shipped roadmap items to `resolved` with their
version, bump `updated` in `roadmap.json`, and re-run the index. A board whose
`updated` is four months old tells the reader more than its contents do.
