diff --git a/docs/testing/consult-gender-optional-20260927/people-detail-other-1280.png b/docs/testing/consult-gender-optional-20260927/people-detail-other-1280.png
new file mode 100644
index 00000000..11bbb513
Binary files /dev/null and b/docs/testing/consult-gender-optional-20260927/people-detail-other-1280.png differ
diff --git a/docs/testing/consult-gender-optional-20260927/people-detail-other-390.png b/docs/testing/consult-gender-optional-20260927/people-detail-other-390.png
new file mode 100644
index 00000000..df5cc28c
Binary files /dev/null and b/docs/testing/consult-gender-optional-20260927/people-detail-other-390.png differ
diff --git a/docs/testing/consult-gender-optional-20260927/people-detail-self-1280.png b/docs/testing/consult-gender-optional-20260927/people-detail-self-1280.png
new file mode 100644
index 00000000..8ea5280b
Binary files /dev/null and b/docs/testing/consult-gender-optional-20260927/people-detail-self-1280.png differ
diff --git a/docs/testing/consult-gender-optional-20260927/people-detail-self-390.png b/docs/testing/consult-gender-optional-20260927/people-detail-self-390.png
new file mode 100644
index 00000000..e4017ef7
Binary files /dev/null and b/docs/testing/consult-gender-optional-20260927/people-detail-self-390.png differ
diff --git a/docs/testing/consult-gender-optional-20260927/people-edit-1280.png b/docs/testing/consult-gender-optional-20260927/people-edit-1280.png
new file mode 100644
index 00000000..80f1e9cd
Binary files /dev/null and b/docs/testing/consult-gender-optional-20260927/people-edit-1280.png differ
diff --git a/docs/testing/consult-gender-optional-20260927/people-edit-390.png b/docs/testing/consult-gender-optional-20260927/people-edit-390.png
new file mode 100644
index 00000000..ebabb3de
Binary files /dev/null and b/docs/testing/consult-gender-optional-20260927/people-edit-390.png differ
diff --git a/docs/testing/consult-gender-optional-20260927/profile-1280.png b/docs/testing/consult-gender-optional-20260927/profile-1280.png
new file mode 100644
index 00000000..03df287d
Binary files /dev/null and b/docs/testing/consult-gender-optional-20260927/profile-1280.png differ
diff --git a/docs/testing/consult-gender-optional-20260927/profile-390.png b/docs/testing/consult-gender-optional-20260927/profile-390.png
new file mode 100644
index 00000000..8643493b
Binary files /dev/null and b/docs/testing/consult-gender-optional-20260927/profile-390.png differ
diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md
index dbfd8335..64ace23f 100644
--- a/frontend/DESIGN.md
+++ b/frontend/DESIGN.md
@@ -655,7 +655,7 @@ page. Three parts now, in reading order:
- **Width / height:** desktop `width: min(100vw - 32px, 880px)`, with `height: min(84vh, 640px)` as the base and `min(84dvh, 640px)` applied inside `@supports (height: 1dvh)`. The right content pane uses 32px horizontal and bottom inset on desktop. All four panes share one class (`.settings-modal`), so switching panes cannot change the frame. At ≤767px the dialog is full-screen with four equal tabs along the top and 16px content inset.
- **Pane menu states:** default is transparent with secondary ink; hover is a 55% wash of `--color-canvas-muted` keeping secondary ink; current is the solid muted surface with primary ink. No accent bar, no trailing navigation chevron, and no weight change — hierarchy here comes from ink rank and surface, matching “Hierarchy inside the nav comes from ink rank, not hue”. The sidebar's 2px `--sidebar-ring` on the active session is deliberately **not** changed to match; the two surfaces read differently on purpose until that is revisited.
- **Content width:** the 880px frame now gives the content pane a deliberate inset instead of leaving controls against the divider. Form panes (个人资料, 通用设置) cap their children at 560px and stay left aligned (`.settings-dialog-content--form > *`); list panes (星盘资料, 账户与点数) stay full-bleed within the inset so tables and card grids keep their columns. The cap sits on the children of the scroll container only — never on `.settings-modal` or `.account-settings-shell` — so it cannot make the frame resize between panes (BUG-554 / BUG-698).
-- **Personal profile:** one row of avatar editing (56px preview, eight palettes, 换一个形象) plus nickname and login email. The header uses the product-level “设置” eyebrow above the current pane title; there is no duplicate 管理星盘资料 button.
+- **Personal profile:** one row of avatar editing (56px preview, eight palettes, 换一个形象) plus nickname and login email. Below them sits **性别(选填)** (2026-09-27, TASK-consult-gender-optional): three 44px choices 女 / 男 / 不填 in one row (the birth-time range chip look: `--color-border`, selected `--color-action` border + `--color-action-soft`), then one caption line 「用于婚恋解读里判断夫星 / 妻星,不填也能用。」. A choice saves at once (like the avatar palette) and shows 「已保存。」; the choices are disabled while saving, no spinner. This is the only place the owner sets it; onboarding never asks. The header uses the product-level “设置” eyebrow above the current pane title; there is no duplicate 管理星盘资料 button.
- **Chart archive (`/people`):** a static secondary page beside 星盘 / 星历 / 我的报告. Self is the first row and cannot be deleted. Each other row is a name plus birth facts, with no relationship field. Detail shows the reported time and the chart time separately, then place and ayanamsa. Shortcuts are 和 TA 对话, 看星盘, 生成报告, and 和我合盘. Adding a sixth person is disabled with 「最多保存 5 个人的星盘」. Delete confirms with the session and report counts before the server removes them together. There is no 「设为默认」 and no 「当前默认」 badge.
- **Subject switcher:** the same control sits beside the title on chat, chart, ephemeris, reports, and the archive. It is not in the sidebar header. Choosing someone in a chat that already has messages switches the current person and opens an empty chat. An empty chat rebinds in place. The list shows at most five people.
- **States:** open, pane switch, list / self / other / add, saving, success, and error.
@@ -972,7 +972,8 @@ Three product rules for assistant markdown in `.message-markdown` (BUG-962 / 963
- **1024px 及以上:两栏。** 左列表(18–22rem),右详情;「‹ 返回」隐藏。右栏正在看的那一行用中性浅底(`is-open`),强调色只给当前人物。
- **列表行**(`.people-archive-row`,最小 64px):40px 圆形首字头像(本人写「我」)+ 名字(本人后跟灰底「本人」标签)+ 一行等宽 `tabular-nums` 的「出生日期 · 排盘时间 · 城市」+ 行尾「›」。资料不完整时这一行写「这份资料还不完整」。**当前人物**(`is-current`,与顶栏「当前星盘」同一个人)是 `--color-action-strong` 描边 + `--color-action-soft` 底。
- **添加**:虚线卡片「+ 添加一个人」(48px,`--color-border-strong` 虚线,文字 `--color-action`),下面小字「还可以保存 N 个人」,N 仍是 `MAX_SAVED_SUBJECTS - 人数`;到顶时换成原来的限额句并禁用。
-- **详情卡**:头部 48px 头像 + 名字 +「本人 · 出生地」(他人只写出生地);两格时间卡「填报时间」「排盘时间」,等宽大号数字。只有采用或确认了**校正结果**(状态 accepted / confirmed **且**有校正案例)时,排盘卡换成 `--color-success` 描边 + `--color-success-muted` 底,标签写「排盘时间 · 已校正」——旧资料读回时的 `confirmed` 兜底不算。下面是出生日期与岁差(显示「Raman(默认)」这类设置里的名字,不露小写键),再下面三个等宽快捷入口「和 TA 对话 / 看星盘 / 生成报告」(图标用侧栏同款 lucide)。他人另有「和我合盘」与「编辑」;本人底部灰字「本人不能删除」。
+- **详情卡**:头部 48px 头像 + 名字 +「本人 · 出生地」(他人只写出生地);两格时间卡「填报时间」「排盘时间」,等宽大号数字。只有采用或确认了**校正结果**(状态 accepted / confirmed **且**有校正案例)时,排盘卡换成 `--color-success` 描边 + `--color-success-muted` 底,标签写「排盘时间 · 已校正」——旧资料读回时的 `confirmed` 兜底不算。下面是出生日期、岁差(显示「Raman(默认)」这类设置里的名字,不露小写键)与性别(「女」「男」,没填写「未填」;本人在值下面加一行灰色小字「在「设置 → 个人资料」里改」,这一页不给本人第二个性别入口),再下面三个等宽快捷入口「和 TA 对话 / 看星盘 / 生成报告」(图标用侧栏同款 lucide)。他人另有「和我合盘」与「编辑」;本人底部灰字「本人不能删除」。
+- **编辑表单里的性别(选填)**(2026-09-27):放在出生地点之后、岁差之前,与个人资料同一个控件(女 / 男 / 不填,44px,一行说明),随「保存」一起提交;选「不填」存为空。
- **删除只在编辑视图底部**,单独一张卡,删除确认、用量计数、查询失败句全部沿用。
- **头像颜色**:本人用 `--color-action-strong` + `--color-on-dark`(同账户首字圆);其他人按 id 稳定地取四个元素色 `--color-element-fire / earth / air / water` 之一,字用 `--color-ink`。不新造色值。
diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md
index ced5dc4f..af11851d 100644
--- a/frontend/docs/VOICE.md
+++ b/frontend/docs/VOICE.md
@@ -20,6 +20,13 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
首页开场语下面可以有一行今日趋势(今日星语卡片的 trend,每日生成);还没生成时写「今天的星语还没写出来。」,没有出生分钟时不写。入口按钮下面平时不写字,只在用户需要动手时写一句:有没做完的校正写「上次那次校正还没完成,可以在历史对话里接着做。」;当前人物不是本人写「生时校正暂时只支持本人。」。不再写「不确定出生时间时,用记得住的经历一步步缩小范围」「上次已经校正完,可以拿最新资料再来一次」,也不要用「·」把两句不相干的提示拼成一行。
+## 性别(选填)(2026-09-27,TASK-consult-gender-optional)
+
+- 标题「性别(选填)」,三个选项「女」「男」「不填」;下面只有一行说明:「用于婚恋解读里判断夫星 / 妻星,不填也能用。」不解释为什么只有两项,不劝用户填。
+- 详情页没填写「未填」,不写「未知」「保密」。本人详情在值下面写「在「设置 → 个人资料」里改」。
+- 保存成功写「已保存。」;失败写「性别没保存上,再试一次。」(服务端有原因时用服务端的句子)。
+- 注册 / 开场流程不问性别。回答里不说「你没填性别」来催填;数据卡写「性别未知」时,按精简清单两边都看。
+
## 生时校正:范围、时刻、吻合率由服务端说(2026-09-27,BUG-1055~1058)
证据轮与交付轮里,范围、代表分钟和吻合率只由服务端说。服务端事实句照原样接在模型正文后面,不参与裁句:
diff --git a/frontend/src/app/(app)/page.tsx b/frontend/src/app/(app)/page.tsx
index 150cfead..6fd16c83 100644
--- a/frontend/src/app/(app)/page.tsx
+++ b/frontend/src/app/(app)/page.tsx
@@ -313,6 +313,7 @@ export default function Home() {
openAccountDialog,
closeAccountDialog,
persistAvatar,
+ persistGender,
saveProfile,
saveOnboardingName,
saveOnboardingBirth,
@@ -1134,7 +1135,7 @@ export default function Home() {
overlayRef: accountDialog,
closeButtonRef: closeButton,
renderProfile: () => (
-
+
),
renderChartLibrary() {
return null;
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index 2dbcd407..da18261d 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -1882,6 +1882,17 @@ button:disabled { cursor: default; opacity: .45; }
.ayanamsa-preference-option[aria-checked="true"] { border-color: var(--color-action); background: var(--color-action-soft); }
.ayanamsa-preference-option small { display: block; margin-top: var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-caption); font-weight: 400; }
.ayanamsa-preference-hint { margin: var(--space-3) 0 0; color: var(--color-ink-tertiary); font-size: var(--type-caption); line-height: 1.5; }
+/* Optional gender (TASK-consult-gender-optional-20260927): three 44px choices
+ in one row, same look as the birth-time range choices; the title matches the
+ profile form's field labels, the hint matches the ayanamsa hint. */
+.profile-gender-field { display: grid; gap: var(--space-2); }
+.profile-gender-title { color: var(--color-ink-secondary); font-size: var(--type-overline); font-weight: 600; }
+.profile-gender-options { display: flex; flex-wrap: wrap; gap: var(--space-2); }
+.profile-gender-option { min-width: 64px; min-height: 44px; padding: 0 var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink); font-size: var(--type-body-sm); cursor: pointer; }
+.profile-gender-option.is-selected { border-color: var(--color-action); background: var(--color-action-soft); }
+.profile-gender-option:disabled { cursor: default; opacity: 0.6; }
+.profile-gender-hint { margin: 0; color: var(--color-ink-tertiary); font-size: var(--type-caption); line-height: 1.5; }
+.people-archive-fact-note { display: block; margin-top: var(--space-1); color: var(--color-ink-tertiary); font-size: var(--type-caption); }
.account-menu-danger, .account-menu-danger > svg { color: var(--color-danger); }
.account-menu-danger[data-highlighted] { background: var(--color-danger-muted); }
diff --git a/frontend/src/components/chart-profile-form.tsx b/frontend/src/components/chart-profile-form.tsx
index 42826b9a..923b37fe 100644
--- a/frontend/src/components/chart-profile-form.tsx
+++ b/frontend/src/components/chart-profile-form.tsx
@@ -3,6 +3,7 @@
import type { Dispatch, FormEvent, SetStateAction } from "react";
import { AyanamsaPreferenceField } from "@/components/ayanamsa-preference-field";
import { ProfileFields } from "@/components/profile-fields";
+import { ProfileGenderField } from "@/components/profile-gender-field";
import { resolveAyanamsa } from "@/lib/ayanamsa";
import type { Profile } from "@/lib/home-types";
@@ -13,6 +14,8 @@ export type ChartProfileFormProps = {
readonly onChange: Dispatch>;
readonly nameInputId: string;
readonly showAyanamsa?: boolean;
+ /** Optional gender (女 / 男 / 不填); saved with the form. */
+ readonly showGender?: boolean;
readonly onSubmit: (event: FormEvent) => void;
readonly onCancel?: () => void;
readonly cancelLabel?: string;
@@ -28,6 +31,7 @@ export function ChartProfileForm({
onChange,
nameInputId,
showAyanamsa = false,
+ showGender = false,
onSubmit,
onCancel,
cancelLabel = "取消",
@@ -39,6 +43,13 @@ export function ChartProfileForm({