# Green Dominance site: how pages are built

Static design prototypes for every page after the landing: shop, joining,
member dashboard, content and admin. One design system, one data set, one
packshot set. No build step: HTML, CSS and ES modules, served as files.

```bash
python3 -m http.server 5200 --directory site
```

- Sitemap (every page, live "Built / Not yet"): http://localhost:5200/
- Design system gallery (every component, light and dark): http://localhost:5200/system/
- Shell demos: `system/demo-member.html`, `system/demo-admin.html`

Read `site/BRIEF.md` first. This file is how to build to it.

## Folders

| Path | What | Owner |
|---|---|---|
| `system/tokens.css` | Every colour, size, radius, shadow, motion value. Light and dark. | foundation |
| `system/base.css` | Reset, type, layout, rounded colour sheets, section heads, rules | foundation |
| `system/components.css` | Every component (see the gallery) | foundation |
| `system/site.js` | Theme, bag, formatters, routes, header and footer, shells, drawer, toasts, dialogs, sponsor check, HTML builders | foundation |
| `system/icons.svg` | One stroke icon sprite | foundation |
| `data/*.js` | Mock data as ES modules (all DUMMY, `isSample`) | foundation |
| `assets/products/` | Packshots for all 25 products, front and three-quarter | foundation |
| `assets/people/`, `assets/herbs/`, `assets/light/` | Story portraits, herb specimens, leaf-light textures | foundation |
| `shop/`, `join/`, `member/`, `content/`, `admin/` | The pages | page sessions |

Need something in `system/` or `data/` that isn't there? Build it in your
own folder (a `page.css` / `page.js` next to your pages) and list it in your
folder's `NOTES.md` for consolidation. Never edit another folder.

## Page template

Copy this. Change the title, the description, `data-area`, and what goes in
`<main>`. Paths assume a page one folder deep (`site/shop/index.html`).

```html
<!doctype html>
<html lang="en-NG">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>The range · Green Dominance</title>
<meta name="description" content="One line for search and sharing.">
<script>(function(){try{var q=new URLSearchParams(location.search).get('theme'),s=localStorage.getItem('gd-theme'),t=q==='light'||q==='dark'?q:s==='light'||s==='dark'?s:(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.dataset.theme=t}catch(e){document.documentElement.dataset.theme='light'}})()</script>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 32 32%27%3E%3Ccircle cx=%276%27 cy=%2716%27 r=%275%27 fill=%27%232e7d32%27/%3E%3Ccircle cx=%2716%27 cy=%2716%27 r=%275%27 fill=%27%23722f37%27/%3E%3Ccircle cx=%2726%27 cy=%2716%27 r=%275%27 fill=%27%23f2c230%27/%3E%3C/svg%3E">
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@62..125,100..900&family=IBM+Plex+Mono:ital,wght@0,500;1,500&display=swap">
<link rel="stylesheet" href="../system/tokens.css">
<link rel="stylesheet" href="../system/base.css">
<link rel="stylesheet" href="../system/components.css">
<link rel="stylesheet" href="page.css"><!-- optional, your folder's own -->
<script type="module" src="page.js"></script><!-- imports ../system/site.js -->
</head>
<body data-area="shop"><!-- shop | join | content: lights up the nav pill -->
<div data-partial="header"></div>
<main id="main" class="page">
  <div class="sheets">
    <section class="sheet sheet--mint">
      <div class="wrap">
        <div class="section-head">
          <p class="eyebrow mono">Shop · 25 products</p>
          <h1 class="title giant">the range<span class="dot-end"></span></h1>
          <div class="aside"><p>One quiet line.</p></div>
        </div>
      </div>
    </section>
  </div>
</main>
<div data-partial="footer"></div>
</body>
</html>
```

`page.js`:

```js
import { ready, productCard, formatNaira, formatPV, bag, toast, href, ROUTES } from '../system/site.js';
import { PRODUCTS, CATEGORIES, query } from '../data/products.js';

await ready;                                  // header, footer and shells are mounted
document.querySelector('[data-grid]').innerHTML = query({ category: 'teas' }).map((p) => productCard(p)).join('');
```

The `<script>` in the head sets `data-theme` before paint (it follows the
system until the visitor picks). **Every page must have it**; the CSS reads
only `[data-theme]`. `?theme=dark` forces a theme for screenshots.

### Dashboard and admin pages

Wrap the page's `<main>` in a shell. `site.js` builds the sidebar, the top
bar, the phone tab bar and the menu sheet around it.

```html
<body>
<div data-shell="member" data-active="earnings" data-title="Earnings">
  <main>
    <div class="page-title"><h1>earnings.</h1><p>Every row opens the order behind it.</p></div>
    ...
  </main>
</div>
</body>
```

`data-active` is the key in `ROUTES.member` (or `ROUTES.admin` with
`data-shell="admin"`): overview, genealogy, team, referral, earnings,
upgradeAccount, upgrade, payouts, orders, reorder, ranks, profile, bank,
security, notifications. Admin: overview, orders, products, members,
enquiries, payouts, compensation, packages, ranks, content, reports,
audit, users. Pages without a nav item (add-member, admin product edit,
member detail) pass the closest parent.

Login, MFA, register and the other join screens are public pages (header
and footer), not shells.

## Routes

Every file name is fixed in `ROUTES` (`system/site.js`) and on the sitemap.
Link with `href(ROUTES.shop.cart)`, never a hand-typed relative path, so
links work from any folder. Query strings carry the item:

| Page | URL |
|---|---|
| Product | `shop/product.html?p=canopy-daily` (`productHref(p)`) |
| Category | `shop/index.html?category=teas` (`categoryHref(id)`) |
| Confirmation | `shop/confirmation.html?ref=GD-10612` |
| Referral landing | `join/invite.html?u=amaka` (stands in for `greendominance.com/amaka`) |
| Register | `join/register.html?ref=amaka` pre-fills the sponsor |
| Genealogy | `member/genealogy.html?u=hauwa` |
| Add member | `member/add-member.html?slot=LRR&parent=kelechi&side=right` |
| Story, article | `content/story.html?s=ngozi`, `content/article.html?a=hibiscus-calyx-to-cup` |
| Admin member, product, order | `admin/member.html?u=amaka`, `admin/product.html?p=canopy-daily`, `admin/order.html?ref=GD-10583` |

## Data

All DUMMY, all marked `isSample`. Import from the module you need:

| Module | Exports |
|---|---|
| `data/products.js` | `PRODUCTS` (25), `CATEGORIES`, `CONTAINERS`, `TINTS`, `byNo`, `bySlug`, `byId`, `inCategory`, `registrationProducts()` (01 and 02 only), `repurchaseProducts()`, `stockState(p)`, `facets()`, `SORTS`, `query({ q, category, format, herb, min, max, inStock, registration, sort })` |
| `data/packages.js` | `PACKAGES` (7 seed), `byLevel`, `activePackages()`, `upgradeQuote(from, to)`, `basketReady(pkg, pv)`, `TO_CONFIRM` |
| `data/plan.js` | `PLAN` (compensation config), `pairingEarning(pv, gen)`, `RANKS` (with prizes), `nextRank`, `EARNING_TYPES`, `HONEST` (the approved honest lines, word for word), `BANNED_WORDS` |
| `data/network.js` | `NETWORK` (the sample binary tree, 23 members, 7 levels), `view(username)` (top, their 2, their 4, with empty slots), `relation(viewer, username)`, `relationLabel`, `downline`, `directs`, `generations`, `lookupSponsor(u)`, `lookupInGenealogy(viewer, u)`, `TODAY` |
| `data/member.js` | The signed-in sample member, Amaka: `ME`, `SUMMARY`, `PV`, `EARNINGS` (each with a link to its order or event; one refund reversal; one held row), `TOTALS`, `UPGRADE_ACCOUNT`, `UPGRADES`, `PAYOUTS` (paid, pending, failed), `BANK`, `ORDERS`, `REPURCHASE`, `RANK_PROGRESS`, `GENEALOGY`, `TEAM`, `DIRECTS`, `GENERATIONS`, `SPONSOR`, `NOTIFICATIONS`, `NOTIFICATION_PREFS`, `SECURITY` |
| `data/admin.js` | `ADMIN_ME`, `ADMINS`, `ROLES`, `PERMISSIONS`, `MEMBERS`, `ORDERS`, `REFUND_REASONS`, `PAYOUT_RUN` (this week, with ready / review / failed lines), `PAST_RUNS`, `COMPENSATION`, `RANK_LIST`, `FULFILMENT`, `AUDIT`, `ENQUIRIES`, `CONTENT_SECTIONS`, `OVERVIEW` |
| `data/content.js` | `STORIES` (8), `ARTICLES` (4), `FAQ_GROUPS`, `PAGES` (placeholders for about, contact, delivery) |
| `data/index.js` | Everything, for pages that want it all |

The numbers agree with each other: leg PV is summed from the tree, earnings
follow the plan (22% on package value for Amaka's Package 4, 4.5% × lesser
leg PV × ₦1,500 for pairing, ₦3,000 / ₦200 for repurchase), payouts are the
weekly sums of earnings, held money is the part above her rate. Don't type
a figure into a page; read it.

Money is whole naira in the mock data (the API will send kobo:
`formatNaira(kobo, { kobo: true })`).

## site.js API

```js
import {
  ready,                                  // Promise: partials and shells mounted
  ROUTES, href, asset, productHref, categoryHref,
  formatNaira, formatPV, formatNumber, formatPercent, formatDate, formatDay, formatDateTime, formatWeek, plural, esc, initials,
  icon, avatar, pv, status, productCard, lineItem, stepper, gnode,
  bag, openBag, closeBag,                 // bag.add(id, qty), set, remove, clear, count(), summary(), items()
  toast,                                  // toast('Saved', { type: 'success' | 'error' | 'info', action: { label, href | onClick } })
  openModal, closeModal, confirmSensitive,
  initTabs, initSponsor, initReveal,
  theme, themeToggle,
} from '../system/site.js';
```

- `formatNaira(22500)` gives `₦22,500`; `formatNaira(-9900)` gives `−₦9,900`; `{ sign: true }` adds `+`. Never "NGN".
- `formatPV(15)` gives `15 PV` (non-breaking space).
- `productCard(p)` is the one product card. Listings, rails, cross-sells: all use it.
- `lineItem(p, qty)` is the one bag / checkout / order row. Put lines inside an element with `data-bag-lines` and the steppers and Remove buttons drive the bag.
- Any `<button data-add="p_01">` adds to the bag and toasts; inside a `[data-buy]` wrapper it reads the quantity from the stepper there.
- `[data-bag-open]` opens the drawer. `document.addEventListener('bag:change', (e) => e.detail)` gives `{ count, subtotal, pv, items }`.
- `confirmSensitive({ title, intro, summary: [[label, valueHTML]], action, tone: 'member' | 'danger', auth: 'password' | 'otp' | null })` resolves `true` / `false`. Use it for payout approval, compensation changes, refunds, suspensions, bank changes, package edits.
- `gnode(node, { you, root, rel })` draws a genealogy node or an empty "+ Add member" slot; see the tree markup in `system/gallery.js` (`R.tree`).
- Sponsor check: the markup in the gallery (section 05) plus `data-sponsor` is wired automatically against everyone; for the dashboard "+", add `data-manual` and call `initSponsor(el, { lookup: (u) => lookupInGenealogy(ME.username, u) })`.
- Tabs: `role="tablist"` / `role="tab"` (with `aria-controls` for panels) are wired with arrow keys; listen for `tabs:change`.
- `.reveal` on an element makes it rise in as it scrolls into view; reduced motion shows it at once.

## Design rules

The bar is The Bloom homepage (branch `claude/hero-a-bloom-b4e5f3`) and
Samuel's live sites. He has rejected template work twice.

1. **One idea per screen.** A giant lowercase head that ends in a full
   stop (`the range.`, `earnings.`), one quiet line, one action. The yellow
   full stop (`.dot-end`) is the spark, used once per page at most.
2. **Colour comes in rounded sheets** (`.sheets` > `.sheet--*`), each
   section its own tone. Every public page has a dark moment (`--deep`), a
   loud one (`--lime` or `--green`), a burgundy one where membership
   appears, soft panels (`--mint`, `--cream`, `--blush`) and visible yellow
   sparks. Green means Shop, burgundy means Member. The dashboard and admin
   are calmer (white page, panels) but use sheets for the moment that
   matters (today's earnings, the payout run waiting).
3. **Never:** icon grids, three-up feature cards, count-ups, stock-photo
   heroes, headline plus two buttons, grey card farms, charts for the sake
   of charts. Figures are type (`.figure`), comparisons are one bar
   (`.legs`, `.meter`), lists are ruled (`.ruled-list`, `.ledger`,
   `.facts`), not boxed.
4. **Type through tokens.** Archivo for everything, IBM Plex Mono 500 for
   labels, PV, references and captions, never prices. No raw sizes or hex
   in pages: use the tokens and classes.
5. **Mobile is half the job.** Design at 390 × 844 as its own composition,
   check 360, no sideways scroll, thumbs reach the main action, 44px
   targets. Tables use `.table--stack` with `data-label` on every cell.
6. **Light and dark** on every page; check both (`?theme=dark`).
7. **States:** every list has empty, loading (`.skel`), error and success.
   Say what happened and what to do, in one line.
8. **Motion means something:** a bottle turns on hover, a sheet rises, a
   meter fills, the bag badge bumps. Use the easing and duration tokens.
   Respect reduced motion (the tokens already do).
9. **Accessibility:** real headings in order, labels on every field, alt
   text on every product (the builders do this), visible focus, AA
   contrast. Errors in words with an icon, never colour alone.
10. **Text contrast.** Words use `--fg`, `--fg-muted` and `--fg-faint`
    (`.faint`). All three pass 4.5:1 on the page and on every sheet,
    because each sheet resets them for itself (gallery section 12 shows the
    measured figures). `--ink`, `--ink-muted` and `--ink-faint` are the
    theme's own ink: use them only on a surface that paints its own theme
    colour (a product card does this already), never on a sheet. Never
    fade words with `opacity` or an `rgba` of your own: pick the token
    instead. A pending or disabled look is a dashed outline or a pattern,
    not a faded card.

## Content rules (every page)

- **No health claims.** Product benefits are `[Approved benefit: EPS]` or
  the plain DUMMY lines. Check copy against `BANNED_WORDS`.
- **No income promises.** "No product sale, no earning. Joining alone earns
  nothing." The disclaimer (`HONEST.disclaimer`) goes under every button
  that starts registration or payment with a package, and on the dashboard.
  No cars, cash or "financial freedom" on public pages. **Rank prizes
  appear only inside the dashboard and admin.**
- **No facts EPS hasn't given.** Use `[placeholders]` styled with `.ph`:
  delivery coverage, support channel, contact, about story, refund policy,
  package 5's value, rank PV targets, payout day.
- **Samples:** "Products and people shown are samples." (`.sample-note`)
  wherever products or people appear. Story captions read `NGOZI · CUSTOMER`;
  never show a story's state.
- **Money:** `formatNaira` and `formatPV` only. No withdraw button anywhere.
  Payouts are weekly, approved by EPS, sent by Paystack to a verified bank in
  the member's own name. Paid, pending and failed look different.

## Product images

`assets/products/NN-front.webp` and `NN-angle.webp` (1200 × 1500,
transparent), plus `-sm` versions at 600 × 750 for cards and rows. Every
product is framed at the same scale on the same baseline, so they sit
together on a shelf. Read paths from `p.images`; never hardcode. See
`assets/products/README.md` for how they were made and rebuilt.

## Checking a page

1. Serve `site/` on your own port (`python3 -m http.server 52xx --directory site`).
2. 1440, 390 and 360 wide; light and dark (`?theme=dark`).
3. No console errors, no 404s, no sideways scroll.
4. Commit only your folder: `git add site/<yours>` then
   `git commit -m "..." -- site/<yours>`. Never switch branches, stash,
   reset, rebase or clean. No AI attribution anywhere.
