# มาตรฐานกลาง — วางข้อความนี้ก่อนใช้ skill ใดๆ ในแชท

> ทุกอย่างที่ทำต่อจากนี้ ต้องเป็นไปตามมาตรฐานด้านล่าง ห้ามใช้ความเห็นตัวเองแทน


---

# DESIGN SYSTEM — locked-in visual rules

> Written up front so everyone (owner - AI - dev) produces the same look.
> Every skill in phases `3-design` and `4-prototype` must reference this file — no re-litigating it every time.

---

## 0. Three guiding principles

1. **No thinking about where to click** — the most important thing on screen must be the most prominent, and there must be only one.
2. **Never tell the user what they already know** — don't show a user their own name on their own card, don't put an "my task" label on a page that's already filtered to them.
3. **Truncated data must show the real count** — show `5 / 18`, not `5`, because a missing number looks like a bug.

---

## 1. Standard stack

| Level | Use |
|---|---|
| Prototype (owner builds it) | Single HTML file + CDN — opens straight in a browser, nothing to install |
| Real system (dev builds it) | Next.js + Tailwind v4 + shadcn/ui + Lucide icons + TypeScript strict |

### Standard CDN block for prototypes (copy as-is)

```html
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://unpkg.com/lucide@latest"></script>
```
Call icons with `<i data-lucide="shopping-cart"></i>` and close the file with `lucide.createIcons()`.

**No web font is loaded.** Type comes from fonts already on the machine — nothing to download, nothing to
wait for, no flash of unstyled text, and one less external dependency. Set this once, in `shared/style.css`:

```css
body { font-family: "Segoe UI", "Segoe Sans", "Helvetica Neue", Helvetica, Arial, "Leelawadee UI", "Noto Sans Thai", "Sukhumvit Set", sans-serif; }
```

Use the CDN so mocks are fast — open the file and see it immediately, no build, nothing to install.
**Prototype only** — the real system installs everything as normal packages.

---

## 1b. No emoji, ever

**No emoji anywhere** — on screen, in code, in comments, in delivered documents, in file names, **and in chat replies to the user** (questions, progress updates, every message).
Reason: emoji render inconsistently across devices, can't be read aloud by screen readers, don't turn up in search, and look unprofessional.

Use this instead:

| Instead of | Use |
|---|---|
| On-screen icon | Lucide icon |
| Status badge | Text + color + Lucide icon |
| Editable/locked marker in code | the literal words `แก้ได้:` and `ห้ามแก้:` |
| Document heading | plain text |

---

## 1c. Forbidden — the tells that scream "an AI made this"

AI is trained on a massive volume of work, so what it picks by default is **the statistical average of everything on the internet**.
The result looks familiar but has no identity. Clients see it and feel "this isn't us."
Everything below is **forbidden** unless the user explicitly asks for it.

### Color
- **No default Tailwind purple/indigo** — `#6366f1` (indigo-500), `#8b5cf6` (violet-500), and nearby shades.
- **No purple-to-blue gradients**, and no multi-stop neon gradients.
- Follow section 2: **one dark primary, one neutral, one accent** only. Use the accent sparingly.
- The neutral must not be an exact mid-gray — tint it slightly toward the accent, so it reads as chosen rather than default.

### Typography
- **No Inter or Roboto as the default** — the fonts an AI reaches for most, to the point they've become a tell.
- Headings and body text should differ clearly — in weight, size, or an entirely different typeface.
- Thai fonts per section 4.

### Layout
- **Do not stuff everything into cards** — a page of identical rounded cards lined up in a row is the clearest tell of all.
  Group content with whitespace, type scale, and subtle background-color shifts instead.
- **Do not default to the canned sequence** Hero -> three feature cards -> pricing -> FAQ
  unless that sequence actually matches the user's real task.
- No subheadings that add no new information, no tabs that exist just to look sophisticated.
- No heavy rounded corners (see section 3), no glassmorphism, no floating 3D blobs, no glow shadows.

### Imagery and icons
- **No generic 3D renders, no stock photos of smiling office people.**
- Icons: Lucide only (single-weight line icons, consistent) — no emoji, per section 1b.
- If there's no real image available, **leave it out** — empty space beats a meaningless picture.

### Copy
- **No invented numbers** like "10x faster" or "saves 80% of your time" unless the user actually supplied that figure.
- No floating marketing phrases — "revolutionary," "cutting-edge," "all-in-one solution."
- Write the way people actually talk — say what it does, not how great it is.

> **Self-check:** if you could strip the system's name off this screen and drop it into a completely different business without it feeling odd,
> it has no identity yet. Go back to section 1d.

---

## 1d. Brand direction — ask when you're about to apply color, not at the start

**Do not ask about color or style at the start of the conversation.** Three reasons:
1. Most people commissioning work can't describe taste in the abstract, but can critique it instantly once they see something real.
2. At the wireframe stage (level 1) color isn't applied yet anyway, so an early answer would just sit unused.
3. Color lives in a single file, `shared/style.css`, so changing it later is cheap — the cost of a wrong guess is low.

**Ask exactly once, right as you're about to move to level 2** (the point where color is actually applied). Ask with a default already stated,
so a one-word answer is enough:

> "กำลังจะใส่สีแล้ว มีโลโก้หรือสีของบริษัทที่อยากให้ใช้ไหม
> ถ้าไม่มี จะทำแบบสะอาดๆ อ่านง่ายให้ก่อน เปลี่ยนทีหลังได้"

**If existing brand material exists** (logo - website - page - an existing system - a business card), pull the primary color, secondary color, and personality from there.
**Do not invent one** — this is the highest-value information you can get, and it's free from a single question.

**If none exists, or the user says "whatever you think"** -> default to **"clean, easy to use"** and say directly that this is the default and why.
**Never go silent and guess** — a silent guess lands on the statistical average, which is exactly what section 1c forbids.

**If the user states a direction at any point** (e.g. "อยากได้ดูทางการหน่อย" — I want it to look more formal), use it immediately, don't ask again.

### Selectable personas — offer these when the user wants to specify one

| Persona | What it looks like | Fits |
|---|---|---|
| ทางการ น่าเชื่อถือ (formal, trustworthy) | Composed dark colors, crisp type, tight spacing, dense information | Finance, government, internal enterprise systems |
| สะอาด ใช้งานง่าย (clean, easy to use) | Neutral-led palette, a single accent, generous whitespace | Back-office systems, daily-use tools |
| อบอุ่น เป็นกันเอง (warm, friendly) | Warm colors, slightly softened corners, conversational copy | Shops, customer service, small teams |
| จริงจัง เน้นข้อมูล (serious, data-forward) | Numbers foregrounded, dense tables, minimal color used only for status | Reports, dashboards, analytics systems |

Record the resulting direction in `HISTORY.md` (and in `BRIEF.md` if the user gave it during the initial conversation)
so every screen, and any dev who takes over later, uses the same set — no need to ask again.

---

## 2. Color

Use **roles**, not color names — this way a theme swap never breaks anything.

| Role | Use when | Tailwind |
|---|---|---|
| `primary` | The one main button per screen | `bg-primary text-primary-foreground` |
| `secondary` | Secondary buttons, filters | `bg-secondary` |
| `muted` | Supporting text, dates, descriptions | `text-muted-foreground` |
| `destructive` | Delete, cancel, danger warnings | `bg-destructive` |
| `success` | Succeeded, paid, passed | green |
| `warning` | Due soon, pending | yellow/orange |

**Rule:** a color means one thing system-wide — if green = success, no screen anywhere may use green to mean "pending."
**Never** let color be the only signal for status — always pair it with text or an icon (so colorblind users can read it too).

---

## 3. Spacing and sizing

- Spacing uses only the step scale `4 - 8 - 12 - 16 - 24 - 32 - 48` px. No odd values like 13px.
- Main content width caps at `1280px`, centered.
- **Corner radius: avoid it** — sharp corners (`rounded-none`) are the default; the most rounding allowed is `rounded-sm` (2px).
  Never use `rounded-lg`, `rounded-xl`, `rounded-full` — heavy rounding reads as toy-like and eats real content space.
  One exception only: profile pictures may be circular.
- Shadows: very subtle (`shadow-sm`) or none at all — use borders instead. Heavy shadows read as dated.

---

## 4. Typography

| Where | Size | Weight |
|---|---|---|
| Page heading | 24-30px | 600 |
| Subheading / card heading | 16-18px | 600 |
| Body text | 14-16px | 400 |
| Supporting text | 12-13px | 400 + `muted` |
| Money / totals | 18-24px | 600 + **tabular-nums** |

- **Use the system font stack in section 1 — do not load a web font.**
  A font stack resolves **per character, not per block**: Latin picks the first font that has the glyph
  (Segoe UI, so it reads like a Microsoft product), and Thai skips past the Latin-only faces to the first
  Thai face available (Leelawadee UI on Windows, Noto Sans Thai or Sukhumvit Set on macOS).
- **Never end a stack at bare `sans-serif` with no Thai face before it** — Helvetica and Arial carry no Thai
  glyphs, so the browser would substitute whatever it finds, and the result differs on every machine.
- **Every number everywhere uses `tabular-nums`** — otherwise digits jitter inside tables.
- No text smaller than 12px anywhere.

---

## 5. On-screen language

- **All Thai**, except technical terms that have no good Thai equivalent.
- Buttons are written as a **verb naming the outcome**: `บันทึกใบสั่งซื้อ` (save the purchase order), not `ตกลง` (OK) / `Submit`.
- Dates display in Thai format `20 ส.ค. 2569` (stored in the system as `YYYY-MM-DD`, Gregorian, always).
- Money displays as `฿1,250.00`, always with thousands separators.
- Error messages must say **what to do next**, not just that something broke.
  Bad: `เกิดข้อผิดพลาด` (an error occurred). Good: `บันทึกไม่สำเร็จ เพราะยังไม่ได้เลือกลูกค้า — เลือกลูกค้าแล้วกดบันทึกอีกครั้ง` (save failed because no customer is selected — select a customer and save again).

---

## 6. All 5 states must exist (the most commonly forgotten rule)

Every screen that fetches data must design for all five:

| State | Must show |
|---|---|
| Loading | A faint skeleton — not a blank white screen, not a spinner centered on the page |
| Has data | Normal state |
| Truly empty | Why it's empty, plus one clear starting action |
| Search found nothing | Different from "truly empty" — must include a clear-filters button |
| Error | The cause, plus a retry button |

---

## 7. Mobile first

- Design at **375px width first**, then scale up.
- Tap targets (buttons, links, checkboxes) must be at least **44 x 44px**.
- Tables on mobile **must never scroll horizontally** — convert to cards instead.
- The primary mobile button sits fixed to the bottom edge, reachable by thumb.
- Breakpoints: `sm 640 - md 768 - lg 1024 - xl 1280`.

---

## 8. Forms

- Field labels sit **above the field**, always — never use a placeholder as the label.
- Required fields are marked `*`, with a note at the top stating `* คือช่องที่ต้องกรอก` (* marks required fields).
- Validate **on blur**, not on every keystroke.
- Error text sits **directly under that field**, never grouped at the top.
- The save button must not be clickable again while a save is in progress.

---

## 9. Tables and lists

- Include sort, search, and pagination once data exceeds 20 rows.
- Numbers align right - text aligns left - dates align left.
- The most important column sits leftmost; action buttons sit rightmost.
- The whole row is clickable, not just a small link inside it.
- If display is truncated, show the true total (`แสดง 5 จาก 18 รายการ` — showing 5 of 18).

### The standard four-band table screen

This layout is settled. Do not reinvent it per screen — a table screen that looks different from the
others is a bug, not a style choice.

```
+---------------------------------------------+
| title band          heading + main action    |  fixed
+---------------------------------------------+
| filter band         search + filters + clear |  fixed
| แสดง 24 / 156 รายการ                          |
+---------------------------------------------+
|                                             |
| table               header row stays put  |^||  scrolls
|                                           |v||
+---------------------------------------------+
| footer band         หน้า 2 / 7   ต่อหน้า  < >  |  fixed
+---------------------------------------------+
```

Only the table body scrolls, per section 12b. The table's own header row stays visible while its rows scroll.

**Filter bar — fixed order, left to right:** search (takes the remaining width) · date range · then the
category filters. Every control is a column: a small muted label above, control height 40px.
On a narrow screen each filter becomes full width and stacks, with search still first.

- The "no filter" choice is a **real option labelled `ทั้งหมด`**, not an empty value.
- A `ล้างตัวกรอง` button appears **only when at least one filter is active**, and clears the search box too.
- Changing any filter **resets to page 1**. Forgetting this strands the user on an empty page 5.
- Filter and page state belong in the URL (section 11c) so a filtered view can be shared and the back button works.

**Count line** — `แสดง 24 / 156 รายการ` sits directly under the filter bar. State it **once**; never repeat it in the footer.

**Pagination** sits in the fixed footer: `หน้า 2 / 7` on the left with a per-page selector (`20 / 50 / 100 / 300`),
previous and next on the right, disabled at the bounds. **No numbered page buttons** — they add width and nobody uses them.

**Two different loading states, and they are not interchangeable:**

| Situation | Show |
|---|---|
| First load, nothing on screen yet | Skeleton rows shaped like the real rows (section 13) |
| Re-fetching with rows already visible (filter or page changed) | **Keep the existing rows**, dim them, and float a small `กำลังโหลด…` pill over them |

Blanking a populated table back to skeletons on every filter change makes the screen flash and loses the
user's place.

**Empty and error states render inside the same table frame**, so the box does not collapse and the filter bar
stays reachable to undo whatever emptied it. Distinguish "no data at all" from "nothing matched your filters" —
the second one needs the clear-filters button (section 6).

**Sorting caveat that bites people:** if the table is paginated server-side, sorting in the browser only sorts
the current page, which looks like a bug to the user. Either sort on the server, or do not offer sorting on a
paginated table.

**Every interactive element carries a `data-testid`** (section 11c), and every column header that is not
self-explanatory carries a tooltip.

---

## 10. Accessibility (minimum bar)

- Text-to-background contrast ratio >= 4.5:1.
- Everything reachable via Tab, with a clearly visible focus ring.
- Images need alt text - clickable icons need an accessible name.
- Support dark mode: define all colors as variables, never hardcode `#fff`.

---

## 11. Motion

- Fast: 150-250ms, `ease-out`, animate only opacity and transform.
- No bounce, no spin, nothing longer than 300ms.
- Respect `prefers-reduced-motion` — if the user has it off, it must actually be off.

---

## 11b. Fill and balance the screen (the most commonly missed thing)

The most common failure: **short content huddled in the top-left corner, half the screen left empty.**
The user sees this and feels "this isn't finished" even though every function is there.

| Content type | How to lay it out |
|---|---|
| Short form - login page - single-search page - single-result page | **Center both horizontally and vertically**, max width `420-520px` |
| Long content - tables - dashboards | Anchor to top, max width `1280px`, centered |
| Two-part content (form + results) | Separate boxes, never stuffed into one card - side by side on wide screens, stacked on narrow ones |

**Instantly checkable rule:** open the page at 1280x800 — **if the bottom half is empty for no reason, it hasn't passed.**
The fix is centering, not force-stretching content to fill the screen.

The correct technique — make the outer container screen-height and center inside it. Works at any screen height without guessing percentages:

```css
.short-content-page {
  min-height: 100dvh;          /* 100dvh, not 100vh — on mobile the browser chrome eats space */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.short-content-page > * { width: 100%; max-width: 480px; }
```

> **Never set a fixed percentage top offset** (e.g. `padding-top: 12%`).
> The taller the screen, the more empty space it leaves at the bottom — this is the single most common cause of the "content stuck at the top" symptom.
> If content exceeds screen height, `justify-content: center` will not clip it — it simply scrolls normally.

**Never show a result before the user has acted.** The result area must be empty or absent until the button is pressed.
A status box that appears the moment the page loads makes the user unsure whether they need to click something.

**External-facing screens must never link to internal screens** — no link, no menu item, no back button that leads there.
Outsiders shouldn't even know an internal side exists. Check this every time a screen serves a different user group than the rest of the app.

---

## 11c. Element reference IDs, and state in the URL

### `data-testid` — required on everything clickable or editable

So anyone can point at exactly "fix this" without describing a location in words.
Works both when a user right-clicks Inspect and copies it over, and when writing automated tests.

```html
<button data-testid="candidate-list.row.open" ...>
<input  data-testid="check-result.email" ...>
<div    data-testid="check-result.status" ...>
```

Format: **`{screen}.{section}.{thing}`**, English, kebab-case, dot-separated.

- Required on: buttons - input fields - links - table rows - tabs - result boxes - status boxes - error messages.
- **Do not change an existing value without a real reason** — tests and notes referencing it break along with it.
- Never use a framework's random auto-generated `id` (`el-8f3a`) as a reference — it changes on every render.

### Important state must live in the URL

Users must be able to **copy the link, send it to someone else, and have it open to the same view**, and the browser back button must work.

| Must be on the URL | Example |
|---|---|
| Which screen/tab is open | `?tab=round3` |
| The selected item | `?id=RC-2026-001` |
| Filters, search terms, sort order | `?status=waiting&q=สมชาย&sort=-created_at` |
| Which page number | `?page=2` |

- In-page state changes should use a technique that **doesn't reload the page**, but the URL must still update to match (see section 13).
- Opening a URL with parameters directly must restore that exact state, not bounce back to defaults.
- **Never put secrets or personal data in the URL** (passwords, ID numbers, tokens) — it persists in browser history and server logs.

---

## 12. The whole-app shell

Screens don't float independently — every screen shares one shell, or the user feels lost.

```
┌──────────────────────────────────┐
│ Top bar (navbar)   system name - menu - user │  <- always present, never disappears, never reloads
├──────────────────────────────────┤
│                                  │
│ Content (main)                    │  <- the only part that changes per screen
│                                  │
├──────────────────────────────────┤
│ Bottom bar (footer)  company name - version  │  <- optional; drop it if there's nothing to put there
└──────────────────────────────────┘
```

**The navbar must carry a readable system name — but do not ask for it up front.**
Same reasoning as the brand question in section 1d: before the user has seen anything, "what should we call it?" gets "ไม่รู้ แล้วแต่เลย" and costs a question for nothing.

- **At level 1**, derive a sensible name from what the work is about and mark it as provisional, e.g. `ระบบบันทึกสต็อกวัตถุดิบ (ชื่อชั่วคราว)`. Say once that the name is a placeholder and can be changed any time.
- **Ask for the real name only at the level 2 brand moment**, folded into the same question as the logo/colour one — one turn, not two.
- If the user names it themselves at any point, use it immediately and stop treating it as provisional.
- Never leave `{system-name}` literally in place, and never leave it blank — the user will think the system is broken.

### Two things that make the top bar "look inconsistent" even though it's the same code

**1. Always reserve space for the scrollbar**

A short-content page has no scrollbar; a long-content page does — a scrollbar takes up roughly 15px of width.
This means **the two top bars shift by different amounts**, and switching between pages makes the menu visibly jump.
The eye catches that something's off but can't say what.

```css
html { scrollbar-gutter: stable; }
```

Set this once, in the shared stylesheet — it fixes the whole system. **Never fix it by hiding the scrollbar.**

**2. Always show which page you're on**

The current page's menu item must be visibly more prominent than the others (darker color + bold + `aria-current="page"`).
If every menu item looks identical, the user has no idea where they are in the system.
And **never use color alone** as the signal — pair it with font weight or an underline too (section 2).

### Mobile top bar — choose based on menu count

| Number of top-level destinations | Use | Why |
|---|---|---|
| 5 or fewer | **Bottom tab bar, fixed to the screen edge** | Thumb-reachable, always visible, one tap to switch |
| More than 5 | **Hamburger button opening a side drawer** | Fits more items without crowding the screen |

Never use a hamburger when there are only 3 menu items — hiding things that should be visible reduces engagement for no reason.
An open drawer must be closable via its X button, tapping the dimmed area outside it, and the Esc key.

---

## 12b. Only the content region scrolls — the frame stays put

The page itself must never be the scroll container. If the whole document scrolls, the top bar and the
footer scroll away with the content, the browser's own scrollbar runs the full height of the window, and
the user loses the frame they navigate by.

```
+-----------------------------+
|  HEADER                     |  fixed — never scrolls
+-----------------------------+
|                          |^||
|  CONTENT                 ||||  this region scrolls, and only this region
|                          |v||
+-----------------------------+
|  FOOTER                     |  fixed — never scrolls
+-----------------------------+
```

```css
.app-shell  { height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
.app-header,
.app-footer { flex: none; }
.app-main   { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
```

> **`min-height: 0` is load-bearing — do not remove it.** A flex child defaults to `min-height: auto`,
> which refuses to shrink below its content. Without it the middle region grows to fit its content, the
> shell overflows, and the layout silently degrades back into a normal scrolling page that merely looks
> correct until the content gets long. This is the single most common way a fixed layout breaks.

**Two layout modes, chosen by what the page is:**

| Page type | Mode |
|---|---|
| Table, list, dashboard — the frame matters and the data is long | Fixed viewport, as above |
| Form, composer, document — the user works top to bottom | Let the whole page scroll normally |

Do not force a form into a fixed frame. A long form in a 60%-height scroll box is worse than a page that scrolls.

- `100dvh`, not `100vh` — on mobile the browser chrome eats the difference and the footer ends up off-screen.
- `overflow: hidden` on the shell is what stops the document itself from scrolling.
- `overscroll-behavior: contain` stops a scroll that reaches the end of the content from dragging the page behind it.
- A fixed footer must not cover content: the scrolling region already ends above it, so no bottom padding hack is needed.
- The mobile bottom tab bar (section 12) is part of the fixed frame, not part of the scrolling region.

**The same three-part structure applies to any bounded box that can overflow** — dialogs, drawers, side panels,
pickers, long filter lists. Header and footer stay, the middle scrolls, and that box's scroll must not leak
into the page behind it.

---

## 12c. Dialogs

**Structure is always header / scrolling body / fixed footer**, per section 12b. The action buttons must stay
visible no matter how long the content is — a user should never have to scroll to find the confirm button.

**Size is fixed and chosen up front, not grown from the content.** A dialog that resizes as its content loads
jumps under the cursor.

The size scale pairs a width with a height, so the frame is stable regardless of content:

| Size | Width | Height | Use for |
|---|---|---|---|
| `sm` | 448px | 60dvh | Confirmations, single-field prompts |
| `md` | 512px | 70dvh | Ordinary forms |
| `lg` | 672px | 75dvh | Forms with several columns |
| `xl` | 896px | 80dvh | Detail views, side-by-side comparison |
| `2xl` | 1024px | 85dvh | A table inside a dialog |

- Width is `min(<size>, calc(100vw - 32px))` so it never exceeds the viewport.
- Height never exceeds `85dvh`; the body scrolls, the dialog frame never does.
- **Pick a size from the scale.** An ad-hoc `max-width: 1100px` is how a set of dialogs stops looking like a set.
- Below 640px viewport width a dialog becomes full-screen — a centred box on a phone wastes the screen.
- Close via the X button, Escape, and clicking the backdrop. A dialog with unsaved changes asks before closing.
- Focus moves into the dialog on open and returns to the trigger on close. Tab stays trapped inside while open.

## 12d. Confirming something destructive

Match the friction to the damage. Too much friction on a trivial action trains people to click through
everything without reading, which is worse than no confirmation at all.

| How bad if it happens by mistake | What is required |
|---|---|
| Reversible, affects one item | A plain confirm dialog. Say what will happen in one sentence. |
| Hard to reverse, or affects many items | A **checkbox** the user must tick, whose label states the actual consequence. The confirm button stays disabled until it is ticked. |
| Irreversible, or touches real customer data | The user must **type the exact name of the thing** into a field before the confirm button enables. |

```
ลบผู้สมัคร 24 คนที่ค้างเกิน 90 วัน

[ ] เข้าใจแล้วว่าข้อมูลคะแนนและความเห็นของผู้ตรวจทั้งหมดจะหายถาวร กู้คืนไม่ได้

พิมพ์คำว่า  ลบผู้สมัคร 24 คน  เพื่อยืนยัน
[____________________________]

                    [ ยกเลิก ]  [ ลบถาวร ]
```

- The confirm button says what it does — `ลบถาวร`, not `ตกลง`. `ยกเลิก` sits on the left and is the default focus.
- The checkbox label must state the consequence, never just "ฉันเข้าใจ".
- The string to type is the item's real name or count, shown right above the field so it can be read and typed.
- **Never pre-tick the checkbox and never pre-fill the field.**

---

## 12e. Telling the user what happened

Every action the user takes must produce visible feedback. Silence reads as "it didn't work" and makes people
click twice. But the feedback has to match the message — a toast is the wrong place for something the user
must act on, because it disappears.

| What happened | Show it as |
|---|---|
| A field is invalid | **Inline, under that field.** Never a toast — the user cannot tell which field a toast is about. |
| Their action succeeded and needs no response | **Toast**, auto-dismiss after 3 seconds |
| Their action failed and they must do something | **Inline banner in the content area**, stays until resolved |
| Loading the screen failed | **In place of the content** (section 6), with a retry button |
| They must decide before anything continues | **Dialog** (section 12c) |

### Toast rules

- Bottom-right on desktop. On mobile, **above** the bottom tab bar, not over it.
- Success auto-dismisses after 3 seconds. **An error toast does not auto-dismiss** — it stays until dismissed.
- At most 3 at once; beyond that, replace rather than stack a wall of them.
- **A toast is confirmation, never the only evidence.** The screen itself must already show the change —
  the row appears, the number updates, the status flips. If the toast is the only proof anything happened,
  the screen is wrong.
- **Never put anything the user needs later in a toast** — an ID, a code, an error detail they must report.
  It vanishes and cannot be recovered.
- Announce it to screen readers (`role="status"` for success, `role="alert"` for errors).
- Write what happened, not that something happened: `บันทึกใบสมัครแล้ว` — not `สำเร็จ`.

### Prefer undo over asking first

For anything **reversible**, do it immediately and offer an undo in the toast, rather than blocking with a
confirm dialog:

```
ย้ายผู้สมัคร 3 คนไปรอบ 2 แล้ว                    [ ยกเลิกการย้าย ]
```

This is faster for the common case (the user meant it) and safer for the rare one (they get it back with one
click). Reserve the confirm dialogs in section 12d for things that genuinely cannot be undone. A confirm dialog
on a reversible action is friction that trains people to click through without reading.

The undo must stay available for at least 8 seconds — longer than a normal toast, because the user has to
notice the mistake first.

---

## 13. Loading and skeletons — never make it feel like an F5 refresh

### What reloads, what stays put

| Part | On first open | On button click / page change |
|---|---|---|
| Top bar - bottom bar - menu | Appears instantly, **no skeleton, no spinner** | **Never flickers, never disappears** — stays put |
| Content (main) | Skeleton per the rules below | Only this part changes |

**The principle:** the shell comes from local values already in hand — it isn't waiting on anything, so there's no reason to show a loading state for it.
Only the content is actually waiting on data — show a loading state there only.

**Never show a blank white screen and reload everything on every button click** — the user will feel like they hit F5, and lose their reading position.
A click should only change the content; the top bar stays, and scroll position is preserved if the content is still the same set.

> Acceptable exception: navigating **between separate screens** in a prototype built as separate files counts as a page change.
> But set the background color in the CSS at the very top of the file, so there's no flash of white during the switch.

### Skeleton rules

The rule depends on whether this is the **first sight** of the screen or a return to it.

| Situation | What to show |
|---|---|
| **First open of a screen in this session** — nothing on screen yet | **Always show the skeleton, and hold it for at least 500ms**, even if the data was instant |
| Returning to a screen whose data is already in hand | **No skeleton.** Show the content immediately |
| Re-fetching while rows are already visible (filter or page changed) | Keep the rows, dim them, float a small loading pill (section 9) |

- **The 500ms floor on first open is deliberate — never optimise it away.** With local mock data everything
  resolves instantly, so without a floor the screen snaps from blank to full and the eye cannot follow it.
  The floor is what makes the screen feel like it loaded rather than flickered.
- **Never show a skeleton on a return visit.** The data is already there; a skeleton is pure delay.
- A skeleton that appears for 100ms and vanishes reads as a glitch — that is what the floor prevents.
- The skeleton **must match the real shape** — same row count, column count, and heights —
  so content doesn't jump once loading finishes.
- **No spinner centered on the page** as the primary approach — reserve spinners for a button that's actively working (e.g. saving).
- On first open with no data yet -> skeleton first, then the "no data" state per section 6.

### Splash screen

**The default is: no splash screen.** It's dead time the user gets nothing back for.

Only add one when there's a genuine startup step that takes longer than 1.5 seconds (e.g. checking user permissions, loading initial config), and:
- Show only the system name and mark, on the app's background color.
- **Disappear the instant it's ready — no artificial delay.** Never exceed 2 seconds under any circumstance.
- Never use it as ad space or for a long animation.

---

## 14. Checklist before calling a screen done

- [ ] Opens at 375px with nothing overflowing the edge
- [ ] Has one clearly dominant primary button per screen
- [ ] All 5 states present (section 6)
- [ ] All Thai on screen, buttons written as outcome verbs
- [ ] Numbers use tabular-nums and thousands separators
- [ ] Tab key reaches everything, focus is visible
- [ ] No redundant information the user already knows
- [ ] Everything clickable/editable has a `data-testid` per the format in 11c
- [ ] Important state lives in the URL - copying the link and reopening it shows the same view - browser back works
- [ ] **Actually seen it rendered** (opened it / screenshotted it) — not verified from code alone

### How much to check, and when — spend the effort where things actually break

Checking costs time. Checking everything at every level makes the work crawl; checking nothing ships broken work.
**Spend the budget on mobile and on the main path — that is where breakage actually happens.**

| Situation | Check this much | Do NOT do this |
|---|---|---|
| Level 1-3 finished | **One representative screen**, viewed in the browser at 375 and at 1280 | Do not look at every screen. Screenshots taken to inspect are scratch — do not keep them in `screenshots/`; only level 4 keeps files there. |
| An edit was requested | **Only the screen that changed** | Do not re-check screens you did not touch |
| **Level 4** | **Every screen on the main path**, at 375 and at 1280, screenshots saved into `screenshots/` | Do not skip 375 — it is the width that breaks. Do not screenshot screens that are not on the main path. |
| Nothing changed since the last check | Skip it entirely | Do not re-verify just to look thorough |

Rules that keep this honest:
- **375 is never optional at level 4.** Desktop-only evidence is not evidence — mobile is where layout actually fails.
- If some screens were deliberately not checked, **say which ones and why** in the level report. Silence reads as "all of them were checked".
- If the environment cannot open a browser, **say plainly that visual verification was not possible**, list what the user must look at themselves, and do not write "tested" anywhere.
- A screen that has never been viewed at any width cannot be part of a level that is declared passed.

### Visually verify at 1280x800 — look at the render and answer every item

Code can't catch these — you must look at the rendered page. **Any "no" answer = not done.**

- [ ] Content is balanced across the whole screen — **the bottom half isn't empty for no reason** (11b)
- [ ] Short content is centered, not huddled in the top-left corner
- [ ] The app shell's top bar is present — not a floating heading on an empty page (section 12)
- [ ] The input area and the result area are visually distinct, not merged into one box
- [ ] Before any button is pressed, no result has appeared yet
- [ ] External-facing screens have no link or menu leading to the internal side
- [ ] Strip the system's name and it still reads as this specific business, not a generic template (section 1c)

### Then repeat at 375x812

- [ ] No horizontal scrolling - tables become cards
- [ ] Mobile menu matches the destination-count rule (section 12), thumb-reachable
- [ ] The top bar doesn't eat so much space that content has barely any room left


---

# HANDOFF PROTOCOL — the contract for passing work between "owner" and "developer"

> This is the heart of the whole system. Every skill in this pipeline exists to make this two-way handoff smooth.

---

## The problem this solves

**The old way:** the owner has a picture in their head -> can't describe it well -> waits on UX/UI design -> waits on a dev to build a prototype -> sees it, it's not right -> loops back. Weeks lost.

**The new way:** the owner can vibe it themselves, see it working within the hour, edit it themselves.
But the **backend** (database, API, security, going live) still needs a developer.

So this system doesn't remove the developer — it makes **the line clear**, and **standardizes what crosses that line**.

---

## Where the responsibility line sits

| Owner side (can do it themselves) | Developer side (must be handed off) |
|---|---|
| Look, button placement, colors, copy | Connecting a real database |
| Screen order, usage flow | API / integrating with other systems |
| Sample (mock) data | Login and user permissions |
| Business rules ("turn red if over 30 days") | Security - real customer data |
| Trying out adding/moving/removing a button | Going live (deploy) - backups |

**Hard rule:** the owner can edit anything **visible** - the dev owns everything **invisible**.

---

## Direction 1 — Owner -> Developer

Invoked via the `/app-send` skill. The output is a zipped **Handoff Package**.

```
handoff-{project-name}-{YYYY-MM-DD}.zip
├── README-สำหรับนักพัฒนา.md   <- read this first — summarizes what to do next
├── HISTORY.md                  <- the decision trail: what changed, why, who asked
├── prototype/                  <- what the owner built
│   ├── index.html               <- viewer only: device-frame shell for browsing screens, not the product
│   ├── screens.json              <- screen list + order (present once the shell layout is used, 2+ screens)
│   ├── screens/                  <- the actual designed screens — this is what the dev builds on
│   ├── shared/                   <- style.css / layout.js / ui.js — shared look and behaviour used by every screen
│   ├── data/mock-data.json     <- sample data — the dev builds the real schema from this
│   ├── LEVEL.md                <- which level was reached, approved by whom and when
│   ├── screenshots/            <- 375px and 1280px evidence from level-4 testing
│   ├── README.md                 <- how to open the prototype (see condition 5 below)
│   └── เปิดดู.command            <- double-click to open in a browser automatically (macOS)
├── SPEC.md                     <- what's needed, written in plain language
├── DATA.md                     <- data used: what fields, sample values
├── RULES.md                    <- every business rule (conditions, calculations, permissions)
├── TODO-DEV.md                 <- the backend work list for the dev, each item with a reason
└── OPEN-QUESTIONS.md           <- what's still unclear/unconfirmed (never filled in with a guess)
```

`screens.json`, `screens/`, and `shared/` only exist when the prototype used the shell layout (2+ screens, per `PROJECT-STRUCTURE.md` section 3) — a single-screen prototype has no shell and `index.html` alone is the product.

**Conditions before sending (incomplete = not ready to send):**
1. The prototype opens and shows the real thing, not just a description of it.
2. Every item in `TODO-DEV.md` states **why it needs a dev** (what the owner specifically can't do themselves and why).
3. `OPEN-QUESTIONS.md` contains only genuinely unknown items — never filled in with a guessed answer. Every item must be traceable to the actual conversation with the owner: something the owner explicitly said they didn't know, or a decision that came up and was left unsettled. If the assistant believes something important was never raised at all, it does not belong in `OPEN-QUESTIONS.md` — it goes in a separate, clearly-labelled section headed `ข้อเสนอให้พิจารณา (ยังไม่เคยคุยกัน)`, so the developer can tell a genuine open question from an assistant-invented suggestion at a glance.
4. No real customer data, passwords, or keys of any kind are anywhere in the package.
5. `prototype/README.md` must instruct opening the prototype through a local server, or by double-clicking `เปิดดู.command` — it must never tell the developer to open `prototype/index.html` (or any screen file) directly by double-clicking the HTML. From level 3 onward the prototype loads `data/mock-data.json` via `fetch()`, which browsers block under `file://`, so opening the file directly renders a blank screen with no explanation.

---

## Direction 2 — Developer -> Owner

Invoked via the `/handback-to-owner` skill. The output is a zipped **Return Kit**.

```
return-{project-name}-{YYYY-MM-DD}.zip
├── README-สำหรับเจ้าของงาน.md  <- plain language, no dev jargon: what was done, what can still be edited
├── app/                         <- what remains editable, marked แก้ได้:/ห้ามแก้: in the code
├── รายงานตรวจสอบ.md            <- findings from /security-review + /bug-hunt (plain language)
├── แก้ตรงไหนได้บ้าง.md          <- self-edit guide: which line has the color, text, buttons
└── CHANGELOG.md                 <- what the dev changed from the original, and why
```

**Marking rule for code in the returned package** — so the owner feels safe editing it:

```html
<!-- แก้ได้: ข้อความปุ่ม เปลี่ยนคำในเครื่องหมายคำพูดได้เลย -->
<button>บันทึกข้อมูล</button>

<!-- ห้ามแก้: เชื่อมกับฐานข้อมูล ถ้าอยากเปลี่ยนให้บอก dev -->
<script>fetch('/api/orders')...</script>
```

**Conditions before returning:**
1. Has passed `/security-review`, with no outstanding "critical" findings.
2. Every block in the returned files carries an แก้ได้ or ห้ามแก้ marker — no exceptions.
3. `README-สำหรับเจ้าของงาน.md` contains no unexplained technical jargon.
4. Opening the file makes it work for real, with nothing extra for the owner to install.

---

## The full cycle (loops indefinitely)

```
Owner (sees only 3 commands)                 Developer
──────────────────────────                   ─────────────
/app-brief    AI asks, owner answers -> BRIEF.md
/app-show   sees the real thing + edits it themselves
                      │
                      ├──── /app-send ────►  1-build    connect DB / API / MCP / AI
                      │                            2-verify   security + bug hunt + test
                      │                            3-deploy   go live
                      │                            4-operate  monitor + write the runbook
                      ◄──── /handback-to-owner ───┤
                      │
/app-show   keeps editing (colors, copy, buttons)
                If it touches something marked ห้ามแก้ -> loop back to /app-send again
```

---

## Rules that prevent things from breaking (the most expensive lessons learned)

1. **Never send work that hasn't been seen rendered** — a description alone always gets misread.
2. **Never guess on someone else's behalf** — if something's unclear, write it into `OPEN-QUESTIONS.md` and ask, don't fill it in yourself.
3. **Never send real data across the line** — the prototype uses sample data only.
4. **A returned package must be genuinely self-editable** — if the owner opens it and is afraid to touch anything, the dev's work isn't actually done.
5. **One package = one topic** — never stuff three features into one package; it becomes unreviewable and the edit loop never ends.


---

# PROJECT STRUCTURE — standard file and folder layout

> Every project that comes off this pipeline must look the same.
> Reason: open any project and immediately know where things are - hand it off without explaining - AI finds the right file on the first try.
> Used together with skills `/scaffold-project` (create) and `/structure-check` (audit).

---

## 1. 6 rules that apply to every project

1. **The project root must be uncluttered** — someone should be able to guess what the project is within 5 seconds. Small odds and ends go into folders.
2. **Names say purpose, not type** — `orders/` beats `components/`, which becomes a dumping ground for everything (organize by subject, not by file kind).
3. **One file = one topic, max 250 lines** — split it once it grows past that.
4. **No `-v2`, `-new`, `-final`, `-copy` file names** — retire the old version into `.archive/` with a note on what replaced it.
5. **Secrets never live in the project** — `.env` is always in `.gitignore`, paired with a `.env.example` that has no real values.
6. **Never hand-edit generated output** — `dist/`, `build/`, `node_modules/`, `.next/` all go in `.gitignore`.

---

## 2. Files required at the root (every project, no exceptions)

| File | Purpose |
|---|---|
| `README.md` | What this is - how to start it in 3 commands - who maintains it |
| `AGENTS.md` | Rules for every AI tool (the shared standard across Codex/Cursor/Copilot/Gemini) |
| `CLAUDE.md` | One line pointing at `AGENTS.md` — **never duplicate the content**, or the two will drift apart |
| `.gitignore` | Must contain at minimum `.env`, `node_modules`, `dist`, `.DS_Store` |
| `.env.example` | The list of variables that must be set — **example values only, never real ones** |
| `CHANGELOG.md` | What changed, newest entry on top |

---

## 3. Standard layout — Prototype (owner builds it)

Goal: **double-click and it opens, nothing to install.**

```
{project-name}-prototype/
├── index.html          <- all the screens
├── data/
│   └── mock-data.json  <- sample data, split out starting at level 3
├── screenshots/        <- screenshots captured during testing (375px and 1280px)
├── LEVEL.md            <- what level it's at now, what's already been approved
├── เปิดดู.command      <- double-click to open in a browser automatically (macOS)
└── README.md           <- plain Thai: what this is, how to open it, what can be edited
```

### Multiple screens: wrap them in a "shell"

**Single screen** -> one file per the layout above, no shell needed.
**Two or more screens** -> use a shell, so the owner can click between screens like using the real system.

```
{project-name}-prototype/
├── index.html          <- the shell, copied wholesale, never edited
├── screens.json        <- screen list + order (edit here when adding a screen)
├── screens/
│   ├── 01-candidate-list.html      <- the actual designed screen, editable
│   └── 02-candidate-detail.html
├── data/mock-data.json
└── LEVEL.md, เปิดดู.command, .snapshots/
```

**The shell is not part of the real system** — it's purely a viewing tool, with a mobile/tablet/desktop frame,
a screen-picker tab strip, next/previous buttons, a counter, and deep links to specific screens. It disappears once the real system ships.
The shell's header must always be labeled "not part of the system" — otherwise a dev may mistake the tab strip for a real menu and build it in.

**Technical requirement: the screen-size frame must be an `<iframe>` — never a scaled-down div.**
CSS breakpoints key off the iframe's width; a scaled div produces a desktop layout squeezed narrow,
which looks like mobile but isn't, and testing against it gives wrong results.

**Each file in `screens/` must open standalone and work completely**, because the dev will pick them up and work on them one screen at a time,
and an edit to one screen touches only that file, without risking breaking the others.

> Exception to the 250-line rule: the shell file `index.html` may run long, because it must bundle its own CSS + JS
> to stay openable without a build step, per the prototype rule — splitting it into separate files would defeat that purpose.

### Always open through a local server — never tell the user to open the file directly

**Never tell the user to "just open index.html"**, for two reasons:

1. **It genuinely breaks** — from level 3 onward, data lives in `data/mock-data.json` and loads via `fetch()`,
   which browsers block when opened as `file://` — the screen goes blank with no explanation.
2. **The user isn't a developer** — a file path means nothing to them, but a clickable link communicates instantly.

Do this instead:

| Situation | Do this |
|---|---|
| Has a terminal / can run commands | Start a local server yourself, then **give a clickable link**, e.g. `http://localhost:5173` — open the browser for them too if you can |
| The user will open it later, on their own | Provide a `เปิดดู.command` file to double-click (must be `chmod +x`) |
| Web chat, nothing executable available | Paste the full HTML, explain how to save and open it — and warn that if data doesn't show up, ask for a version with the data embedded inline instead |

Standard `เปิดดู.command` contents:

```bash
#!/bin/bash
cd "$(dirname "$0")"
PORT=5173
while lsof -i :$PORT >/dev/null 2>&1; do PORT=$((PORT+1)); done
echo "เปิดที่ http://localhost:$PORT  (ปิดหน้าต่างนี้เพื่อหยุด)"
(sleep 1 && open "http://localhost:$PORT") &
python3 -m http.server $PORT
```

**Rule for reporting results to the user:** give the full clickable link - say how to stop it - **never lead with the file path**.
The path can be mentioned at the very end as extra info, but it's never the thing the user needs to read first.

### Work log — the owner side has no git, so it needs an equivalent

Developers have git to look back and roll back with. **The owner side has none of that**, but the same needs exist:
"what did this look like yesterday" - "why did this change" - "can I go back to the previous version."

So every owner-side project is required to have these 3 things, **created and updated automatically, without the user asking**:

```
{project-name}/
├── HISTORY.md              <- log of every change, newest on top
├── BRIEF.md
└── {project-name}-prototype/
    ├── LEVEL.md            <- current status (what level it's at)
    └── .snapshots/         <- copies kept for rollback
        └── 2026-08-20-1430-level2/
```

**Keep responsibilities distinct, don't duplicate:** `LEVEL.md` = status *right now* - `HISTORY.md` = what *has happened*.

#### `HISTORY.md` format (newest entry always on top)

```markdown
# บันทึกการทำงาน — {ชื่องาน}

## 2026-08-20 14:30 · ผ่านระดับ 2 (กดได้)
สั่งโดย: เจ้าของงาน
ทำอะไร: ทำปุ่มให้กดได้จริง เปลี่ยนหน้าได้ ใส่สีตามมาตรฐาน
ไฟล์ที่เปลี่ยน: index.html
ย้อนกลับได้ที่: .snapshots/2026-08-20-1430-ระดับ2/

## 2026-08-20 13:05 · แก้ตามที่ขอ (อยู่ระดับ 1 เหมือนเดิม)
สั่งโดย: เจ้าของงาน — "ย้ายปุ่มบันทึกไปข้างบน"
ทำอะไร: ย้ายปุ่มบันทึกจากท้ายฟอร์มขึ้นไปหัวหน้าจอ
ไฟล์ที่เปลี่ยน: index.html
ย้อนกลับได้ที่: .snapshots/2026-08-20-1305-ก่อนย้ายปุ่ม/
```

Rules:
- Write it in **plain Thai a non-developer can read** — no technical jargon, no function names.
- The "สั่งโดย" (requested by) field should quote the user's own words briefly, when available — so it's clear *why* something changed, not just *what* changed.
- Log every time a file changes, not only on level transitions.
- **Never edit or delete old entries** — only ever add a new one on top.

#### `.snapshots/` — keep only what's needed

- Take a copy **before** every level-up edit, and before any edit that touches multiple places at once.
- Small edits (wording, a color tweak) don't need a snapshot — it would just add clutter.
- Keep the **last 10** snapshots; once that's exceeded, delete the oldest and note the deletion in `HISTORY.md`.
- Name folders in plain language, `{date}-{time}-{what-changed}`, so the right one can be picked without opening it.
- **Never include `.snapshots/` in the package sent to the dev** — but **`HISTORY.md` must be included**, since the dev needs to see what decisions were made.

### The 4 prototype levels (never skip one)

A prototype grows one level at a time. **Each level requires the owner's approval before moving to the next.**
Always record in `LEVEL.md` what level it's currently at, and who approved it and when.

| Level | Name | Has | Doesn't have yet |
|---|---|---|---|
| 1 | Wireframe | Position, order, sizing, real copy | Not clickable - no color styling yet |
| 2 | Clickable | Working buttons, screen navigation, fillable forms - full styling per `DESIGN-SYSTEM.md` | Data still hardcoded in the file |
| 3 | Data separated | Data moved to `data/mock-data.json` - all 5 states present | Not yet tested |
| 4 | Tested | Main path verified + screenshots at 375px and 1280px | — ready to hand off to dev |

### `LEVEL.md` format — one machine-readable line, then human history

`LEVEL.md`'s **first line, always, with nothing above it**, must be an HTML comment carrying a machine-readable status:

```
<!-- STATUS: level=4 approved=yes updated=2026-08-20 -->
```

Field meanings:
- `level` — the highest level (1-4) whose work is **finished**.
- `approved` — `yes` only after the owner has actually said the current level passes; `no` otherwise. Never set to `yes` by an AI on the owner's behalf.
- `updated` — the date of the last change to this file, `YYYY-MM-DD`.

Being an HTML comment, this line renders invisibly wherever `LEVEL.md` is displayed as Markdown — it changes nothing for a human reader.

The human-readable Thai history lines continue below it exactly as before (see the `HISTORY.md`-style entries used elsewhere in this doc) — people still read those for the story of what happened.

**Rule: any skill or gate that needs to know the current level or approval state must parse the `STATUS:` line — it must never string-match the Thai prose.** Two skills agreeing through a hand-written sentence is fragile: reword the Thai and a literal-text gate silently breaks even though the underlying status hasn't changed. The `STATUS:` line is the single source of truth for machines; the Thai lines below it are for people.

### `LEVEL.md` also holds the parked-requests list

A message from the owner often carries more than one instruction, and not all of them belong to the step in
progress. Stopping to do each one immediately kills momentum; dropping them silently is worse — the owner
finds out much later that a request simply vanished.

So `LEVEL.md` carries a section for requests that were heard but not yet done:

```markdown
## ค้างไว้ (ยังไม่ได้ทำ)
- 2026-08-21 — "เปลี่ยนเบอร์โทรในข้อมูลตัวอย่างเป็น 08x-xxx-xxxx" (รับทราบตอนปิดระดับ 1)
```

Rules:
- **Acknowledge every instruction in the message, out loud, before doing anything.** Say which ones are being
  done now and which are being parked. Never respond to only the convenient half.
- Do what belongs to the current step. Park the rest here, quoting the owner's own words.
- **Raise the parked list again at the next level boundary** and ask whether to clear it before moving on.
- An item leaves this list only when it is actually done — and then it gets an entry in `HISTORY.md`.
- If the list is empty, omit the section entirely rather than leaving an empty heading.

> This is the difference between "not yet" and "forgotten". The owner is entitled to know which one happened.

### Level 4 must also verify the acceptance criteria from `BRIEF.md`

Reaching level 4 is not just "the main path works and is screenshotted" — it also requires walking every acceptance criterion recorded in `BRIEF.md` against the **running prototype**, one by one, and recording each as:

- **ผ่าน** — actually confirmed working in the running prototype.
- **ไม่ผ่าน** — tried against the running prototype and it did not hold.
- **ทำไม่ได้ในขั้น prototype** — cannot be verified at prototype stage because it depends on something a prototype has no way to have (a real backend, persistence, an external integration, etc).

A criterion landing in **ทำไม่ได้ในขั้น prototype** is not a failure of the prototype — a prototype has no backend by design — but it must never be silently dropped: it must be written down against the specific criterion, and it must become an item in `TODO-DEV.md` so the dev picks it up. Declaring level 4 done while any acceptance criterion sits unverified — including by simply reading the code and assuming it must be fine — is forbidden; verification means exercising the running prototype, not reading its source.

### From which level do responsive and UX rules apply

**Responsive applies from level 1 onward, no exceptions**, because the layout skeleton *is itself* a responsive decision.
If level 1 is built as a wide desktop grid and mobile is only considered at level 2, the skeleton has to be torn out and redone —
which defeats the entire purpose of having a level 1. **What can be deferred is decoration, never structure.**

| Must pass at level | Item | Reference |
|---|---|---|
| **1** | Designed at 375px first, then scaled up - nothing overflows the edge - tables become cards on mobile | `DESIGN-SYSTEM.md` §7, §9 |
| **1** | One clearly dominant primary button per screen - correct priority order | §0 |
| **1** | Real Thai copy, buttons as outcome verbs, not "OK"/"Submit" | §5 |
| **1** | Tap targets no smaller than 44x44px - no redundant info the user already knows | §7, §0 |
| **2** | Color, typography, spacing, sharp corners — all to standard | §1b-4 |
| **2** | Forms: labels above the field - errors under that field - can't double-submit while saving | §8 |
| **2** | Tab reaches everything with visible focus - contrast >= 4.5:1 - dark mode supported | §10 |
| **3** | All 5 states, switchable from real data in the file | §6 |
| **3** | Handles abnormal data — very long names, negative values, zero, enough rows to need pagination | §6, §9 |
| **4** | Proven with real screenshots at 375px and 1280px - the main path walked through end to end | — |

**The single easiest rule to remember:** level 1 must **already work on mobile** — it just isn't pretty yet.
If it overflows the edge or needs horizontal scrolling at 375px, it hasn't passed level 1 — do not proceed.

### `index.html` internal structure (required at every level)

Always split into 3 sections, in this order, with clear separating comments:
```
1) Config    — colors, copy, constants                          (owner-editable)
2) Data      — level 1-2: inline here · level 3+: loaded from data/mock-data.json  (editable)
3) Logic     — behavior                                          (dev-owned)
```

### `data/mock-data.json` (from level 3 onward)

Split out because **the dev can take this file straight into a database schema**, no need to reverse-engineer it out of HTML,
and the owner can edit sample data themselves without touching code.

```json
{
  "_คำอธิบาย": "ข้อมูลสมมติเท่านั้น ห้ามใส่ข้อมูลลูกค้าจริง",
  "orders": [
    { "id": "OD-001", "customer_name": "สมชาย ใจดี", "total": 1250, "status": "รอชำระ", "created_at": "2026-08-01" }
  ]
}
```

- Field names are `snake_case` English (matching the naming standard in section 5) - values inside can be Thai.
- Dates as `YYYY-MM-DD` - money as plain numbers, no `฿`, no comma separators.
- Must include enough data to test all 5 states: has data - empty - search returns nothing - enough rows to paginate - abnormal data (negative values, very long names).
- **No real customer data, passwords, or keys of any kind** — `/app-send` scans for this and refuses to package it.

---

## 4. Standard layout — Real system (dev builds it)

```
{project-name}/
├── README.md  AGENTS.md  CLAUDE.md  .gitignore  .env.example  CHANGELOG.md
├── docs/
│   ├── SPEC.md            <- what's needed (sourced from the owner's handoff package)
│   ├── RULES.md           <- every business rule
│   ├── ARCHITECTURE.md    <- what the system is made of, how the parts talk to each other
│   └── handoff/           <- handoff packages exchanged with the owner, kept as history
├── src/
│   ├── modules/           <- organized by "subject," not by file kind
│   │   └── orders/        <- this subject's screens + logic + types, all together
│   │       ├── ui/
│   │       ├── logic/
│   │       └── types.ts
│   ├── shared/            <- only things used by >=2 modules
│   │   ├── ui/            <- buttons, cards, tables, per DESIGN-SYSTEM.md
│   │   ├── lib/
│   │   └── types/
│   └── config/            <- the single place env values are read from — nowhere else reads process.env directly
├── server/                <- backend (if any)
│   ├── routes/            <- 1 file = 1 endpoint group
│   ├── services/          <- business logic — never stuffed into routes
│   ├── db/
│   │   ├── schema.*       <- table structure
│   │   └── migrations/    <- ordered by time — never edit one that's already run
│   └── middleware/
├── tests/
│   ├── unit/  integration/  e2e/
├── scripts/               <- utility scripts (backup, seed, deploy)
└── .archive/              <- retired material, with a file noting what replaced it
```

**File-placement rules:**
- Something used in exactly one place stays inside that module — **never promote it to `shared/` up front**.
- Only promote to `shared/` once at least 2 real usages exist.
- `routes/` must contain no business logic — receive/validate input, then call `services/`.
- env is read in exactly one place: `config/`.

---

## 5. Naming

| Type | Format | Example |
|---|---|---|
| Folder | kebab-case | `order-history/` |
| Generic code file | kebab-case | `create-order.ts` |
| File exporting a component | PascalCase | `OrderTable.tsx` |
| Doc file | UPPER-KEBAB | `ARCHITECTURE.md` |
| Database table | snake_case, plural | `order_items` |
| Column | snake_case | `created_at` |
| Env variable | UPPER_SNAKE | `DATABASE_URL` |

- Date fields always end in `_at` - boolean fields start with `is_` or `has_`.
- **Dates are always stored as `YYYY-MM-DD`, Gregorian** — convert to Buddhist Era only at display time.
- No Thai characters or spaces in code file names (doc file names may be Thai).

---

## 6. Split files by "unit of change"

**One rule: things that usually change together live in the same file - things that usually change separately live in separate files.**

The reason isn't aesthetics, it's real cost — every time a change is requested, AI has to read the file first.
If everything is dumped in one file, editing a single button's text means reading the whole system — slow, expensive, and risky to unrelated parts.
Conversely, splitting too finely, so that things that must change together are scattered, means opening ten files to fix one thing — equally bad.

### Test whether a split is correct

Ask: **"How many files does one typical edit request have to touch?"**

| Request | Should touch | If it touches more, it means |
|---|---|---|
| "Change the button text on the list page" | 1 file | — |
| "Change the system name in the top bar" | 1 file | The top bar is being copy-pasted onto every page — pull it out to one place |
| "Change the primary button color system-wide" | 1 file | Color is hardcoded in multiple places — make it a single variable |
| "Add a new screen" | 2 files (the screen file + the screen list) | — |

### The split prototype layout

```
{project-name}-prototype/
├── index.html            shell, never edited
├── screens.json          screen list
├── screens/
│   ├── 01-candidate-list.html    just this screen's content
│   └── 02-detail.html
├── shared/
│   ├── style.css         colors, typography, buttons, spacing — edits here affect every screen
│   ├── layout.js         top bar, bottom bar, mobile menu — written once, used on every screen
│   └── ui.js             skeletons, money/date formatting, all 5 states
└── data/mock-data.json
```

**Which files are copied as-is, and which are meant to be edited** — get this wrong and nothing can be styled:

| File | Status |
|---|---|
| `index.html` (the shell) | **Copied verbatim, never edited.** It is viewer chrome, not the product. |
| `screens.json` · `screens/*` | Written fresh for this project. |
| `shared/style.css` · `shared/layout.js` · `shared/ui.js` | **Copied as a starting point, then edited.** This is where the system name, colours and shared behaviour live — editing them is the intended way to apply a brand direction. "Copy as-is" never applied to these. |

- **Never copy the top/bottom bar into every screen file** — call it from `shared/layout.js`.
- Files in `screens/` still **open standalone and work completely**, since they reference `../shared/` via relative paths
  ("complete" means it works when opened, not that it must be a single isolated file).
- Still no build step required — just open through a local server per section 3.

> **Exception: a single-screen prototype** doesn't need `shared/` split out at all — one file is enough.
> Split only once a second screen exists — don't split preemptively because you expect one later.

### Every folder with many files needs an index

If a folder holds more than ~5 files, add a file stating what's where (`README.md` or `screens.json`)
so the right file can be found **without opening and reading every file first** — this is the single biggest source of wasted tokens.

### The same principle applies to our own skills

`SKILL.md` stays short, readable in one pass - deep content lives in `references/*.md`, opened **only when actually needed**.
Full rules in `CONVENTIONS.md` section 9.

## 7. Clean up before calling it done (required for every task)

Unused files are more dangerous than they seem — the next person (or the next AI pass) will open the wrong file, edit the wrong copy,
and wonder why the edit didn't take effect. **Every file in the project must have an answer to "who uses this."**

### Always clean up before announcing something is done

| Type | Example | What to do |
|---|---|---|
| Leftover experiment files | `test.html`, `try2.js`, `index-copy.html` | Delete |
| Backup files made while editing | `*.bak`, `*.old`, `*.orig` | Delete — history already lives in `HISTORY.md`/git |
| Names implying a duplicate exists | `-v2`, `-new`, `-final`, `-copy`, `-ใหม่` | Keep one, delete or archive the rest |
| System junk | `.DS_Store`, `Thumbs.db` | Delete + add to `.gitignore` |
| Anything always regeneratable | `dist/`, `build/`, `node_modules/` | Never commit — add to `.gitignore` |
| Orphaned images/attachments | Leftover images in `screenshots/` from a previous round | Delete |
| Commented-out code | Blocks left "in case it's needed later" | Delete |
| Retired but worth keeping | An old approach that might come back | Move to `.archive/` **with a line noting what replaced it** |

### Rules

1. **Temporary files are fine to create, but must be cleaned up before the task ends** — experiment files, one-off scripts, debug files.
   If one genuinely needs to stay, say so in the report — what's left and why.
2. **Never delete something you haven't inspected first** — always open and confirm it's really what you think it is.
   If unsure whether something is still in use, **ask, don't guess.**
3. **Never delete a file the user created themselves without asking first** — anything the user made or that pre-existed always needs permission first.
4. **Retired is not the same as delete-immediately** — anything worth keeping for reference goes to `.archive/`, but **never placed alongside the live version**.
   Absolutely never leave `screen-v2.html` sitting next to `screen.html` with no indication of which one is real.
5. **Auto-generated output must always be re-creatable** — if deleting it and regenerating it doesn't work, it isn't really auto-generated;
   it needs to be kept as a source file instead.
6. **Ask yourself before finishing every time:** "if someone who's never seen this project opened it right now, would any file confuse them?"

### Prototype side only

- `.snapshots/` keeps the last 10; once exceeded, delete the oldest and note the deletion in `HISTORY.md`.
- A retired screen must be removed from `screens/` **and** removed from `screens.json` at the same time —
  leaving the file but removing it from the list creates an orphaned file nobody will ever open.
- Keep only the most recent round of images in `screenshots/`, used to confirm level 4.

## 8. Checklist before calling the structure clean

- [ ] Project root has every required file per section 2
- [ ] `.env` is in `.gitignore` and no real value leaked into any file
- [ ] No file exceeds 250 lines
- [ ] No `-v2`, `-new`, `-final`, `-copy` name exists anywhere
- [ ] Everything in `shared/` has >=2 real usages
- [ ] `routes/` contains no business logic
- [ ] `CLAUDE.md` is a single line pointing to `AGENTS.md`, not a copy of its content
- [ ] Opening the project for the first time makes it obvious which file to read first
- [ ] A typical edit request touches no more than 1-2 files (section 6) — no top bar/color duplicated in multiple places
- [ ] Any folder with more than 5 files has an index so the right file can be found without reading every file first
- [ ] Cleanup per section 7 is done — no leftover experiment files, backup files, `-v2`/`-copy` names, or orphaned files
