# AIEditorRSP — Account / Editor / Quota / Hero UX Handoff

- task_id: t_e7dd01b6
- owner: 墨影
- site: https://aieditorrsp.net/
- repo: /root/projects/aieditorrsp
- generated_at: 2026-06-04 09:50 UTC
- verdict: DESIGN_CONDITIONAL_GO

## 0. 结论

DESIGN_CONDITIONAL_GO。当前生产页已经有可用的暗色工业风、上传入口、prompt textarea、模板条、Generate 和 result status；但 Header/account 状态机仍是 P0 断点，Upgrade 仍不是清晰付费动作，logout 不可发现，account loading/error 缺失。Hero 工具区在 desktop 1366 首屏勉强可见但仍过高；mobile 390 工具 console 高 764px，压住首屏价值，需要继续压缩。

本 handoff 给墨界/墨枢的实现口径：不是重做整站视觉，而是在现有 design system 上补 AccountState 组件、Quota action card、editor 状态结构、首屏高度约束和 mobile compact mode。

## 1. 生产复核证据

### 1.1 读取来源

- Owner brief: `/root/.hermes/reports/aieditorrsp-product-closeout-20260604/owner-feedback-brief.md`
- Product Contract: `/root/.hermes/reports/aieditorrsp-product-closeout-20260604/product-contract.md`
- Code reviewed:
  - `/root/projects/aieditorrsp/src/components/AuthStatusLink.tsx`
  - `/root/projects/aieditorrsp/src/components/ProductPreviewEditor.tsx`
  - `/root/projects/aieditorrsp/src/app/globals.css`

### 1.2 Browser measurement

Active production measurement at `/`:

```json
{
  "desktop_1366x768": {
    "hero": "1180x745, bottom 809",
    "copy": "491x339, bottom 463",
    "editor": "600x631, bottom 755",
    "generate": "bottom 503",
    "templates": "height 168, bottom 682",
    "status": "height 72, bottom 754",
    "header_account_text": "SIGN IN / 0 FREE EDITS LEFT"
  },
  "mobile_390x844": {
    "hero": "386x944, bottom 1008",
    "copy": "354x108, bottom 190",
    "editor": "354x764, bottom 968",
    "generate": "bottom 769",
    "templates": "height 115, bottom 895",
    "status": "height 72, bottom 967"
  }
}
```

Design reading:

- Desktop: 工具区视觉重量仍偏大，但 Generate 与主任务可见；可以通过压缩 template strip/status/account 进入 GO。
- Mobile: 当前 console 过长，首屏几乎被工具区吞掉；必须切换成 progressive compact flow。
- Header: anonymous 只有 Sign in + 0 free edits left，缺同屏 pricing/upgrade CTA；登录态仍缺 logout/menu/loading/error/free_exhausted/pro_paid 的完整视觉。

## 2. Design principle

一屏内必须回答四个问题：

1. Who am I? — anonymous/free/pro/error/loading。
2. What can I do now? — upload / generate / download / retry。
3. How many edits remain? — credits/remaining/free/paid。
4. If blocked, what is the paid/login action? — Sign in / Upgrade / Buy credits / View pricing / Checkout。

视觉优先级：

1. Hero H1 + one-line promise。
2. Upload source + prompt + Generate。
3. Account/credits compact state。
4. Result preview / quota blocker。
5. Templates 作为辅助，不应撑高首屏。

## 3. Header AccountState component spec

替换当前 `AuthStatusLink` 的单链接模型。新组件建议命名：`AccountStateControl`。

### 3.1 Data model

组件输入应吃 `/api/credits` 的稳定字段，建议墨枢补 `state` 枚举；前端可先 derive。

```ts
type AccountVisualState =
  | "loading"
  | "error"
  | "anonymous_ok"
  | "anonymous_exhausted"
  | "free_ok"
  | "free_exhausted"
  | "pro_paid";

type CreditsPayload = {
  authenticated: boolean;
  user?: { email?: string; name?: string | null; avatar_url?: string | null } | null;
  plan?: "free" | "pro" | "paid" | "monthly" | "yearly" | string;
  remaining?: number;
  free_remaining?: number;
  paid_remaining?: number;
  paid_enabled?: boolean;
  checkout?: { monthly?: string; yearly?: string; credit_pack?: string };
  recommended_actions?: string[];
  state?: AccountVisualState;
};
```

Derived state fallback:

```ts
function deriveAccountState(c?: CreditsPayload | null, isLoading?: boolean, isError?: boolean): AccountVisualState {
  if (isLoading) return "loading";
  if (isError || !c) return "error";
  const remaining = Number(c.remaining ?? c.free_remaining ?? 0);
  const paid = Number(c.paid_remaining ?? 0);
  const plan = String(c.plan ?? "free").toLowerCase();
  const pro = c.authenticated && (paid > 0 || ["pro", "paid", "monthly", "yearly"].includes(plan));
  if (pro) return "pro_paid";
  if (!c.authenticated && remaining <= 0) return "anonymous_exhausted";
  if (!c.authenticated) return "anonymous_ok";
  if (remaining <= 0) return "free_exhausted";
  return "free_ok";
}
```

### 3.2 Desktop layout

Header right area must be one compact group, not several competing links.

Structure:

```tsx
<div className={`account-state account-state--${state}`} data-account-state={state}>
  <button className="account-state__summary" aria-haspopup="menu" aria-expanded={open}>
    <AvatarOrIcon />
    <span className="account-state__copy">
      <strong>{primaryLabel}</strong>
      <small>{secondaryLabel}</small>
    </span>
  </button>
  <a className="account-state__cta" href={primaryCtaHref} data-event={primaryCtaEvent}>{primaryCtaLabel}</a>
  {open && <AccountMenu />}
</div>
```

Sizing:

- `.account-state`: max-width 310px desktop; height 44px; display flex; gap 8px。
- Summary: 176–198px, border 1px #303844, background #15191f。
- CTA: 92–110px, button-level background #ffb000 for upgrade/pricing, teal outline for retry/manage。
- No wrapping in header; truncate email/name。
- Logout lives inside dropdown, not as permanent top-level button。

### 3.3 Mobile layout

Mobile header remains compact. Account state goes into nav drawer/menu, with a small top pill optional.

Mobile drawer card:

```tsx
<div className="mobile-account-card" data-account-state={state}>
  <div className="mobile-account-card__row">
    <AvatarOrIcon />
    <div>
      <strong>{primaryLabel}</strong>
      <small>{secondaryLabel}</small>
    </div>
  </div>
  <div className="mobile-account-card__actions">
    {primaryAction}
    {secondaryAction}
    {logoutIfSignedIn}
  </div>
</div>
```

Rules:

- Mobile card height target: 92–128px。
- Touch target: 44px min。
- Do not put full email + all credit numbers in sticky header; use drawer.
- If quota exhausted, primary action is full-width yellow `Upgrade to Pro` or `Sign in to continue`.

## 4. Account visual states

### 4.1 loading

Desktop:

- Primary: skeleton bar, label hidden.
- Secondary: `Checking credits…`
- CTA: disabled skeleton chip.
- Width fixed same as loaded state to avoid layout jump.

Copy:

- primary: `Checking account`
- secondary: `Credits loading…`
- CTA: disabled `…`

CSS:

```css
.account-state--loading .account-state__summary,
.account-state--loading .account-state__cta{
  color:transparent;
  background:linear-gradient(90deg,#15191f,#242b34,#15191f);
  background-size:200% 100%;
  animation:account-pulse 1.2s linear infinite;
}
@keyframes account-pulse{to{background-position:-200% 0}}
```

### 4.2 error

Desktop:

- Icon: warning / sync_problem。
- Primary: `Account unavailable`
- Secondary: `Credits could not load`
- CTA: `Retry`
- Dropdown action: `Sign in`, `Pricing` optional.

Do not display as anonymous.

Events: `account_state_error`, retry click can fire `account_state_retry`.

### 4.3 anonymous_ok

Desktop:

- Primary: `Not signed in`
- Secondary: `{free_remaining} free edits left`
- CTA: `Sign in`
- Secondary link in dropdown: `View pricing`

If `free_remaining` unknown, show `Free edits available` not `0`.

Header label:

```text
Not signed in
3 free edits left
[Sign in]
```

### 4.4 anonymous_exhausted

Desktop:

- Primary: `Free edits used`
- Secondary: `Sign in to continue`
- CTA: `Upgrade`
- Dropdown: `Sign in`, `View pricing`

CTA href:

- Upgrade: `/api/auth/login?return_to=/pricing`
- Pricing: `/pricing`

Visual:

- Summary border #ffb000 at 35% opacity。
- CTA yellow filled。
- This is conversion critical; do not reduce to grey text.

### 4.5 free_ok

Desktop:

- Avatar initials/photo.
- Primary: name/email truncated.
- Secondary: `Free · {remaining} edits left`
- CTA: `Upgrade`
- Dropdown: `Account`, `Pricing`, `Logout`

Logout:

- visible menu item, not buried.
- calls `/api/auth/logout?return_to=/` or POST `/api/auth/logout`.
- event: `logout_click` before navigation.

### 4.6 free_exhausted

Desktop:

- Primary: `Free plan`
- Secondary: `0 edits left`
- CTA: `Upgrade`
- Dropdown: `Buy credits`, `View pricing`, `Logout`

Visual:

- CTA yellow filled。
- Summary uses warning dot。
- Do not show generic `Upgrade/Billing` text.

### 4.7 pro_paid

Desktop:

- Primary: name/email truncated.
- Secondary: `Pro · {paid_remaining || remaining} credits`
- CTA: `Manage`
- Dropdown: `Account`, `Buy credits`, `Logout`

Rule:

- Do not keep pushing same Pro plan as primary CTA.
- Pricing page should highlight current plan separately; header only says Manage/Account.

## 5. Account menu behavior

Menu items by state:

| State | Primary CTA | Menu items |
|---|---|---|
| loading | disabled | none |
| error | Retry | Sign in, Pricing |
| anonymous_ok | Sign in | View pricing |
| anonymous_exhausted | Upgrade | Sign in, View pricing |
| free_ok | Upgrade | Account, Pricing, Logout |
| free_exhausted | Upgrade | Buy credits, View pricing, Logout |
| pro_paid | Manage | Account, Buy credits, Logout |

Implementation note:

```tsx
async function logout() {
  window.trackAieRsp?.("logout_click", { surface: "header" });
  await fetch("/api/auth/logout", { method: "POST" }).catch(() => undefined);
  window.dispatchEvent(new Event("aieditorrsp:credits-refresh"));
  window.location.assign("/");
}
```

## 6. Editor state design

Current `ProductPreviewEditor` has most state hooks, but event names and layout hierarchy need tightening.

### 6.1 Empty state

Goal:首屏像产品入口，不像长 console。

Visible order:

1. Upload source image card。
2. Prompt textarea。
3. Generate button。
4. Compact style chips。
5. Collapsed result status line。

Empty state result should be a single strip, not large card:

```tsx
<div className="editor-status-strip editor-status-strip--idle">
  <span className="status-dot" />
  <strong>Preview will appear here</strong>
  <small>Upload + prompt + generate.</small>
</div>
```

Target height: 48–56px desktop, 44–52px mobile。

### 6.2 Uploaded preview

Required elements:

- thumbnail 112–156px desktop, 88–112px mobile。
- filename truncated middle/end。
- file meta: size + type。
- Replace image。
- Remove image。

Layout:

```tsx
<div className="source-preview-card" data-editor-state="uploaded">
  <img className="source-preview-card__thumb" src={previewUrl} alt="Selected source preview" />
  <div className="source-preview-card__meta">
    <strong>{file.name}</strong>
    <small>{formatBytes(file.size)} · {file.type}</small>
    <div className="source-preview-card__actions">
      <button>Replace</button>
      <button>Remove</button>
    </div>
  </div>
</div>
```

Event:

```ts
trackEditorEvent("upload_image", {
  surface,
  file_type: file.type || "unknown",
  file_size_bucket: bucketFileSize(file.size),
});
```

### 6.3 Loading state

Generate button:

- disabled。
- label: `Generating… 1/3` / `Generating… 2/3` / `Generating… 3/3`。
- show spinner but do not change button width.

Status panel:

- Step line only in hero compact mode。
- Full editor page can show 3 rows.

Copy:

- 1/3 `Checking credits and upload`
- 2/3 `Applying style prompt`
- 3/3 `Preparing preview and credit update`

Events:

- On click: `generate_click` with `surface`, `template`, `has_source_image`, `prompt_length_bucket`。
- Do not use only `tool_start`.

### 6.4 Result state

Hero compact mode:

- Replace idle strip with result card.
- Show result thumbnail 72–96px on desktop hero, 64px mobile.
- Actions: `Download` primary, `Open`, `Copy prompt`, `Try another`.
- `Edit prompt` can be icon/button secondary.

Full editor page:

- Larger result preview OK, max-height 360px desktop / 240px mobile。

Copy:

- title: `Your edited image is ready`
- detail: `Download it, open it full size, or reuse the prompt for another pass.`

Events:

- Success: `generate_success` with `surface`, `template`, `has_image`, `credits_refreshed: true|false`。
- Copy: `prompt_copy` with `surface: "editor_result"`.

### 6.5 Quota exhausted state

This must be visually distinct from generic error.

```tsx
<div className="quota-action-card" data-editor-state="quota_exhausted">
  <p className="studio-badge"><span />CREDITS REQUIRED</p>
  <h3>Credits needed to continue</h3>
  <p>{state-specific copy}</p>
  <div className="quota-action-card__actions">
    <a className="primary" href={primaryHref}>Upgrade to Pro</a>
    <a href={secondaryHref}>Buy credit pack</a>
    <a href="/pricing">View pricing</a>
  </div>
</div>
```

Anonymous quota copy:

- title: `Sign in or upgrade to keep editing`
- detail: `Free edits are used on this browser. Sign in before checkout so credits attach to your account.`
- actions: `Sign in`, `View pricing`, `Upgrade to Pro`.

Signed-in free quota copy:

- title: `Credits needed to continue`
- detail: `Your free edits are used. Upgrade monthly or buy a credit pack to generate the next preview.`
- actions: `Upgrade to Pro`, `Buy credit pack`, `View pricing`.

Events:

- On entering state: `quota_exhausted`.
- Pricing click: `pricing_click`.
- Checkout route: `checkout_start` before navigation where possible.

### 6.6 Error state

Generic errors are not quota blockers.

Provider unavailable:

- title: `Provider unavailable`
- detail: `Generation did not complete. No credits are charged before a usable provider result.`
- actions: `Try again`, `Copy prompt`, `View status/pricing` optional。

Upload required:

- title: `Upload required`
- detail: `Choose a JPG, PNG, or WebP before generating.`
- action: focus upload。

Network error:

- title: `Network error`
- detail: `Unable to reach the generation service. Retry when the connection is stable.`
- action: retry。

Events: `generate_error` with `code`, not just `tool_result`.

## 7. First-screen proportion repair

### 7.1 Desktop target

For desktop 1280 / 1366 / 1440:

- Hero max width: 1180px retained。
- Hero grid target: left 44%, right 56%。
- Hero top/bottom padding: 44–52px desktop, not 60+ if editor is tall。
- Editor hero card target height:
  - 1280x720: max 540px。
  - 1366x768: max 560px。
  - 1440x900: max 590px。
- Current desktop 1366 editor is 631px; reduce by ~70px.

What to compress:

1. Topline: 60px → 44px.
2. Upload/prompt grid: keep 172px max, prompt textarea 88–96px。
3. Hero footer: 62px → 48px。
4. Template strip: 168px → 92–112px; cards become chips/cards, no `em` description in hero。
5. Idle/result status: 72px → 48–56px in idle; expandable result/quota only after action.

Recommended CSS:

```css
.studio-hero--tool{
  grid-template-columns:minmax(360px,.88fr) minmax(540px,1.12fr);
  gap:38px;
  padding-top:48px;
  padding-bottom:44px;
  align-items:start;
}
.editor-console--hero{max-height:560px;}
.editor-console--hero .editor-console__topline{min-height:44px;padding:10px 14px;}
.hero-editor-grid{grid-template-columns:minmax(190px,.92fr) minmax(260px,1.08fr);gap:10px;padding:12px 14px 8px;}
.editor-console--hero .editor-upload,
.editor-console--hero .editor-prompt-card{min-height:154px;}
.editor-console--hero .editor-prompt-card textarea{min-height:82px;max-height:96px;}
.hero-editor-footer{min-height:50px;padding:0 14px 10px;display:flex;align-items:center;gap:12px;}
.editor-template-strip{padding:0 14px 8px;}
.editor-template-strip>div{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;}
.editor-template-strip .editor-template{min-height:54px;grid-template-columns:20px 1fr;padding:8px;}
.editor-template-strip .editor-template__copy small,
.editor-template-strip .editor-template__copy em{display:none;}
.editor-home-status{min-height:48px;max-height:56px;padding:8px 14px;display:grid;grid-template-columns:auto 1fr;align-items:center;gap:10px;}
.editor-home-status h3{font-size:14px;line-height:1.2;margin:0;}
.editor-home-status p:not(.studio-badge){display:none;}
```

If result/quota state appears, allow card to expand below fold rather than pushing initial hero:

```css
.editor-home-status--ready:has(img),
.editor-home-status--error{
  max-height:none;
}
```

If `:has()` compatibility is a concern, add state class `editor-home-status--has-result`.

### 7.2 1280 fallback

At <=1280 width, avoid forcing same two-column density.

```css
@media (max-width:1280px) and (min-width:981px){
  .studio-hero--tool{grid-template-columns:minmax(340px,.82fr) minmax(520px,1.18fr);gap:28px;padding-left:24px;padding-right:24px;}
  .studio-hero--tool h1{font-size:clamp(40px,4vw,52px);}
  .studio-hero--tool .studio-hero__copy>p:not(.studio-badge){font-size:16px;line-height:1.55;}
  .editor-console--hero{max-height:540px;}
}
```

### 7.3 Mobile 390/430 target

Current 390 mobile: editor 764px, hero 944px. Target:

- Copy block: 120–150px。
- Editor compact block before Generate: 330–420px。
- Generate visible by 560–620px from viewport top。
- Template strip not fully expanded above fold; show horizontal chips or disclosure.
- Result idle status can be hidden behind one 44px strip.

Mobile flow:

1. H1 + one-line subhead。
2. Upload source compact。
3. Prompt textarea compact。
4. Generate button。
5. Optional `Choose style` horizontal chips (after Generate or one-row before Generate)。
6. Status strip。

Mobile CSS:

```css
@media (max-width:560px){
  .studio-hero--tool{padding-top:22px;padding-bottom:32px;gap:16px;}
  .studio-hero--tool h1{font-size:34px;line-height:1.06;max-width:360px;}
  .studio-hero--tool .studio-hero__copy>p:not(.studio-badge){display:block;font-size:14px;line-height:1.45;max-height:42px;overflow:hidden;}
  .studio-hero__actions{display:none;}
  .editor-console--hero{border-radius:8px;max-height:none;}
  .editor-console--hero .editor-console__topline{min-height:38px;padding:8px 12px;}
  .hero-editor-grid{grid-template-columns:1fr;padding:10px 12px 8px;gap:8px;}
  .editor-console--hero .editor-upload{min-height:108px;}
  .editor-console--hero .editor-prompt-card{min-height:116px;}
  .editor-console--hero .editor-prompt-card textarea{min-height:72px;max-height:78px;font-size:13px;}
  .hero-editor-footer{padding:0 12px 8px;gap:8px;}
  .hero-editor-footer>span{display:none;}
  .editor-console--hero .editor-generate{height:46px;width:100%;}
  .editor-template-strip{padding:0 12px 8px;}
  .editor-template-strip>span{display:none;}
  .editor-template-strip>div{display:flex;gap:8px;overflow-x:auto;scrollbar-width:none;}
  .editor-template-strip .editor-template{flex:0 0 auto;min-width:116px;min-height:40px;padding:7px 8px;display:flex;align-items:center;gap:7px;}
  .editor-template-strip .editor-template__thumb{width:18px;height:18px;}
  .editor-template-strip .editor-template__copy strong{font-size:12px;}
  .editor-template-strip .editor-template__copy small,
  .editor-template-strip .editor-template__copy em{display:none;}
  .editor-home-status{min-height:44px;max-height:50px;padding:8px 12px;}
  .editor-home-status .studio-badge{margin:0;font-size:10px!important;}
  .editor-home-status h3{font-size:13px;margin:0;}
  .editor-home-status p:not(.studio-badge){display:none;}
}
```

## 8. CSS token patch

Add these tokens to `globals.css` or convert to Tailwind variables if preferred.

```css
:root{
  --aie-bg:#080a0d;
  --aie-surface:#15191f;
  --aie-surface-2:#1b2028;
  --aie-border:#303844;
  --aie-text:#f4efe6;
  --aie-muted:#a9b0b8;
  --aie-primary:#ffb000;
  --aie-teal:#43ddbc;
  --aie-danger:#ff5b7c;
  --aie-warning:#ffd597;
}

.account-state{display:flex;align-items:center;gap:8px;height:44px;max-width:310px;min-width:0;}
.account-state__summary{height:44px;min-width:0;width:188px;display:flex;align-items:center;gap:9px;padding:0 10px;border:1px solid var(--aie-border);background:rgba(21,25,31,.92);color:var(--aie-text);border-radius:8px;cursor:pointer;}
.account-state__avatar{width:28px;height:28px;border-radius:6px;display:grid;place-items:center;flex:0 0 auto;background:rgba(67,221,188,.14);color:var(--aie-teal);font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:800;overflow:hidden;}
.account-state__avatar img{width:100%;height:100%;object-fit:cover;}
.account-state__copy{min-width:0;display:flex;flex-direction:column;align-items:flex-start;line-height:1.1;}
.account-state__copy strong{max-width:128px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;letter-spacing:.02em;}
.account-state__copy small{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--aie-muted);font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.04em;text-transform:uppercase;}
.account-state__cta{height:44px;min-width:94px;display:inline-flex;align-items:center;justify-content:center;padding:0 14px;border-radius:8px;background:var(--aie-primary);color:#130d05;text-decoration:none;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:800;text-transform:uppercase;white-space:nowrap;}
.account-state--pro_paid .account-state__cta,
.account-state--error .account-state__cta{background:transparent;color:var(--aie-teal);border:1px solid rgba(67,221,188,.5);}
.account-state--free_exhausted .account-state__summary,
.account-state--anonymous_exhausted .account-state__summary{border-color:rgba(255,176,0,.55);box-shadow:0 0 0 1px rgba(255,176,0,.08),0 0 26px rgba(255,176,0,.08);}
.account-menu{position:absolute;right:0;top:54px;width:220px;padding:8px;background:#0b0d10;border:1px solid var(--aie-border);border-radius:10px;box-shadow:0 24px 80px rgba(0,0,0,.45);z-index:80;}
.account-menu a,.account-menu button{width:100%;min-height:38px;display:flex;align-items:center;justify-content:space-between;padding:0 10px;border:0;background:transparent;color:var(--aie-text);text-decoration:none;font:12px 'JetBrains Mono',monospace;text-transform:uppercase;cursor:pointer;}
.account-menu a:hover,.account-menu button:hover{background:rgba(67,221,188,.08);color:var(--aie-teal);}
.account-menu__danger{color:var(--aie-warning)!important;}
```

## 9. Frontend implementation notes

### 9.1 Replace `AuthStatusLink.tsx`

Current issues:

- `credits` initial null renders `Sign in` instead of loading。
- catch block swallows error and keeps sign-in state。
- signed-in state is one link to `/pricing`; no menu/logout。
- anonymous has no pricing/upgrade CTA in the same group。
- no state class / data selector for QA。

Required implementation:

- Track `loading`, `error`, `open` menu state。
- Derive account state。
- Use separate summary button + primary CTA + menu。
- Add `data-account-state`, `data-auth-action`, `data-plan`, `data-credits-remaining`.
- Add logout POST/GET path and event。

### 9.2 Update `ProductPreviewEditor.tsx`

Current issues:

- event names still `tool_start` / `tool_result` instead of contract events。
- upload preview exists but event missing。
- quota card is present but generic action handling does not vary anonymous vs signed-in free。
- hero idle status and templates still too tall。

Required changes:

- On file select: emit `upload_image`。
- On generate click: emit `generate_click` before request。
- On success: emit `generate_success`。
- On error: emit `generate_error`; if quota/login error also emit `quota_exhausted`。
- Pricing/checkout CTA handlers emit `pricing_click` and `checkout_start`.
- Add `data-editor-state={state.type}` and `data-editor-code={state.code}`.
- Add explicit `quota` state or helper `isQuotaBlocked` for layout class.

### 9.3 State class contract for QA

Required stable selectors:

```html
<header data-account-state="anonymous_ok|anonymous_exhausted|free_ok|free_exhausted|pro_paid|loading|error">
<section id="editor" data-editor-state="idle|uploaded|loading|result|quota_exhausted|error">
<a data-cta="sign_in|upgrade|buy_credits|pricing|checkout|logout|download|copy_prompt|try_another">
<button data-action="replace_image|remove_image|generate|retry|logout">
```

## 10. Acceptance checklist for 墨界

### Header / account

- [ ] anonymous_ok: Sign in + free edits + Pricing/Upgrade affordance visible。
- [ ] anonymous_exhausted: `Free edits used` + yellow `Upgrade` button。
- [ ] free_ok: avatar/email + Free + remaining + Upgrade + dropdown。
- [ ] free_exhausted: 0 edits + Upgrade + Buy credits/Pricing/Logout in menu。
- [ ] pro_paid: Pro/Paid + credits + Manage + Logout, no same-plan hard sell。
- [ ] loading: skeleton, no fake Sign in。
- [ ] error: Account unavailable + Retry, no fake Sign in。
- [ ] mobile drawer mirrors all account states。
- [ ] logout discoverable and emits `logout_click`.

### Editor

- [ ] empty: upload + prompt + Generate visible above fold。
- [ ] uploaded: source thumbnail + filename + size/type + Replace/Remove。
- [ ] loading: Generate disabled + progress step + duplicate submit protection。
- [ ] result: inline preview + Download/Open/Copy/Try another/Edit prompt。
- [ ] quota_exhausted: strong action card; not plain text。
- [ ] error: code + human explanation + next action。
- [ ] event names match contract。

### First screen

- [ ] 1366x768: editor card target <=560px initial state; Generate and account visible。
- [ ] 1440 desktop: hero copy and editor balanced; editor does not dominate.
- [ ] 1280 desktop: no horizontal overflow, no over-tall console.
- [ ] 390 mobile: Generate visible without console swallowing whole first screen.
- [ ] 430 mobile: same as 390; no horizontal overflow.

## 11. Handoff to downstream agents

### To 墨枢

Need API support or confirmed fallback:

- `/api/credits.state` enum preferred。
- `checkout.monthly/yearly/credit_pack` links or recommended action targets。
- logout GET/POST stable。
- Anonymous checkout should login then return to pricing/checkout; signed-in can create session。
- Do not require real live card payment for this task.

### To 墨界

Implement as a targeted refactor:

1. Replace `AuthStatusLink` with `AccountStateControl`.
2. Add account menu/logout.
3. Update editor event names and selectors.
4. Apply first-screen CSS compression.
5. Smoke desktop/mobile.

### To 墨测

QA should force/mock states where real accounts are unavailable:

- `/api/credits` mocked/intercepted for anonymous_ok, anonymous_exhausted, free_ok, free_exhausted, pro_paid, loading, error。
- Editor should be tested with a local JPG/PNG/WebP source。
- Quota error can be simulated via API returning `FREE_QUOTA_USED` / `LOGIN_REQUIRED`.

## 12. Design verdict

`DESIGN_CONDITIONAL_GO`

Reason:

- GO for implementation direction: visual language is stable; no need for new Stitch pass.
- CONDITIONAL because production currently fails Header account 6-state, logout visibility, loading/error distinction, and mobile first-screen compression.
- After above patch, this can move to `DESIGN_GO` if browser evidence passes desktop 1366/1440/1280 and mobile 390/430.
