# Creator Gap Closure — Phase 0 + Phase 1 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build the seven shared UI primitives, then use them to close the six P0 journey breaks in the creator prototype, so no creator journey terminates in a dead end.

**Architecture:** Everything lands in one file, `creator/main.dc.html`. Screens are `<sc-if value="{{ isX }}">` blocks inside `<x-dc>` (which ends at line 4206); the boolean flags and event handlers they bind to are properties on the object returned from `Component`'s render method (starts ~line 5578). Navigation is `this.push('routeName', {...})` / `this.back()`. There is no build step and no component system for author-defined markup, so a "primitive" is a documented markup pattern with one canonical instance in a dev gallery that later tasks copy from.

**Tech Stack:** Plain HTML + CSS custom properties + the `x-dc` template runtime. Design tokens come from `creator/_ds/startview-design-system-*/tokens/*.css`. Local server is Python (`server.py`) on port 8123 via `.claude/launch.json`.

**Spec:** [`docs/specs/2026-08-19-creator-prototype-gap-closure-design.md`](../specs/2026-08-19-creator-prototype-gap-closure-design.md)

## Global Constraints

- **All edits go in `creator/main.dc.html`.** No new files, no changes to `startview-platform`.
- **Branch:** `creator-update`.
- **No test runner exists in this repo.** "Verify" always means: serve the prototype and observe it in the browser. The TDD cycle is adapted — *state the observable check, confirm it fails, implement, confirm it passes*. Never write pytest/jest steps; there is no harness to run them.
- **Commit messages are Korean**, matching repo history (`feat(creator): …`, `fix(creator): …`). No AI attribution, no `Co-Authored-By`.
- **Colors come from tokens only.** Use `var(--error)`, `var(--success)`, `var(--surface-1)`, `var(--surface-2)`, `var(--ink)`, `var(--text-secondary)`, `var(--text-tertiary)`, `var(--hairline)`, `var(--status-review-bg)`, `var(--status-picked-bg)`, `var(--status-cancel-bg)`, `var(--status-done-bg)`, `var(--status-passed-bg)`. Never hardcode a hex value.
- **Spacing/radius come from tokens only:** `var(--space-N)`, `var(--radius-sm|md|lg|xl|2xl|pill)`.
- **Korean copy** for all user-facing strings, `class="sv-kr"` on any Korean text that wraps.
- **Fonts** follow the existing idiom: `font:600 15px/20px var(--font-body-semibold),var(--font-fallback-kr)`.
- **Doherty tiers are fixed:** <400ms no indicator · 400ms–1s skeleton · >1s determinate progress.

## How to verify anything in this plan

Run once at the start of a session and leave it running:

```
mcp__Claude_Browser__preview_start  {"name": "startview-prototypes"}
```

Then for each check:

```
mcp__Claude_Browser__navigate       {"url": "http://localhost:8123/creator/main.dc.html"}
mcp__Claude_Browser__read_console_messages  {"onlyErrors": true}
mcp__Claude_Browser__read_page      {"filter": "all"}
```

`read_console_messages` returning any error means the task is not done, regardless of how the screen looks — a template binding to a flag that does not exist fails silently in the markup but logs.

To reach a screen that has no UI entry point yet, drive the state machine directly:

```
mcp__Claude_Browser__javascript_tool {"action":"javascript_exec","text":"document.querySelector('x-dc').__dc.setState({auth:null, route:'devStates'})"}
```

---

## File Structure

One file changes. Within it, five regions — know which you are editing:

| Region | Lines (at plan time) | Contents |
|---|---|---|
| `<head>` + `<style>` | 1–71 | Global CSS, keyframes, `.sv-*` utility classes |
| Markup, inside `<x-dc>` | 72–4206 | Every `<sc-if>` screen block |
| Seed data | 4300–4930 | `OFFERS`, `SEED`, `HELP`, `NOTICES`, status maps |
| `class Component` | 4930–5560 | `state = {}`, navigation helpers, action methods |
| Render object | 5560–end | Every `isX` flag and `onY` handler |

Adding a screen always touches three of them: seed data (if it needs rows), the markup region, and the render object. Adding a route also touches `state` and a `push` call.

---

## Task 1: Dev gallery route

Phase 0 ships no user-facing screen, so without this there is nothing to review it against. This route is the canonical reference implementation of all seven primitives; Tasks 2–8 fill it in, and Phase 1 tasks copy blocks out of it.

**Files:**
- Modify: `creator/main.dc.html` — `state` (~4931), markup before `</x-dc>` (~4206), render object (~5578)

**Interfaces:**
- Produces: route key `'devStates'`; render flag `isStates`; handler `openStates()` on `Component`; no UI entry point (deliberately unreachable except via `setState`)

- [ ] **Step 1: State the observable check**

Navigating to `route:'devStates'` should show a screen titled `공용 상태 갤러리`. Right now it will show a blank canvas, because no `sc-if` matches.

- [ ] **Step 2: Confirm it fails**

```
mcp__Claude_Browser__navigate {"url":"http://localhost:8123/creator/main.dc.html"}
mcp__Claude_Browser__javascript_tool {"action":"javascript_exec","text":"document.querySelector('x-dc').__dc.setState({auth:null, route:'devStates'})"}
mcp__Claude_Browser__read_page {"filter":"all"}
```

Expected: no `공용 상태 갤러리` text anywhere in the tree.

- [ ] **Step 3: Add the render flag**

In the render object, immediately after the line `isPenalty: !s.auth && s.route === 'penalty',`, add:

```js
      isStates: !s.auth && s.route === 'devStates',
```

- [ ] **Step 4: Add the markup block**

Immediately before `</x-dc>` (line ~4206), add:

```html
    <!-- ─────────── 공용 상태 갤러리 (개발용 · UI 진입점 없음) ─────────── -->
    <sc-if value="{{ isStates }}" hint-placeholder-val="{{ true }}">
    <div class="sv-scroll" style="position:absolute;inset:0;background:var(--canvas)">
      <div style="display:flex;align-items:center;gap:10px;padding:14px 16px 0">
        <button onClick="{{ goBack }}" style="display:flex"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="arrow-left" variant="line" size="22px" color="var(--ink)" hint-size="22px,22px"></x-import></button>
        <h1 class="sv-kr" style="font:500 20px/26px var(--font-display-medium),var(--font-fallback-kr);letter-spacing:-0.3px;color:var(--ink)">공용 상태 갤러리</h1>
      </div>
      <p class="sv-kr" style="padding:6px 16px 0;font:400 12px/1.4 var(--font-body),var(--font-fallback-kr);color:var(--text-tertiary)">Phase 0 프리미티브의 기준 구현. 새 화면은 여기서 복사해 쓴다.</p>
      <div id="sv-states-body" style="padding:16px"></div>
    </div>
    </sc-if>
```

- [ ] **Step 5: Add the navigation helper**

In `class Component`, immediately after `openNoti() { this.push('noti', {}); }` (~line 5185), add:

```js
  openStates() { this.push('devStates', {}); }
```

- [ ] **Step 6: Confirm it passes**

Repeat Step 2's commands. Expected: `공용 상태 갤러리` appears in the tree, the back arrow returns to the previous screen, and `read_console_messages {"onlyErrors":true}` is empty.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 공용 상태 갤러리 라우트 — Phase 0 검토 판"
```

---

## Task 2: Loading primitives

**Files:**
- Modify: `creator/main.dc.html` — `<style>` (~line 40, beside the other `@keyframes`), gallery body

**Interfaces:**
- Consumes: `isStates` (Task 1)
- Produces: CSS classes `.sv-skel`, `.sv-skel-row`, `.sv-skel-hero`, `.sv-skel-grid`; keyframe `sv-shimmer`. Later tasks reference these class names verbatim.

- [ ] **Step 1: State the observable check**

The gallery shows three skeleton shapes — a list row, a detail hero, a card grid — each animating a shimmer. Currently the repo contains zero occurrences of `skel`.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "sv-skel" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add the CSS**

In `<style>`, after the `@keyframes sv-push` line, add:

```css
@keyframes sv-shimmer{from{background-position:-200px 0}to{background-position:calc(200px + 100%) 0}}
.sv-skel{background:var(--surface-1);background-image:linear-gradient(90deg,transparent,rgba(0,0,0,0.04),transparent);
  background-size:200px 100%;background-repeat:no-repeat;animation:sv-shimmer 1.2s ease-in-out infinite;border-radius:var(--radius-sm)}
@media (prefers-reduced-motion:reduce){.sv-skel{animation:none}}
.sv-skel-row{display:flex;gap:12px;align-items:center;padding:12px 0}
.sv-skel-hero{width:100%;height:180px;border-radius:var(--radius-2xl)}
.sv-skel-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
```

The `prefers-reduced-motion` guard is required — the repo already honors it elsewhere.

- [ ] **Step 4: Add the three shapes to the gallery**

Inside `<div id="sv-states-body">`, add:

```html
      <h2 class="sv-kr" style="font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">로딩 — 목록 행</h2>
      <div style="margin-top:8px">
        <div class="sv-skel-row"><span class="sv-skel" style="width:56px;height:56px;border-radius:var(--radius-md);flex:none"></span><span style="flex:1"><span class="sv-skel" style="display:block;height:13px;width:70%"></span><span class="sv-skel" style="display:block;height:11px;width:40%;margin-top:7px"></span></span></div>
        <div class="sv-skel-row"><span class="sv-skel" style="width:56px;height:56px;border-radius:var(--radius-md);flex:none"></span><span style="flex:1"><span class="sv-skel" style="display:block;height:13px;width:55%"></span><span class="sv-skel" style="display:block;height:11px;width:35%;margin-top:7px"></span></span></div>
      </div>
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">로딩 — 상세 히어로</h2>
      <div style="margin-top:8px"><span class="sv-skel sv-skel-hero" style="display:block"></span><span class="sv-skel" style="display:block;height:18px;width:60%;margin-top:14px"></span><span class="sv-skel" style="display:block;height:12px;width:85%;margin-top:8px"></span></div>
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">로딩 — 카드 그리드</h2>
      <div class="sv-skel-grid" style="margin-top:8px">
        <span><span class="sv-skel" style="display:block;height:120px;border-radius:var(--radius-xl)"></span><span class="sv-skel" style="display:block;height:12px;width:80%;margin-top:8px"></span></span>
        <span><span class="sv-skel" style="display:block;height:120px;border-radius:var(--radius-xl)"></span><span class="sv-skel" style="display:block;height:12px;width:65%;margin-top:8px"></span></span>
      </div>
```

- [ ] **Step 5: Confirm it passes**

Navigate to the gallery. Expected: three labelled skeleton groups, shimmer animating. Screenshot to confirm the shimmer reads as motion and not as a static grey block:

```
mcp__Claude_Browser__computer {"action":"screenshot"}
```

- [ ] **Step 6: Record the Doherty tier rule**

Directly above the `@keyframes sv-shimmer` line, add this comment — later tasks must not have to rediscover the rule:

```css
/* 로딩 3단 — 400ms 미만은 아무것도 그리지 않는다(깜빡이는 스피너가 더 나쁘다),
 * 400ms~1s 는 목적지 모양의 스켈레톤, 1s 초과는 진행률을 가진 인디케이터. */
```

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 스켈레톤 3종 — 목록 행·상세 히어로·카드 그리드"
```

---

## Task 3: Error and retry

**Files:**
- Modify: `creator/main.dc.html` — gallery body

**Interfaces:**
- Consumes: `isStates` (Task 1)
- Produces: markup patterns `sv-err-inline` and `sv-err-full` (documented by comment, copied by later tasks)

- [ ] **Step 1: State the observable check**

The gallery shows two error treatments, each with a retry control. The repo currently has no retry affordance (`재시도` count is 0).

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "재시도" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add both scopes to the gallery**

```html
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">오류 — 구간(다른 곳은 쓸 수 있다)</h2>
      <!-- sv-err-inline: 한 섹션만 실패. 페이지의 나머지는 살아 있다. -->
      <div style="margin-top:8px;display:flex;align-items:center;gap:10px;padding:14px;border-radius:var(--radius-xl);background:var(--surface-1)">
        <x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="alert" variant="line" size="18px" color="var(--error)" hint-size="18px,18px"></x-import>
        <span class="sv-kr" style="flex:1;font:400 13px/18px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">추천 캠페인을 불러오지 못했어요</span>
        <button style="flex:none;padding:7px 12px;border-radius:var(--radius-sm);background:var(--surface-2);font:600 12px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">다시 시도</button>
      </div>
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">오류 — 화면 전체</h2>
      <!-- sv-err-full: 아무것도 불러오지 못했다. -->
      <div style="margin-top:8px;display:flex;flex-direction:column;align-items:center;text-align:center;padding:40px 24px;border-radius:var(--radius-xl);background:var(--surface-1)">
        <x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="alert" variant="line" size="28px" color="var(--text-tertiary)" hint-size="28px,28px"></x-import>
        <span class="sv-kr" style="margin-top:12px;font:600 15px/20px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">연결이 불안정해요</span>
        <span class="sv-kr" style="margin-top:6px;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">잠시 후 다시 시도해주세요.</span>
        <button style="margin-top:18px;height:44px;padding:0 22px;border-radius:var(--radius-lg);background:var(--ink);font:600 14px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">다시 시도</button>
      </div>
```

Both scopes state *what* failed, not just that something did. An error that says only `오류가 발생했습니다` fails this task.

- [ ] **Step 4: Confirm it passes**

Navigate to the gallery. Expected: both blocks visible, each with a `다시 시도` control. `read_console_messages {"onlyErrors":true}` empty.

- [ ] **Step 5: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 오류·재시도 2종 — 구간 오류와 화면 전체 오류"
```

---

## Task 4: Empty states

**Files:**
- Modify: `creator/main.dc.html` — gallery body

**Interfaces:**
- Consumes: `isStates` (Task 1)
- Produces: markup patterns `sv-empty` and `sv-empty-filtered`

Per Filtering items §7 these are two different states with different copy and different CTAs. Collapsing them into one is the failure this task exists to prevent.

- [ ] **Step 1: State the observable check**

Gallery shows both variants side by side, visibly different in copy and CTA.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "sv-empty" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add both variants**

```html
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">빈 상태 — 아직 없음</h2>
      <!-- sv-empty: 정말로 아무것도 없다. CTA 는 첫 개를 만들러 보낸다. -->
      <div style="margin-top:8px;display:flex;flex-direction:column;align-items:center;text-align:center;padding:36px 24px">
        <span style="width:48px;height:48px;border-radius:var(--radius-circle);background:var(--surface-1);display:flex;align-items:center;justify-content:center"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="task" variant="line" size="22px" color="var(--text-tertiary)" hint-size="22px,22px"></x-import></span>
        <span class="sv-kr" style="margin-top:12px;font:600 15px/20px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">아직 신청한 캠페인이 없어요</span>
        <span class="sv-kr" style="margin-top:6px;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">마음에 드는 캠페인을 찾아 지원해보세요.</span>
        <button style="margin-top:18px;height:44px;padding:0 22px;border-radius:var(--radius-lg);background:var(--ink);font:600 14px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">캠페인 둘러보기</button>
      </div>
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">빈 상태 — 필터 결과 없음</h2>
      <!-- sv-empty-filtered: 있긴 한데 필터가 걸러냈다. CTA 는 필터를 푼다. -->
      <div style="margin-top:8px;display:flex;flex-direction:column;align-items:center;text-align:center;padding:36px 24px">
        <span class="sv-kr" style="font:600 15px/20px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">조건에 맞는 캠페인이 없어요</span>
        <span class="sv-kr" style="margin-top:6px;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">필터를 줄이면 더 많은 캠페인을 볼 수 있어요.</span>
        <button style="margin-top:16px;height:40px;padding:0 18px;border-radius:var(--radius-lg);background:var(--surface-2);font:600 13px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">필터 해제</button>
      </div>
```

- [ ] **Step 4: Confirm it passes**

Navigate to the gallery. Expected: two blocks whose headline and CTA differ. If both say the same thing, the task is not done.

- [ ] **Step 5: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 빈 상태 2종 — 아직 없음과 필터 결과 없음을 나눈다"
```

---

## Task 5: Upload trio

The largest primitive, and the one with the most downstream consumers (review submission, revision re-upload, group-buy footage). Governed by Uploading media, all seven items.

**Files:**
- Modify: `creator/main.dc.html` — gallery body

**Interfaces:**
- Consumes: `isStates` (Task 1)
- Produces: markup patterns `sv-up-idle`, `sv-up-busy`, `sv-up-fail`. Phase 2 copies these verbatim.

- [ ] **Step 1: State the observable check**

Gallery shows three upload states. The idle state names the constraints **before** any attempt — `500MB 이하`, video-only. Each failure names a recovery action.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "sv-up-idle" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add the idle state (constraints stated up front)**

```html
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">업로드 — 대기</h2>
      <!-- sv-up-idle: 제약을 먼저 말한다. 시도한 뒤에 알려주는 건 오류가 아니라 함정이다. -->
      <div style="margin-top:8px;display:flex;flex-direction:column;align-items:center;text-align:center;padding:28px 20px;border-radius:var(--radius-xl);border:1px dashed var(--hairline);background:var(--surface-1)">
        <x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="upload" variant="line" size="24px" color="var(--text-secondary)" hint-size="24px,24px"></x-import>
        <span class="sv-kr" style="margin-top:10px;font:600 14px/19px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">영상을 올려주세요</span>
        <span class="sv-kr" style="margin-top:5px;font:400 12px/17px var(--font-body),var(--font-fallback-kr);color:var(--text-tertiary)">MP4·MOV · 500MB 이하</span>
        <button style="margin-top:14px;height:40px;padding:0 18px;border-radius:var(--radius-lg);background:var(--ink);font:600 13px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">파일 선택</button>
      </div>
```

- [ ] **Step 4: Add the in-progress state (filename + percentage + cancel)**

```html
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">업로드 — 진행 중</h2>
      <!-- sv-up-busy: 1s 를 넘는 작업이므로 진행률을 가진 인디케이터를 쓴다(Doherty 3단). -->
      <div style="margin-top:8px;padding:14px;border-radius:var(--radius-xl);background:var(--surface-1)">
        <div style="display:flex;align-items:center;gap:10px">
          <span class="sv-kr" style="flex:1;min-width:0;font:500 13px/18px var(--font-body-medium),var(--font-fallback-kr);color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">리뷰_최종본_0819.mp4</span>
          <span style="flex:none;font:600 12px/1.2 var(--font-body-semibold);color:var(--text-secondary)">62%</span>
          <button style="flex:none;display:flex"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="close" variant="line" size="16px" color="var(--text-tertiary)" hint-size="16px,16px"></x-import></button>
        </div>
        <span style="display:block;margin-top:10px;height:4px;border-radius:var(--radius-pill);background:var(--surface-2);overflow:hidden"><span style="display:block;width:62%;height:100%;background:var(--ink)"></span></span>
      </div>
```

- [ ] **Step 5: Add the two failure states, each with a recovery action**

```html
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">업로드 — 실패</h2>
      <!-- sv-up-fail: 무엇이 잘못됐는지와 무엇을 하면 되는지를 함께 준다. -->
      <div style="margin-top:8px;display:flex;align-items:center;gap:10px;padding:14px;border-radius:var(--radius-xl);background:var(--status-cancel-bg)">
        <x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="alert" variant="line" size="18px" color="var(--error)" hint-size="18px,18px"></x-import>
        <span style="flex:1;min-width:0">
          <span class="sv-kr" style="display:block;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">파일이 너무 커요 — 620MB</span>
          <span class="sv-kr" style="display:block;margin-top:2px;font:400 12px/17px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">500MB 이하로 줄여서 다시 올려주세요.</span>
        </span>
        <button style="flex:none;padding:7px 12px;border-radius:var(--radius-sm);background:var(--canvas);font:600 12px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">다시 선택</button>
      </div>
      <div style="margin-top:8px;display:flex;align-items:center;gap:10px;padding:14px;border-radius:var(--radius-xl);background:var(--status-cancel-bg)">
        <x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="alert" variant="line" size="18px" color="var(--error)" hint-size="18px,18px"></x-import>
        <span style="flex:1;min-width:0">
          <span class="sv-kr" style="display:block;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">영상 파일만 올릴 수 있어요</span>
          <span class="sv-kr" style="display:block;margin-top:2px;font:400 12px/17px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">MP4 또는 MOV 파일을 선택해주세요.</span>
        </span>
        <button style="flex:none;padding:7px 12px;border-radius:var(--radius-sm);background:var(--canvas);font:600 12px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">다시 선택</button>
      </div>
```

- [ ] **Step 6: Confirm it passes**

Navigate to the gallery. Check against Uploading media: constraints visible before upload (§4), progress with filename and percentage (§3), outcome with recovery (§5), cancel present (§6). All four must hold.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 업로드 상태 3종 — 제약 선고지·진행률·복구 가능한 실패"
```

---

## Task 6: Destructive-confirm sheet

**Files:**
- Modify: `creator/main.dc.html` — `state` (~4931), gallery body, render object

**Interfaces:**
- Consumes: `isStates` (Task 1)
- Produces: state field `confirmSheet` (`null` or an object `{title, body, reversible, confirmLabel}`); render flags `confirmOpen`, `confirmTitle`, `confirmBody`, `confirmNote`, `confirmLabel`; handlers `closeConfirm()`, `askConfirm(cfg)` on `Component`. Phase 1 Task 14 and Phase 2 both consume `askConfirm`.

- [ ] **Step 1: State the observable check**

Calling `askConfirm({...})` opens a bottom sheet that states what happens and whether it can be undone, before the confirm button.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "askConfirm" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add state and methods**

In `state = {`, on the line with `sheet:null, sheetChannel:null,`, append:

```js
    confirmSheet:null,
```

In `class Component`, after `openStates()`, add:

```js
  /** 되돌릴 수 있는지를 confirm 앞에 말한다 — 누른 뒤에 알려주면 이미 늦다. */
  askConfirm(cfg) { this.setState({ confirmSheet: cfg }); }
  closeConfirm() { this.setState({ confirmSheet:null }); }
```

- [ ] **Step 4: Add the render flags**

In the render object, after `isStates`, add:

```js
      confirmOpen: !!s.confirmSheet,
      confirmTitle: (s.confirmSheet && s.confirmSheet.title) || '',
      confirmBody: (s.confirmSheet && s.confirmSheet.body) || '',
      confirmNote: s.confirmSheet ? (s.confirmSheet.reversible ? '나중에 다시 할 수 있어요.' : '되돌릴 수 없어요.') : '',
      confirmLabel: (s.confirmSheet && s.confirmSheet.confirmLabel) || '확인',
      closeConfirm: () => this.closeConfirm(),
```

- [ ] **Step 5: Add the sheet markup**

Immediately before `</x-dc>`, after the gallery block:

```html
    <sc-if value="{{ confirmOpen }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--scrim);z-index:60;display:flex;flex-direction:column;justify-content:flex-end;animation:sv-fade 200ms ease-out">
      <div style="background:var(--canvas);border-radius:var(--radius-3xl) var(--radius-3xl) 0 0;padding:24px 20px 20px;animation:sv-sheet 280ms cubic-bezier(0.32,0.72,0,1)">
        <h2 class="sv-kr" style="font:600 17px/23px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">{{ confirmTitle }}</h2>
        <p class="sv-kr" style="margin-top:8px;font:400 14px/21px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">{{ confirmBody }}</p>
        <p class="sv-kr" style="margin-top:6px;font:500 13px/19px var(--font-body-medium),var(--font-fallback-kr);color:var(--text-tertiary)">{{ confirmNote }}</p>
        <div style="display:flex;gap:8px;margin-top:22px">
          <button onClick="{{ closeConfirm }}" style="flex:1;height:50px;border-radius:var(--radius-lg);background:var(--surface-2);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">돌아가기</button>
          <button onClick="{{ closeConfirm }}" style="flex:1;height:50px;border-radius:var(--radius-lg);background:var(--error);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">{{ confirmLabel }}</button>
        </div>
      </div>
    </div>
    </sc-if>
```

- [ ] **Step 6: Add a gallery trigger**

In the gallery body:

```html
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">파괴적 확인 시트</h2>
      <button onClick="{{ demoConfirm }}" style="margin-top:8px;height:44px;padding:0 18px;border-radius:var(--radius-lg);background:var(--surface-2);font:600 13px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">시트 열기</button>
```

And in the render object:

```js
      demoConfirm: () => this.askConfirm({ title:'리뷰 참여를 취소할까요?', body:'지금 취소하면 이 캠페인의 선정이 사라지고, 다시 신청해야 해요.', reversible:false, confirmLabel:'참여 취소' }),
```

- [ ] **Step 7: Confirm it passes**

Navigate to the gallery, tap `시트 열기`. Expected: sheet slides up, states the consequence and `되돌릴 수 없어요.`, both buttons dismiss it. No guilt-tripping copy (Deleting account §3).

- [ ] **Step 8: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 파괴적 확인 시트 — 결과와 되돌림 가능 여부를 먼저 말한다"
```

---

## Task 7: Success-screen pattern

**Files:**
- Modify: `creator/main.dc.html` — gallery body

**Interfaces:**
- Consumes: `isStates` (Task 1)
- Produces: markup pattern `sv-done`. Task 9 (application complete) and Task 12 (demand-survey done) both instantiate it.

Layout is fixed by Serial Position Effect: what happened at the top, what happens next at the bottom, detail in the middle.

- [ ] **Step 1: State the observable check**

Gallery shows a success block whose first element is the outcome and whose last element is the next action.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "sv-done" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add the pattern**

```html
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">성공 화면</h2>
      <!-- sv-done: 첫 줄에 무슨 일이 있었는지, 마지막 줄에 다음에 무엇을 하는지.
       * 계열 위치 효과 — 처음과 끝이 기억에 남으므로 중간에 부차 정보를 둔다. -->
      <div style="margin-top:8px;display:flex;flex-direction:column;align-items:center;text-align:center;padding:36px 24px;border-radius:var(--radius-xl);background:var(--surface-1)">
        <span style="width:56px;height:56px;border-radius:var(--radius-circle);background:var(--status-passed-bg);display:flex;align-items:center;justify-content:center;animation:sv-mark 320ms cubic-bezier(0.32,0.72,0,1)">
          <x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="check" variant="line" size="26px" color="var(--success)" hint-size="26px,26px"></x-import>
        </span>
        <span class="sv-kr" style="margin-top:14px;font:500 20px/27px var(--font-display-medium),var(--font-fallback-kr);letter-spacing:-0.3px;color:var(--ink)">신청을 보냈어요</span>
        <span class="sv-kr" style="margin-top:16px;padding:12px 14px;border-radius:var(--radius-md);background:var(--canvas);font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">글로우드롭 · 8월 22일 발표</span>
        <span class="sv-kr" style="margin-top:16px;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-tertiary)">선정되면 알림으로 알려드릴게요.</span>
        <button style="margin-top:20px;width:100%;height:50px;border-radius:var(--radius-lg);background:var(--ink);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">내 협업에서 보기</button>
      </div>
```

- [ ] **Step 4: Confirm it passes**

Navigate to the gallery. Expected: check mark animates in once; outcome is the first text; the CTA is the last element.

- [ ] **Step 5: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 성공 화면 패턴 — 결과가 처음, 다음 할 일이 끝"
```

---

## Task 8: Input-error convention

Unlike Tasks 2–7 this one also **changes existing behavior**: the signup password field currently validates on every keystroke, which Showing input error §1–2 forbids.

**Files:**
- Modify: `creator/main.dc.html` — render object ~6301 and ~6307 (`suPwMissing`, `suPw2Note`), `state`, gallery body

**Interfaces:**
- Consumes: `isStates` (Task 1)
- Produces: state field `touched` (an object, keys are field names); render helper `wasTouched(name)`; markup pattern `sv-field-err`

- [ ] **Step 1: State the observable check**

Typing a short password into the signup field shows **no** error until focus leaves the field; refocusing clears it.

- [ ] **Step 2: Confirm it fails**

```
mcp__Claude_Browser__navigate {"url":"http://localhost:8123/creator/main.dc.html"}
mcp__Claude_Browser__javascript_tool {"action":"javascript_exec","text":"document.querySelector('x-dc').__dc.setState({auth:'signup', suStep:1})"}
```

Type `abc` into the password field and read the page without blurring. Expected (current, wrong): `비밀번호는 8자 이상이어야 해요.` already visible while the field still has focus.

- [ ] **Step 3: Add touch tracking**

In `state = {`, beside `suPw:'', suPw2:'',` append:

```js
    touched:{},
```

In `class Component`, after `closeConfirm()`, add:

```js
  /** 입력 중에는 판정하지 않는다 — 포커스를 잃은 뒤에 본다. 다시 잡으면 지운다. */
  markTouched(name) { this.setState((st) => ({ touched: { ...st.touched, [name]:true } })); }
  clearTouched(name) { this.setState((st) => ({ touched: { ...st.touched, [name]:false } })); }
```

- [ ] **Step 4: Gate the existing password notes on blur**

Replace the `suPw2Note` line (~6307) with:

```js
      suPw2Note: !s.touched.suPw2 ? '' : s.suPw.length < 8 ? '비밀번호는 8자 이상이어야 해요.' : s.suPw === s.suPw2 ? '비밀번호가 일치해요.' : '비밀번호가 일치하지 않아요.',
```

Add the two handlers to the render object:

```js
      blurSuPw2: () => this.markTouched('suPw2'),
      focusSuPw2: () => this.clearTouched('suPw2'),
```

Then bind them on the `suPw2` input (~line 264) by adding `onBlur="{{ blurSuPw2 }}" onFocus="{{ focusSuPw2 }}"`.

- [ ] **Step 5: Confirm it passes**

Repeat Step 2. Expected: no error while typing; error appears on blur; refocusing clears it.

- [ ] **Step 6: Document the convention in the gallery**

```html
      <h2 class="sv-kr" style="margin-top:24px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">입력 오류 규칙</h2>
      <p class="sv-kr" style="margin-top:6px;font:400 12px/18px var(--font-body),var(--font-fallback-kr);color:var(--text-tertiary)">입력 중에는 기본 상태를 유지한다 · 포커스를 잃을 때 판정한다 · 다시 포커스하면 지운다. 새 폼은 markTouched/clearTouched 를 쓴다.</p>
```

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "fix(creator): 입력 오류는 블러에서 판정한다 — 타이핑 중 판정 제거"
```

---

## Phase 0 gate

Before starting Task 9, confirm all seven primitives render in the gallery with no console errors, and check them against the checklists named in the spec §6: Uploading media (all 7), Showing input error (all 4), Filtering items §7, Deleting account §3. Fix anything that fails now — every Phase 1 task builds on these.

---

## Task 9: Application complete

Closes the audit's most severe dead end: submitting the application form currently leads nowhere.

**Files:**
- Modify: `creator/main.dc.html` — markup, render object, `class Component` (`submitApply`, ~5285)

**Interfaces:**
- Consumes: `sv-done` pattern (Task 7)
- Produces: route key `'applyDone'`; render flags `isApplyDone`, `doneBrand`, `doneDate`; handler `goMyCollab()`

- [ ] **Step 1: State the observable check**

Completing the apply flow lands on a success screen naming the brand and the announcement date, with a CTA into 내 협업. Currently `submitApply` pushes the user straight to a list.

- [ ] **Step 2: Confirm it fails**

```
mcp__Claude_Browser__javascript_tool {"action":"javascript_exec","text":"document.querySelector('x-dc').__dc.setState({auth:null, route:'applyDone'})"}
mcp__Claude_Browser__read_page {"filter":"all"}
```

Expected: blank canvas, no success screen.

- [ ] **Step 3: Add the render flags**

```js
      isApplyDone: !s.auth && s.route === 'applyDone',
      doneBrand: (this.offer(s.offerId) || {}).brand || '',
      doneDate: (this.offer(s.offerId) || {}).announceAt || '모집 마감 다음 날',
      goMyCollab: () => this.setState({ route:null, tab:'campaigns', stack:[] }),
```

- [ ] **Step 4: Add the markup**

Copy the `sv-done` block from the gallery, substituting bindings — full screen rather than a card:

```html
    <!-- ─────────── 신청 완료 ─────────── -->
    <sc-if value="{{ isApplyDone }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--canvas);z-index:30;display:flex;flex-direction:column;animation:sv-push 350ms cubic-bezier(0.32,0.72,0,1)">
      <div class="sv-scroll" style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:0 28px">
        <span style="width:56px;height:56px;border-radius:var(--radius-circle);background:var(--status-passed-bg);display:flex;align-items:center;justify-content:center;animation:sv-mark 320ms cubic-bezier(0.32,0.72,0,1)">
          <x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="check" variant="line" size="26px" color="var(--success)" hint-size="26px,26px"></x-import>
        </span>
        <h1 class="sv-kr" style="margin-top:16px;font:500 22px/30px var(--font-display-medium),var(--font-fallback-kr);letter-spacing:-0.3px;color:var(--ink)">신청을 보냈어요</h1>
        <span class="sv-kr" style="margin-top:18px;padding:14px 16px;border-radius:var(--radius-md);background:var(--surface-1);font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">{{ doneBrand }} · {{ doneDate }} 발표</span>
        <p class="sv-kr" style="margin-top:16px;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-tertiary)">선정되면 알림으로 알려드릴게요.</p>
      </div>
      <div style="padding:0 28px 22px">
        <button onClick="{{ goMyCollab }}" style="width:100%;height:52px;border-radius:var(--radius-lg);background:var(--ink);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">내 협업에서 보기</button>
      </div>
    </div>
    </sc-if>
```

- [ ] **Step 5: Route the apply submission into it**

In `submitApply` (~5285), the call currently ends by setting rows and navigating away. Change its final `setState` so that after appending to `applications` it pushes the new route instead of returning to the list:

```js
    this.setState({ applications: rows, sheet:null }, () => this.push('applyDone', {}));
```

- [ ] **Step 6: Confirm it passes**

Walk the real flow: open a campaign detail, apply, agree to the four terms, submit. Expected: the success screen appears, names the brand, and the CTA lands on the 내 협업 tab with the new row present.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 신청 완료 화면 — 신청 뒤 갈 곳이 없던 막다른 길을 닫는다"
```

---

## Task 10: Demand-survey detail

**Files:**
- Modify: `creator/main.dc.html` — seed data (~4400), markup, render object

**Interfaces:**
- Consumes: `sv-empty` (Task 4), skeletons (Task 2)
- Produces: seed constant `SURVEYS` (array of `{id, brand, product, cover, window, channels, baseRate, extendRate, note}`); route `'survey'`; flags `isSurvey`, `surveyRow`; handler `openSurvey(id)`; `state.surveyId`

- [ ] **Step 1: State the observable check**

A demand-survey detail screen exists showing the brand, the collaboration window, the eligible channel types, and a CTA into the rate-proposal form.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "수요조사" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add seed data**

Next to the `OFFERS` constant:

```js
/** 수요조사 — 캠페인 전에 브랜드가 단가를 물어보는 자리다.
 *  크리에이터는 희망 단가를 제안하고, 채택되면 캠페인으로 전환된다. */
const SURVEYS = [
  { id:'s1', brand:'글로우드롭', product:'수분 앰플 30ml', cover:'assets/imagery/campaigns/cocktail.webp',
    window:'8월 25일 ~ 9월 5일', channels:['인스타그램','틱톡'], baseRate:150000, extendRate:50000,
    note:'2주 사용 후 변화가 보이는 리뷰를 찾고 있어요.' },
  { id:'s2', brand:'모닝루틴', product:'드립백 커피 20개입', cover:'assets/imagery/campaigns/coffee.webp',
    window:'9월 1일 ~ 9월 14일', channels:['유튜브 쇼츠','인스타그램'], baseRate:220000, extendRate:80000,
    note:'아침 루틴에 자연스럽게 들어가는 영상이면 좋아요.' },
];
```

- [ ] **Step 4: Add state, navigation, and flags**

In `state`, beside `offerId:null,` add `surveyId:null,`.

In `class Component`, after `openStates()`:

```js
  survey(id) { return SURVEYS.find((x) => x.id === id) || null; }
  openSurvey(id) { this.push('survey', { surveyId:id }); }
```

In the render object:

```js
      isSurvey: !s.auth && s.route === 'survey',
      surveyRow: this.survey(s.surveyId) || {},
      openSurveyApply: () => this.push('surveyApply', {}),
```

- [ ] **Step 5: Add the markup**

```html
    <!-- ─────────── 수요조사 상세 ─────────── -->
    <sc-if value="{{ isSurvey }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--canvas);z-index:30;display:flex;flex-direction:column;animation:sv-push 350ms cubic-bezier(0.32,0.72,0,1)">
      <div style="display:flex;align-items:center;gap:10px;padding:14px 16px 0">
        <button onClick="{{ goBack }}" style="display:flex"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="arrow-left" variant="line" size="22px" color="var(--ink)" hint-size="22px,22px"></x-import></button>
        <span class="sv-kr" style="flex:1;font:500 17px/23px var(--font-display-medium),var(--font-fallback-kr);color:var(--ink)">수요조사</span>
      </div>
      <div class="sv-scroll" style="flex:1;padding:16px 16px 120px">
        <img src="{{ surveyRow.cover }}" alt="" style="width:100%;height:180px;object-fit:cover;border-radius:var(--radius-2xl);display:block">
        <h1 class="sv-kr" style="margin-top:16px;font:500 21px/28px var(--font-display-medium),var(--font-fallback-kr);letter-spacing:-0.3px;color:var(--ink)">{{ surveyRow.brand }}</h1>
        <p class="sv-kr" style="margin-top:4px;font:400 14px/20px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">{{ surveyRow.product }}</p>
        <div style="margin-top:20px;padding:16px;border-radius:var(--radius-xl);background:var(--surface-1)">
          <span class="sv-kr" style="display:block;font:600 12px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-tertiary)">협업 기간</span>
          <span class="sv-kr" style="display:block;margin-top:5px;font:500 15px/21px var(--font-body-medium),var(--font-fallback-kr);color:var(--ink)">{{ surveyRow.window }}</span>
        </div>
        <p class="sv-kr" style="margin-top:20px;font:400 14px/22px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">{{ surveyRow.note }}</p>
      </div>
      <div style="padding:0 16px 22px;background:var(--canvas)">
        <button onClick="{{ openSurveyApply }}" style="width:100%;height:52px;border-radius:var(--radius-lg);background:var(--ink);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">협업 참여 신청</button>
      </div>
    </div>
    </sc-if>
```

- [ ] **Step 6: Confirm it passes**

```
mcp__Claude_Browser__javascript_tool {"action":"javascript_exec","text":"document.querySelector('x-dc').__dc.setState({auth:null, route:'survey', surveyId:'s1'})"}
```

Expected: 글로우드롭 detail renders with cover, window, and CTA. Console clean.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 수요조사 상세 — 화면이 하나도 없던 여정의 입구"
```

---

## Task 11: Demand-survey rate-proposal form

**Files:**
- Modify: `creator/main.dc.html` — markup, render object, `state`

**Interfaces:**
- Consumes: `SURVEYS` and `surveyRow` (Task 10); `markTouched`/`clearTouched` (Task 8); `sv-empty` (Task 4)
- Produces: route `'surveyApply'`; flags `isSurveyApply`, `svRate`, `svExtend`, `svRateNote`, `svBlocked`, `svNoChannel`; handlers `onSvRate`, `onSvExtend`, `openSurveyConfirm`; state `svRate`, `svExtend`

- [ ] **Step 1: State the observable check**

The form takes a desired collaboration rate and an extension fee, validates on blur (not while typing), and blocks the CTA until the rate is valid. If the creator has no connected channel it shows the empty state instead of the form.

- [ ] **Step 2: Confirm it fails**

```
mcp__Claude_Browser__javascript_tool {"action":"javascript_exec","text":"document.querySelector('x-dc').__dc.setState({auth:null, route:'surveyApply'})"}
```

Expected: blank canvas.

- [ ] **Step 3: Add state**

In `state`, beside `surveyId:null,` add:

```js
    svRate:'', svExtend:'',
```

- [ ] **Step 4: Add the render flags**

```js
      isSurveyApply: !s.auth && s.route === 'surveyApply',
      svRate: s.svRate,
      svExtend: s.svExtend,
      svNoChannel: !s.channels.length,
      onSvRate: (v) => this.setState({ svRate: String(v).replace(/[^0-9]/g, '') }),
      onSvExtend: (v) => this.setState({ svExtend: String(v).replace(/[^0-9]/g, '') }),
      blurSvRate: () => this.markTouched('svRate'),
      focusSvRate: () => this.clearTouched('svRate'),
      svRateNote: !s.touched.svRate ? '' : !s.svRate ? '희망 단가를 입력해주세요.' : Number(s.svRate) < 10000 ? '10,000원 이상으로 적어주세요.' : '',
      svBlocked: !s.svRate || Number(s.svRate) < 10000,
      openSurveyConfirm: () => this.setState({ sheet:'surveyConfirm' }),
```

- [ ] **Step 5: Add the markup**

```html
    <!-- ─────────── 수요조사 참여 신청 ─────────── -->
    <sc-if value="{{ isSurveyApply }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--canvas);z-index:32;display:flex;flex-direction:column;animation:sv-push 350ms cubic-bezier(0.32,0.72,0,1)">
      <div style="display:flex;align-items:center;gap:10px;padding:14px 16px 0">
        <button onClick="{{ goBack }}" style="display:flex"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="arrow-left" variant="line" size="22px" color="var(--ink)" hint-size="22px,22px"></x-import></button>
        <span class="sv-kr" style="flex:1;font:500 17px/23px var(--font-display-medium),var(--font-fallback-kr);color:var(--ink)">협업 조건 제안</span>
      </div>
      <sc-if value="{{ svNoChannel }}" hint-placeholder-val="{{ true }}">
      <div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:0 28px">
        <span class="sv-kr" style="font:600 15px/20px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">먼저 채널을 연결해주세요</span>
        <span class="sv-kr" style="margin-top:6px;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">연결한 채널이 있어야 협업 조건을 제안할 수 있어요.</span>
        <button onClick="{{ openConnect }}" style="margin-top:18px;height:44px;padding:0 22px;border-radius:var(--radius-lg);background:var(--ink);font:600 14px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">채널 연결하기</button>
      </div>
      </sc-if>
      <sc-if value="{{ !svNoChannel }}" hint-placeholder-val="{{ true }}">
      <div class="sv-scroll" style="flex:1;padding:20px 16px 120px">
        <label class="sv-kr" style="display:block;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">협업 희망 금액</label>
        <div style="margin-top:8px;display:flex;align-items:center;gap:8px;height:var(--field-height);padding:0 14px;border-radius:var(--radius-md);background:var(--surface-field)">
          <input value="{{ svRate }}" onChange="{{ onSvRate }}" onBlur="{{ blurSvRate }}" onFocus="{{ focusSvRate }}" inputmode="numeric" placeholder="150,000" style="flex:1;min-width:0;border:0;outline:none;background:transparent;font:400 15px/1.2 var(--font-body),var(--font-fallback-kr);color:var(--ink)">
          <span style="flex:none;font:500 14px/1.2 var(--font-body-medium),var(--font-fallback-kr);color:var(--text-tertiary)">원</span>
        </div>
        <sc-if value="{{ svRateNote }}" hint-placeholder-val="{{ true }}">
        <p class="sv-kr" style="margin-top:6px;font:400 12px/17px var(--font-body),var(--font-fallback-kr);color:var(--error)">{{ svRateNote }}</p>
        </sc-if>
        <label class="sv-kr" style="display:block;margin-top:22px;font:600 13px/18px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">연장 비용 <span style="color:var(--text-tertiary);font-weight:400">선택</span></label>
        <div style="margin-top:8px;display:flex;align-items:center;gap:8px;height:var(--field-height);padding:0 14px;border-radius:var(--radius-md);background:var(--surface-field)">
          <input value="{{ svExtend }}" onChange="{{ onSvExtend }}" inputmode="numeric" placeholder="50,000" style="flex:1;min-width:0;border:0;outline:none;background:transparent;font:400 15px/1.2 var(--font-body),var(--font-fallback-kr);color:var(--ink)">
          <span style="flex:none;font:500 14px/1.2 var(--font-body-medium),var(--font-fallback-kr);color:var(--text-tertiary)">원</span>
        </div>
        <p class="sv-kr" style="margin-top:6px;font:400 12px/17px var(--font-body),var(--font-fallback-kr);color:var(--text-tertiary)">게시물을 더 오래 유지할 때 받을 금액이에요.</p>
      </div>
      <div style="padding:0 16px 22px">
        <button onClick="{{ openSurveyConfirm }}" disabled="{{ svBlocked }}" style="width:100%;height:52px;border-radius:var(--radius-lg);background:var(--ink);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">조건 확인하기</button>
      </div>
      </sc-if>
    </div>
    </sc-if>
```

- [ ] **Step 6: Confirm it passes**

Reach the form from the Task 10 detail. Type `500` into the rate and **do not blur** — no error should appear (Showing input error §2). Blur — `10,000원 이상으로 적어주세요.` appears. Refocus — it clears. Then set `channels:[]` via `setState` and confirm the empty state replaces the form.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 수요조사 단가 제안 폼 — 희망 금액·연장 비용"
```

---

## Task 12: Demand-survey confirm sheet and completion

**Files:**
- Modify: `creator/main.dc.html` — markup, render object

**Interfaces:**
- Consumes: `svRate`/`svExtend` (Task 11); `sv-done` (Task 7)
- Produces: flags `surveyConfirmOpen`, `svSummary`, `svAgree`, `svConfirmBlocked`, `isSurveyDone`; handlers `toggleSvAgree`, `submitSurvey`, `goSurveyList`; state `svAgree`

- [ ] **Step 1: State the observable check**

Confirming opens a sheet recapping the proposed rate with a required consent checkbox; the confirm button is blocked until it is checked; confirming lands on a success screen.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "surveyConfirmOpen" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add state and flags**

In `state`, beside `svRate:'', svExtend:'',` add `svAgree:false,`.

```js
      surveyConfirmOpen: s.sheet === 'surveyConfirm',
      svSummary: Number(s.svRate || 0).toLocaleString() + '원' + (s.svExtend ? ' · 연장 ' + Number(s.svExtend).toLocaleString() + '원' : ''),
      svAgree: s.svAgree,
      svConfirmBlocked: !s.svAgree,
      toggleSvAgree: () => this.setState((st) => ({ svAgree: !st.svAgree })),
      submitSurvey: () => this.setState({ sheet:null }, () => this.push('surveyDone', {})),
      isSurveyDone: !s.auth && s.route === 'surveyDone',
      goSurveyList: () => this.setState({ route:null, tab:'campaigns', stack:[] }),
```

- [ ] **Step 4: Add the confirm sheet markup**

```html
    <sc-if value="{{ surveyConfirmOpen }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--scrim);z-index:60;display:flex;flex-direction:column;justify-content:flex-end;animation:sv-fade 200ms ease-out">
      <div style="background:var(--canvas);border-radius:var(--radius-3xl) var(--radius-3xl) 0 0;padding:24px 20px 20px;animation:sv-sheet 280ms cubic-bezier(0.32,0.72,0,1)">
        <h2 class="sv-kr" style="font:600 17px/23px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">아래 조건으로 신청할까요?</h2>
        <div style="margin-top:14px;padding:14px;border-radius:var(--radius-md);background:var(--surface-1)">
          <span class="sv-kr" style="display:block;font:600 12px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-tertiary)">제안 금액</span>
          <span class="sv-kr" style="display:block;margin-top:5px;font:600 17px/23px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">{{ svSummary }}</span>
        </div>
        <button onClick="{{ toggleSvAgree }}" style="margin-top:16px;display:flex;align-items:center;gap:10px;width:100%;text-align:left">
          <span style="width:20px;height:20px;flex:none;border-radius:var(--radius-xs);background:var(--surface-2);display:flex;align-items:center;justify-content:center"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="check" variant="line" size="14px" color="var(--ink)" hint-size="14px,14px"></x-import></span>
          <span class="sv-kr" style="flex:1;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">필수 약관에 모두 동의합니다.</span>
        </button>
        <div style="display:flex;gap:8px;margin-top:20px">
          <button onClick="{{ closeSheet }}" style="flex:1;height:50px;border-radius:var(--radius-lg);background:var(--surface-2);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">돌아가기</button>
          <button onClick="{{ submitSurvey }}" disabled="{{ svConfirmBlocked }}" style="flex:1;height:50px;border-radius:var(--radius-lg);background:var(--ink);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">신청하기</button>
        </div>
      </div>
    </div>
    </sc-if>
```

- [ ] **Step 5: Add the completion screen**

Copy the Task 9 markup, changing the flag to `isSurveyDone`, the headline to `조건을 보냈어요`, the middle detail to `{{ svSummary }}`, the tertiary line to `브랜드가 검토하면 알림으로 알려드릴게요.`, and the CTA `onClick` to `{{ goSurveyList }}`.

- [ ] **Step 6: Confirm it passes**

Walk the whole journey C: detail → form → confirm sheet → done. Verify the confirm button is inert until the checkbox is on. Console clean.

- [ ] **Step 7: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 수요조사 조건 확인 시트·완료 화면 — 여정 C 를 잇는다"
```

---

## Task 13: Proposals inbox

**Files:**
- Modify: `creator/main.dc.html` — seed data, markup, render object

**Interfaces:**
- Consumes: `sv-empty` (Task 4), skeletons (Task 2)
- Produces: seed `PROPOSAL_ROWS`; status map `PROP_STATUS` (keys: `채택 대기`, `광고주 검토 중`, `채택됨`, `확정됨`, `미채택`, `거절`, `기한 만료`; each value `{bg, fg, label}`); route `'proposals'`; flags `isProposals`, `propTab`, `propRows`, `propEmpty`; handlers `openProposals()`, `setPropTab(tab)`

- [ ] **Step 1: State the observable check**

A screen with 받은/보낸 tabs listing proposals, each row carrying a status badge from the shared map, with a deadline line where a response is still owed.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "PROP_STATUS" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add the status vocabulary**

Beside the existing `P_STATUS` map:

```js
/** 제안 상태 한 벌. 배지 색은 여기서만 정한다 — 화면마다 다시 고르면 어긋난다. */
const PROP_STATUS = {
  '채택 대기':    { bg:'var(--status-review-bg)', fg:'var(--ink)' },
  '광고주 검토 중':{ bg:'var(--status-review-bg)', fg:'var(--text-secondary)' },
  '채택됨':      { bg:'var(--status-picked-bg)', fg:'var(--ink)' },
  '확정됨':      { bg:'var(--status-passed-bg)', fg:'var(--success)' },
  '미채택':      { bg:'var(--status-cancel-bg)', fg:'var(--text-secondary)' },
  '거절':        { bg:'var(--status-cancel-bg)', fg:'var(--text-secondary)' },
  '기한 만료':    { bg:'var(--status-cancel-bg)', fg:'var(--text-tertiary)' },
};

const PROPOSAL_ROWS = [
  { id:'p1', dir:'received', brand:'글로우드롭', product:'수분 앰플 30ml', status:'채택 대기', dueIn:2, rate:150000 },
  { id:'p2', dir:'received', brand:'모닝루틴',   product:'드립백 커피',    status:'채택됨',   dueIn:5, rate:220000 },
  { id:'p3', dir:'sent',     brand:'스킨레이어', product:'클렌징 폼',      status:'광고주 검토 중', dueIn:0, rate:180000 },
  { id:'p4', dir:'sent',     brand:'데일리핏',   product:'요가 매트',      status:'미채택',   dueIn:0, rate:200000 },
];
```

- [ ] **Step 4: Add state and flags**

In `state`, add `propTab:'received',`.

```js
      isProposals: !s.auth && s.route === 'proposals',
      propTab: s.propTab,
      setPropTab: (t) => this.setState({ propTab: t }),
      propRows: PROPOSAL_ROWS.filter((r) => r.dir === s.propTab).map((r) => ({
        ...r,
        badge: PROP_STATUS[r.status] || PROP_STATUS['채택 대기'],
        rateLine: r.rate.toLocaleString() + '원',
        dueLine: r.dueIn > 0 ? r.dueIn + '일 안에 응답해주세요' : '',
      })),
      propEmpty: !PROPOSAL_ROWS.filter((r) => r.dir === s.propTab).length,
```

In `class Component`, after `openStates()`: `openProposals() { this.push('proposals', {}); }`

- [ ] **Step 5: Add the markup**

```html
    <!-- ─────────── 제안 (받은 · 보낸) ─────────── -->
    <sc-if value="{{ isProposals }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--canvas);z-index:30;display:flex;flex-direction:column;animation:sv-push 350ms cubic-bezier(0.32,0.72,0,1)">
      <div style="display:flex;align-items:center;gap:10px;padding:14px 16px 0">
        <button onClick="{{ goBack }}" style="display:flex"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="arrow-left" variant="line" size="22px" color="var(--ink)" hint-size="22px,22px"></x-import></button>
        <span class="sv-kr" style="flex:1;font:500 17px/23px var(--font-display-medium),var(--font-fallback-kr);color:var(--ink)">제안</span>
      </div>
      <div style="display:flex;gap:6px;padding:14px 16px 0">
        <button onClick="{{ setPropTabReceived }}" style="padding:8px 14px;border-radius:var(--radius-pill);background:var(--surface-2);font:600 13px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">받은</button>
        <button onClick="{{ setPropTabSent }}" style="padding:8px 14px;border-radius:var(--radius-pill);background:var(--surface-1);font:600 13px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-secondary)">보낸</button>
      </div>
      <div class="sv-scroll" style="flex:1;padding:12px 16px 110px">
        <sc-for each="{{ propRows }}" as="row">
        <button onClick="{{ row.open }}" style="display:block;width:100%;text-align:left;margin-top:8px;padding:16px;border-radius:var(--radius-xl);background:var(--surface-1)">
          <div style="display:flex;align-items:center;gap:8px">
            <span class="sv-kr" style="flex:1;font:600 15px/20px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">{{ row.brand }}</span>
            <span class="sv-kr" style="flex:none;padding:4px 9px;border-radius:var(--radius-xs);background:{{ row.badge.bg }};font:600 11px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:{{ row.badge.fg }}">{{ row.status }}</span>
          </div>
          <span class="sv-kr" style="display:block;margin-top:4px;font:400 13px/18px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">{{ row.product }} · {{ row.rateLine }}</span>
          <sc-if value="{{ row.dueLine }}" hint-placeholder-val="{{ true }}">
          <span class="sv-kr" style="display:block;margin-top:8px;font:600 12px/17px var(--font-body-semibold),var(--font-fallback-kr);color:var(--error)">{{ row.dueLine }}</span>
          </sc-if>
        </button>
        </sc-for>
        <sc-if value="{{ propEmpty }}" hint-placeholder-val="{{ true }}">
        <div style="display:flex;flex-direction:column;align-items:center;text-align:center;padding:48px 24px">
          <span class="sv-kr" style="font:600 15px/20px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">아직 제안이 없어요</span>
          <span class="sv-kr" style="margin-top:6px;font:400 13px/19px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">브랜드 존에서 먼저 제안해볼 수 있어요.</span>
        </div>
        </sc-if>
      </div>
    </div>
    </sc-if>
```

Add the two tab handlers and the per-row `open` in the render object:

```js
      setPropTabReceived: () => this.setState({ propTab:'received' }),
      setPropTabSent: () => this.setState({ propTab:'sent' }),
```

and inside the `propRows` map, add `open: () => this.push('proposalDetail', { propId: r.id }),`.

- [ ] **Step 6: Add a MY entry point**

In the MY space menu, beside the existing 페널티 row, add a row labelled `제안` whose `onClick` is `{{ openProposals }}`, and expose `openProposals: () => this.openProposals(),` in the render object.

- [ ] **Step 7: Confirm it passes**

From MY, tap 제안. Expected: 받은 tab lists two rows with distinct badge colors, the 채택 대기 row shows a red deadline line, switching to 보낸 swaps the rows. Console clean.

- [ ] **Step 8: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 제안함 — 받은·보낸 탭과 상태 배지 한 벌"
```

---

## Task 14: Proposal detail

**Files:**
- Modify: `creator/main.dc.html` — markup, render object

**Interfaces:**
- Consumes: `PROPOSAL_ROWS`, `PROP_STATUS` (Task 13); `askConfirm` (Task 6)
- Produces: route `'proposalDetail'`; flags `isPropDetail`, `propRow`, `canRespond`; handlers `acceptProposal`, `rejectProposal`, `reproposeProposal`; state `propId`

- [ ] **Step 1: State the observable check**

Opening a row shows the proposal's terms with accept and reject actions; rejecting routes through the destructive-confirm sheet; a 미채택 row offers 거절 후 다시 제안 instead.

- [ ] **Step 2: Confirm it fails**

```
mcp__Claude_Browser__javascript_tool {"action":"javascript_exec","text":"document.querySelector('x-dc').__dc.setState({auth:null, route:'proposalDetail', propId:'p1'})"}
```

Expected: blank canvas.

- [ ] **Step 3: Add state and flags**

In `state`, add `propId:null,`.

```js
      isPropDetail: !s.auth && s.route === 'proposalDetail',
      propRow: (() => {
        const r = PROPOSAL_ROWS.find((x) => x.id === s.propId) || {};
        return { ...r, badge: PROP_STATUS[r.status] || PROP_STATUS['채택 대기'], rateLine: (r.rate || 0).toLocaleString() + '원' };
      })(),
      canRespond: (() => { const r = PROPOSAL_ROWS.find((x) => x.id === s.propId); return !!r && r.status === '채택 대기'; })(),
      canRepropose: (() => { const r = PROPOSAL_ROWS.find((x) => x.id === s.propId); return !!r && r.status === '미채택'; })(),
      acceptProposal: () => this.setState({ route:null, tab:'campaigns', stack:[] }),
      rejectProposal: () => this.askConfirm({ title:'이 제안을 거절할까요?', body:'거절하면 이 조건은 사라지고, 브랜드에 거절 사실이 전달돼요.', reversible:true, confirmLabel:'거절하기' }),
      reproposeProposal: () => this.push('proposal', {}),
```

- [ ] **Step 4: Add the markup**

```html
    <!-- ─────────── 제안 상세 ─────────── -->
    <sc-if value="{{ isPropDetail }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--canvas);z-index:32;display:flex;flex-direction:column;animation:sv-push 350ms cubic-bezier(0.32,0.72,0,1)">
      <div style="display:flex;align-items:center;gap:10px;padding:14px 16px 0">
        <button onClick="{{ goBack }}" style="display:flex"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="arrow-left" variant="line" size="22px" color="var(--ink)" hint-size="22px,22px"></x-import></button>
        <span class="sv-kr" style="flex:1;font:500 17px/23px var(--font-display-medium),var(--font-fallback-kr);color:var(--ink)">제안 상세</span>
      </div>
      <div class="sv-scroll" style="flex:1;padding:18px 16px 120px">
        <span class="sv-kr" style="display:inline-block;padding:5px 10px;border-radius:var(--radius-xs);background:{{ propRow.badge.bg }};font:600 11px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:{{ propRow.badge.fg }}">{{ propRow.status }}</span>
        <h1 class="sv-kr" style="margin-top:12px;font:500 21px/28px var(--font-display-medium),var(--font-fallback-kr);letter-spacing:-0.3px;color:var(--ink)">{{ propRow.brand }}</h1>
        <p class="sv-kr" style="margin-top:4px;font:400 14px/20px var(--font-body),var(--font-fallback-kr);color:var(--text-secondary)">{{ propRow.product }}</p>
        <div style="margin-top:20px;padding:16px;border-radius:var(--radius-xl);background:var(--surface-1)">
          <span class="sv-kr" style="display:block;font:600 12px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-tertiary)">협업 금액</span>
          <span class="sv-kr" style="display:block;margin-top:5px;font:600 19px/26px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">{{ propRow.rateLine }}</span>
        </div>
      </div>
      <sc-if value="{{ canRespond }}" hint-placeholder-val="{{ true }}">
      <div style="display:flex;gap:8px;padding:0 16px 22px">
        <button onClick="{{ rejectProposal }}" style="flex:1;height:52px;border-radius:var(--radius-lg);background:var(--surface-2);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">거절</button>
        <button onClick="{{ acceptProposal }}" style="flex:2;height:52px;border-radius:var(--radius-lg);background:var(--ink);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">수락하기</button>
      </div>
      </sc-if>
      <sc-if value="{{ canRepropose }}" hint-placeholder-val="{{ true }}">
      <div style="padding:0 16px 22px">
        <button onClick="{{ reproposeProposal }}" style="width:100%;height:52px;border-radius:var(--radius-lg);background:var(--ink);font:600 15px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">새 조건으로 다시 제안</button>
      </div>
      </sc-if>
    </div>
    </sc-if>
```

- [ ] **Step 5: Confirm it passes**

From the inbox tap the 채택 대기 row: accept/reject both present, reject opens the shared confirm sheet with `되돌릴 수 있어요` phrasing. Tap the 미채택 row (`p4`): only 다시 제안 shows. Console clean.

- [ ] **Step 6: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 제안 상세 — 수락·거절·다시 제안"
```

---

## Task 15: Revision-request response

**Files:**
- Modify: `creator/main.dc.html` — seed data (`SEED` collaborations, ~4423), markup, render object

**Interfaces:**
- Consumes: upload trio markup (Task 5); `askConfirm` (Task 6)
- Produces: route `'revision'`; flags `isRevision`, `revReason`, `revDue`; handler `openRevision(id)`

The audit's finding: the prototype has a `수정 요청` status but nowhere to read the advertiser's reason or re-upload. Both are added here.

- [ ] **Step 1: State the observable check**

Opening a `수정 요청` collaboration shows the advertiser's rejection reason verbatim and a re-upload area carrying the Task 5 constraints.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "isRevision" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Extend the seed row with a reason**

The existing seed row at ~4423 is `{ id:'seed-3', offerId:'o7', status:'수정 요청', task:'반려 사유 1건 · 재등록', dueIn:5, stage:4, kind:'delivery' }`. Add the advertiser's message:

```js
      revReason:'조명이 어두워 제품 색이 실제와 달라 보여요. 자연광에서 다시 찍어주시면 좋겠어요.', revDue:'8월 24일',
```

- [ ] **Step 4: Add the flags**

```js
      isRevision: !s.auth && s.route === 'revision',
      revReason: (this.collab(s.reviewId) || {}).revReason || '',
      revDue: (this.collab(s.reviewId) || {}).revDue || '',
      openRevision: () => this.push('revision', {}),
```

If no `collab(id)` helper exists, add one beside `offer(id)`:

```js
  collab(id) { return (this.state.applications || []).find((a) => a.id === id) || null; }
```

- [ ] **Step 5: Add the markup**

```html
    <!-- ─────────── 수정 요청 대응 ─────────── -->
    <sc-if value="{{ isRevision }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--canvas);z-index:32;display:flex;flex-direction:column;animation:sv-push 350ms cubic-bezier(0.32,0.72,0,1)">
      <div style="display:flex;align-items:center;gap:10px;padding:14px 16px 0">
        <button onClick="{{ goBack }}" style="display:flex"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="arrow-left" variant="line" size="22px" color="var(--ink)" hint-size="22px,22px"></x-import></button>
        <span class="sv-kr" style="flex:1;font:500 17px/23px var(--font-display-medium),var(--font-fallback-kr);color:var(--ink)">수정 요청</span>
      </div>
      <div class="sv-scroll" style="flex:1;padding:18px 16px 120px">
        <div style="padding:16px;border-radius:var(--radius-xl);background:var(--status-cancel-bg)">
          <span class="sv-kr" style="display:block;font:600 12px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-tertiary)">광고주 메시지</span>
          <p class="sv-kr" style="margin-top:8px;font:400 14px/22px var(--font-body),var(--font-fallback-kr);color:var(--ink)">{{ revReason }}</p>
        </div>
        <p class="sv-kr" style="margin-top:12px;font:600 13px/19px var(--font-body-semibold),var(--font-fallback-kr);color:var(--error)">{{ revDue }}까지 다시 올려주세요</p>
        <h2 class="sv-kr" style="margin-top:26px;font:600 14px/19px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">수정한 영상 올리기</h2>
        <div style="margin-top:10px;display:flex;flex-direction:column;align-items:center;text-align:center;padding:28px 20px;border-radius:var(--radius-xl);border:1px dashed var(--hairline);background:var(--surface-1)">
          <x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="upload" variant="line" size="24px" color="var(--text-secondary)" hint-size="24px,24px"></x-import>
          <span class="sv-kr" style="margin-top:10px;font:600 14px/19px var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">수정한 영상을 올려주세요</span>
          <span class="sv-kr" style="margin-top:5px;font:400 12px/17px var(--font-body),var(--font-fallback-kr);color:var(--text-tertiary)">MP4·MOV · 500MB 이하</span>
          <button style="margin-top:14px;height:40px;padding:0 18px;border-radius:var(--radius-lg);background:var(--ink);font:600 13px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:#fff">파일 선택</button>
        </div>
      </div>
    </div>
    </sc-if>
```

- [ ] **Step 6: Link it from the collaboration row**

On the `수정 요청` row in 내 협업, set the row's `onClick` to `{{ openRevision }}` so the status is actionable rather than informational.

- [ ] **Step 7: Confirm it passes**

From 내 협업, tap the `수정 요청` row. Expected: the advertiser's sentence appears verbatim, the deadline is visible, and the upload area names both constraints before any attempt.

- [ ] **Step 8: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 수정 요청 대응 — 반려 사유 원문과 재업로드"
```

---

## Task 16: Payout history

Implements the spec's "both" payout model: automatic monthly payouts and early-withdrawal requests share one timeline.

**Files:**
- Modify: `creator/main.dc.html` — seed data, markup, render object

**Interfaces:**
- Consumes: `sv-empty` (Task 4)
- Produces: seed `PAYOUTS`; route `'payoutHistory'`; flags `isPayoutHistory`, `payoutRows`, `paidTotal`, `nextAccrued`; handler `openPayoutHistory()`

- [ ] **Step 1: State the observable check**

A history screen where each row's *kind* — automatic or early request — is legible without reading the amount, plus totals for received-to-date and next-payout accrual.

- [ ] **Step 2: Confirm it fails**

```bash
grep -c "payoutHistory" creator/main.dc.html
```

Expected: `0`.

- [ ] **Step 3: Add seed data**

```js
/** 정산은 두 갈래다 — 매월 자동 지급과 조기 인출 신청. 한 타임라인에 섞이므로
 *  줄의 첫 자리(종류)와 끝자리(상태)가 무엇인지 먼저 읽히게 둔다. */
const PAYOUTS = [
  { id:'po1', kind:'자동', date:'2026.08.10', amount:420000, status:'지급 완료' },
  { id:'po2', kind:'조기', date:'2026.07.28', amount:150000, status:'지급 처리 중' },
  { id:'po3', kind:'자동', date:'2026.07.10', amount:380000, status:'지급 완료' },
  { id:'po4', kind:'자동', date:'2026.06.10', amount:3200,   status:'이월' },
];
```

- [ ] **Step 4: Add the flags**

```js
      isPayoutHistory: !s.auth && s.route === 'payoutHistory',
      payoutRows: PAYOUTS.map((p) => ({
        ...p,
        amountLine: p.amount.toLocaleString() + '원',
        kindBg: p.kind === '자동' ? 'var(--surface-2)' : 'var(--status-review-bg)',
        statusFg: p.status === '이월' ? 'var(--text-tertiary)' : p.status === '지급 처리 중' ? 'var(--text-secondary)' : 'var(--success)',
        note: p.status === '이월' ? '5,000원 미만이라 다음 달로 넘어갔어요' : '',
      })),
      paidTotal: PAYOUTS.filter((p) => p.status === '지급 완료').reduce((a, p) => a + p.amount, 0).toLocaleString() + '원',
      nextAccrued: '86,400원',
      openPayoutHistory: () => this.push('payoutHistory', {}),
```

- [ ] **Step 5: Add the markup**

```html
    <!-- ─────────── 지급 내역 ─────────── -->
    <sc-if value="{{ isPayoutHistory }}" hint-placeholder-val="{{ true }}">
    <div style="position:absolute;inset:0;background:var(--canvas);z-index:30;display:flex;flex-direction:column;animation:sv-push 350ms cubic-bezier(0.32,0.72,0,1)">
      <div style="display:flex;align-items:center;gap:10px;padding:14px 16px 0">
        <button onClick="{{ goBack }}" style="display:flex"><x-import component-from-global-scope="StartViewDesignSystem_7416ce.Icon" name="arrow-left" variant="line" size="22px" color="var(--ink)" hint-size="22px,22px"></x-import></button>
        <span class="sv-kr" style="flex:1;font:500 17px/23px var(--font-display-medium),var(--font-fallback-kr);color:var(--ink)">지급 내역</span>
      </div>
      <div style="display:flex;gap:8px;padding:16px 16px 0">
        <div style="flex:1;padding:14px;border-radius:var(--radius-xl);background:var(--surface-1)">
          <span class="sv-kr" style="display:block;font:600 11px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-tertiary)">지금까지 받은 금액</span>
          <span style="display:block;margin-top:5px;font:700 17px/23px var(--font-display-bold);color:var(--ink)">{{ paidTotal }}</span>
        </div>
        <div style="flex:1;padding:14px;border-radius:var(--radius-xl);background:var(--surface-1)">
          <span class="sv-kr" style="display:block;font:600 11px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--text-tertiary)">다음 지급 예정</span>
          <span style="display:block;margin-top:5px;font:700 17px/23px var(--font-display-bold);color:var(--ink)">{{ nextAccrued }}</span>
        </div>
      </div>
      <div class="sv-scroll" style="flex:1;padding:16px 16px 110px">
        <sc-for each="{{ payoutRows }}" as="row">
        <div style="margin-top:8px;padding:16px;border-radius:var(--radius-xl);background:var(--surface-1)">
          <div style="display:flex;align-items:center;gap:8px">
            <span class="sv-kr" style="flex:none;padding:4px 9px;border-radius:var(--radius-xs);background:{{ row.kindBg }};font:600 11px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:var(--ink)">{{ row.kind }}</span>
            <span style="flex:1;font:400 12px/1.2 var(--font-body);color:var(--text-tertiary)">{{ row.date }}</span>
            <span class="sv-kr" style="flex:none;font:600 12px/1.2 var(--font-body-semibold),var(--font-fallback-kr);color:{{ row.statusFg }}">{{ row.status }}</span>
          </div>
          <span style="display:block;margin-top:8px;font:600 17px/23px var(--font-body-semibold);color:var(--ink)">{{ row.amountLine }}</span>
          <sc-if value="{{ row.note }}" hint-placeholder-val="{{ true }}">
          <span class="sv-kr" style="display:block;margin-top:5px;font:400 12px/17px var(--font-body),var(--font-fallback-kr);color:var(--text-tertiary)">{{ row.note }}</span>
          </sc-if>
        </div>
        </sc-for>
      </div>
    </div>
    </sc-if>
```

The row puts kind first and status last — the two positions Serial Position Effect says are recalled — with the amount in between.

- [ ] **Step 6: Link it from the settlement screen**

On the 정산 screen, add a row `지급 내역` whose `onClick` is `{{ openPayoutHistory }}`, and expose `openPayoutHistory` in the render object.

- [ ] **Step 7: Confirm it passes**

From MY → 정산 → 지급 내역. Expected: four rows, the 자동 and 조기 badges visually distinct, the 이월 row carrying its explanatory note, both totals populated.

- [ ] **Step 8: Commit**

```bash
git add creator/main.dc.html
git commit -m "feat(creator): 지급 내역 — 자동 지급과 조기 인출을 한 타임라인에"
```

---

## Phase 1 gate

Walk every journey in the spec §7 from start to finish and confirm none dead-ends:

- **A. Campaign participation** — discover → detail → apply → **complete** → my collaborations
- **B. Brand proposal** — brand zone → proposal → **sent inbox** → **detail** → accept
- **C. Demand survey** — **detail → form → confirm → done**
- **Revision** — collaboration list → **수정 요청 row → reason + re-upload**
- **Settlement** — MY → 정산 → **지급 내역**

Then run `read_console_messages {"onlyErrors":true}` once more on each. Any error blocks the gate.

---

## Self-review notes

**Spec coverage.** Spec §4 items 0.1–0.7 → Tasks 2–8. Spec §5 Phase 1's nine screens → Tasks 9–16 (Task 12 carries two: confirm sheet and completion). Spec §6 acceptance rows for Phases 0 and 1 → the two gates. Task 1 has no spec section; it exists because Phase 0 is otherwise unreviewable, and is noted as such.

**Deferred to later plans.** Spec §5 Phases 2–5 are out of scope here by design — one plan per phase, per the scope check. Spec §8's three open questions all touch Phase 5 only and block nothing in this plan.

**Naming consistency.** `askConfirm`/`closeConfirm` (Task 6) are used unchanged in Tasks 14 and 15. `markTouched`/`clearTouched` (Task 8) are used unchanged in Task 11. `PROP_STATUS` (Task 13) is consumed unchanged in Task 14. `sv-done` (Task 7) is instantiated in Tasks 9 and 12. The upload markup (Task 5) is instantiated in Task 15.

**Known soft spot.** Task 15 assumes a `collab(id)` lookup; if the codebase already exposes an equivalent under a different name, use the existing one rather than adding a second — Step 4 says so explicitly.
