/* =========================================================
   figures4.jsx — one architecture diagram per module
   (m1-arch … m9-arch), shown on each module overview page.
   Appends to the same FIGN object defined in figures.jsx and
   reuses FigFrame / FBox / FArrow / FT via the global scope.
   ========================================================= */

/* ---------------- m1 · the shop's service landscape ---------------- */
FIGN["m1-arch"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={216} cap={L("门店客服的全景:三个入口(电话、微信私域、平台 IM)汇进同一批工作,而这批工作最后只落到四件事上——接住预约、答清问题、改约提醒、事后召回。只有第一件有硬时间窗口,也只有它直接对应当天的营业额,所以整本书的第一件事是把漏接的电话换算成钱。", "The landscape: three doorways — the phone, the WeChat private domain, and platform IM — feed one body of work that reduces to four jobs: catch bookings, answer questions, reschedule and remind, win people back. Only the first has a hard time window and only it maps directly onto today's revenue, which is why the book starts by converting missed calls into money.")}>
      <FBox x={20} y={30} w={116} h={34} label={L("电话来电", "phone calls")} tone="p" />
      <FBox x={20} y={76} w={116} h={34} label={L("微信私域", "WeChat")} tone="p" />
      <FBox x={20} y={122} w={116} h={34} label={L("美团 / 点评 / 抖音", "platform IM")} tone="p" />
      {[47, 93, 139].map((y, i) => <FArrow key={i} x1={136} y1={y} x2={176} y2={93} />)}
      <FBox x={178} y={68} w={104} h={50} label={L("前台 + AI", "front desk + AI")} tone="a" />
      {[
        L("接住预约(有时间窗口)", "catch bookings (time-boxed)"),
        L("答清问题", "answer questions"),
        L("改约与提醒", "reschedule and remind"),
        L("事后召回", "win them back"),
      ].map((t, i) => (
        <g key={i}>
          <FArrow x1={282} y1={93} x2={318} y2={40 + i * 38} />
          <rect x={320} y={26 + i * 38} width={230} height={28} rx="5" fill={i === 0 ? "color-mix(in srgb, var(--primary) 16%, transparent)" : "var(--surface-2)"} stroke={i === 0 ? "var(--primary)" : "var(--hairline-strong)"} />
          <text x={435} y={44 + i * 38} textAnchor="middle" style={{ font: "500 10px var(--f-body)", fill: "var(--ink)" }}>{t}</text>
          {i === 0 ? <FArrow x1={550} y1={40} x2={584} y2={40} c="#2e9e6b" /> : null}
        </g>
      ))}
      <FBox x={586} y={24} w={76} h={32} label={L("今日营业额", "today's revenue")} tone="ok" />
      <FT x={340} y={196} cls="tm">{L("漏接一通预约电话 = 客单价 × 转化 × 复购 —— 这就是技术预算的上限", "one missed booking call = ticket × conversion × repeat — the ceiling on the technology budget")}</FT>
      <FT x={340} y={212} cls="tn">{L("Erlang-B 呼损模型:电话不排队,漏掉的不进任何报表", "Erlang-B blocking: the phone does not queue, and what is lost enters no report")}</FT>
    </FigFrame>
  );
};

/* ---------------- m2 · the ASR path ---------------- */
FIGN["m2-arch"] = function ({ idx }) {
  const L = useL();
  const chain = [
    { l: L("麦克风 / 电话", "mic / phone line"), sub: "8k · 16k", tone: "n" },
    { l: L("采样与编码", "sample and encode"), sub: "G.711 / Opus", tone: "n" },
    { l: "VAD", sub: L("说话了吗", "is this speech"), tone: "warn" },
    { l: L("特征提取", "features"), sub: "fbank / mel", tone: "n" },
    { l: L("声学模型", "acoustic model"), sub: "RNN-T / Paraformer", tone: "p" },
    { l: L("解码", "decoding"), sub: L("热词 + 语言模型", "hotwords + LM"), tone: "a" },
    { l: L("文本 + 置信度", "text + conf"), sub: "", tone: "ok" },
  ];
  return (
    <FigFrame idx={idx} h={214} cap={L("识别侧的完整路径。两个地方决定了门店场景的成败,而它们都不在模型里:一是最左边的信道(电话 8 kHz 砍掉 4 kHz 以上的擦音能量),二是 VAD 与端点检测(机器凭什么认为你说完了)。解码阶段的热词与语言模型偏置,是把技师名、项目名这些专有名词救回来的地方;最右边的置信度则是后面转人工判断的输入。", "The full recognition path. Two things decide the outcome in a shop, and neither lives in the model: the channel on the far left (8 kHz telephony removing the fricative energy above 4 kHz), and VAD plus endpointing (how the machine decides you finished). Hotword and language-model biasing at the decoding stage is where therapist and service names are rescued, and the confidence score on the right is the input to the handoff decision later.")}>
      {chain.map((c, i) => {
        const x = 14 + i * 96;
        return (
          <g key={i}>
            <FBox x={x} y={56} w={84} h={46} label={c.l} sub={c.sub} tone={c.tone} />
            {i < chain.length - 1 ? <FArrow x1={x + 84} y1={79} x2={x + 96} y2={79} /> : null}
          </g>
        );
      })}
      <FT x={70} y={34} cls="ta">{L("这里就决定了上限", "ceiling set here")}</FT>
      <FArrow x1={56} y1={40} x2={56} y2={54} c="var(--accent)" />
      <FT x={302} y={34} cls="ta">{L("体验的主旋钮", "the main experience dial")}</FT>
      <FArrow x1={302} y1={40} x2={302} y2={54} c="var(--accent)" />
      <FT x={578} y={34} cls="tp">{L("专有名词在此救回", "names rescued here")}</FT>
      <FArrow x1={590} y1={40} x2={590} y2={54} c="var(--primary)" />
      <FBox x={206} y={130} w={188} h={34} label={L("端点判定:说完了吗", "endpointing: are they done")} tone="warn" />
      <FBox x={410} y={130} w={252} h={34} label={L("CER / 槽位准确率 / 置信度", "CER / slot accuracy / confidence")} tone="ok" />
      <FT x={340} y={188} cls="tm">{L("厂商的准确率不能直接用:测试集不是你的场景", "vendor accuracy does not transfer: their test set is not yours")}</FT>
      <FT x={340} y={206} cls="tn">{L("拿自己门店的十通真实录音跑一次,才是有效的评测", "ten of your own recordings is the only evaluation that means anything")}</FT>
    </FigFrame>
  );
};

/* ---------------- m3 · the TTS path ---------------- */
FIGN["m3-arch"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={216} cap={L("合成侧的路径:业务系统给出文本,先做文本规范化(金额、时间、电话、楼层)和多音字处理,再用 SSML 标注停顿与朗读方式,然后进声学模型与声码器,最后按标点切块流式播放。注意上半条链路(规范化与 SSML)完全没有推理成本,却决定了顾客觉得它像不像人;下半条链路(模型与声码器)决定音质与并发成本。音色如果用了真人的,那条虚线上的授权与标识是硬约束。", "The synthesis path: the business system emits text, which is normalised (amounts, times, phone numbers, floors) and checked for heteronyms, marked up with SSML for pauses and reading modes, then passed to the acoustic model and vocoder, and finally chunked on punctuation for streaming playback. The upper half — normalisation and SSML — costs no inference at all and decides whether it sounds human; the lower half decides audio quality and concurrency cost. If the voice belongs to a real person, the authorisation and labelling on the dashed line are hard constraints.")}>
      <FBox x={16} y={30} w={110} h={34} label={L("业务文本", "business text")} tone="n" />
      <FArrow x1={126} y1={47} x2={148} y2={47} />
      <FBox x={150} y={30} w={120} h={34} label={L("文本规范化 TN", "normalisation")} tone="p" />
      <FArrow x1={270} y1={47} x2={292} y2={47} />
      <FBox x={294} y={30} w={120} h={34} label={L("多音字与词典", "heteronym lexicon")} tone="p" />
      <FArrow x1={414} y1={47} x2={436} y2={47} />
      <FBox x={438} y={30} w={120} h={34} label="SSML" tone="p" />
      <FT x={594} y={50} cls="tp">{L("零推理成本", "no inference cost")}</FT>

      <FArrow x1={498} y1={64} x2={498} y2={88} />
      <FBox x={438} y={90} w={120} h={38} label={L("声学模型", "acoustic model")} tone="a" />
      <FArrow x1={438} y1={109} x2={416} y2={109} />
      <FBox x={296} y={90} w={118} h={38} label={L("声码器", "vocoder")} sub="HiFi-GAN" tone="a" />
      <FArrow x1={296} y1={109} x2={274} y2={109} />
      <FBox x={152} y={90} w={120} h={38} label={L("分块流式播放", "chunked streaming")} tone="ok" />
      <FArrow x1={152} y1={109} x2={130} y2={109} />
      <FBox x={16} y={90} w={112} h={38} label={L("顾客听到", "the customer hears")} tone="ok" />
      <FT x={600} y={112} cls="ta">{L("算力与并发成本", "compute cost")}</FT>

      <rect x={152} y={146} width={406} height={34} rx="6" fill="none" stroke="#c0453f" strokeDasharray="5 3" />
      <FT x={355} y={168} cls="ta">{L("若用真人音色:授权 · 限定 · 可撤回 · 标识 · 离职下线", "a real voice: authorisation · scope · revocation · labelling")}</FT>
      <FT x={340} y={204} cls="tn">{L("首包延迟决定「反应快不快」,缓冲欠载决定「会不会卡」", "first-packet latency decides how responsive it feels; underrun decides whether it stutters")}</FT>
    </FigFrame>
  );
};

/* ---------------- m4 · the dialogue brain ---------------- */
FIGN["m4-arch"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={222} cap={L("对话大脑的内部结构:识别出来的文本先过意图与槽位(头部走规则秒回,长尾交给模型),需要门店知识的走检索,需要改变世界的走工具调用。三条线最后汇进大模型生成回复,而回复在送去合成之前必须过一遍护栏——禁语、价格校验、越界拒绝。两个硬规则贯穿始终:价格与空档只能来自工具调用,绝不能来自模型记忆;检索不到就明确说不知道并转人工。", "Inside the dialogue brain: recognised text first passes intent and slot extraction (rules answer the head instantly, the model catches the tail), anything needing shop knowledge goes to retrieval, and anything that changes the world goes to a tool call. All three feed generation, and the reply must pass the guardrails — banned phrasing, price validation, out-of-bounds refusal — before it is synthesised. Two hard rules run through everything: prices and slots come only from tool calls, never from the model's memory, and when retrieval finds nothing, say so and hand off.")}>
      <FBox x={16} y={82} w={94} h={40} label={L("识别文本", "recognised text")} tone="n" />
      <FArrow x1={110} y1={102} x2={134} y2={102} />
      <FBox x={136} y={82} w={110} h={40} label={L("意图 + 槽位", "intent + slots")} sub={L("规则 / 模型", "rules / model")} tone="p" />
      <FArrow x1={246} y1={94} x2={276} y2={44} />
      <FArrow x1={246} y1={102} x2={276} y2={102} />
      <FArrow x1={246} y1={112} x2={276} y2={162} />
      <FBox x={278} y={26} w={128} h={38} label={L("知识检索 RAG", "retrieval")} tone="a" />
      <FBox x={278} y={84} w={128} h={38} label={L("对话状态", "dialogue state")} tone="n" />
      <FBox x={278} y={144} w={128} h={38} label={L("工具调用", "tool calls")} sub={L("查 · 占 · 落单", "check · hold · commit")} tone="a" />
      <FBox x={418} y={26} w={98} h={38} label={L("门店知识库", "knowledge base")} tone="n" />
      <FBox x={418} y={144} w={98} h={38} label={L("预约系统", "booking system")} tone="n" />
      <FArrow x1={406} y1={45} x2={416} y2={45} />
      <FArrow x1={406} y1={163} x2={416} y2={163} />
      <FArrow x1={406} y1={103} x2={438} y2={103} />
      <FBox x={440} y={84} w={98} h={38} label={L("大模型生成", "generation")} tone="p" />
      <FArrow x1={538} y1={103} x2={562} y2={103} />
      <FBox x={564} y={78} w={98} h={50} label={L("护栏", "guardrails")} sub={L("禁语 · 校验 · 拒绝", "banned · validate · refuse")} tone="warn" />
      <FT x={613} y={144} cls="tn">{L("→ 送去合成", "→ to synthesis")}</FT>
      <FT x={340} y={200} cls="tm">{L("价格与空档只能来自工具调用,绝不能来自模型记忆", "prices and slots come only from tool calls, never from the model's memory")}</FT>
      <FT x={340} y={216} cls="tn">{L("检索不到就明确说不知道并转人工 —— 这一行代码的性价比最高", "when retrieval finds nothing, say so and hand off — the highest-return line of code in the module")}</FT>
    </FigFrame>
  );
};

/* ---------------- m5 · the realtime orchestration ---------------- */
FIGN["m5-arch"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={218} cap={L("实时链路的两层:下面是媒体层(SIP/RTP 或 WebRTC、回声消除、VAD),上面是编排层(LiveKit Agents、Pipecat 或自研),负责把 ASR、大模型和 TTS 的流拼接起来,并处理打断、超时与降级。真正把延迟压下来的三个动作都发生在编排层,而且都是「提前开始」:ASR 的中间结果就送去预热大模型,大模型的第一句话就送去合成,合成出的第一块就开始播。", "Two layers make up the realtime path. Below sits the media layer — SIP/RTP or WebRTC, echo cancellation, VAD. Above it sits orchestration — LiveKit Agents, Pipecat or something in-house — splicing the ASR, LLM and TTS streams together and handling interrupts, timeouts and degradation. The three moves that actually cut latency all live in orchestration and are all forms of starting earlier: partial ASR warms the LLM, the model's first sentence goes straight to synthesis, and the first synthesised chunk starts playing.")}>
      <rect x={16} y={126} width={646} height={60} rx="8" fill="var(--surface-2)" stroke="var(--hairline-strong)" />
      <FT x={80} y={144} cls="tk">{L("媒体层", "media layer")}</FT>
      <FBox x={150} y={138} w={110} h={38} label="SIP / RTP" tone="n" />
      <FBox x={268} y={138} w={110} h={38} label="WebRTC" tone="n" />
      <FBox x={386} y={138} w={110} h={38} label={L("回声消除 AEC", "echo cancellation")} tone="warn" />
      <FBox x={504} y={138} w={144} h={38} label={L("VAD / 端点检测", "VAD / endpointing")} tone="warn" />

      <rect x={16} y={22} width={646} height={92} rx="8" fill="none" stroke="var(--primary)" strokeDasharray="5 3" />
      <FT x={92} y={40} cls="tp">{L("编排层", "orchestration")}</FT>
      <FT x={92} y={56} cls="tn">LiveKit / Pipecat</FT>
      <FBox x={166} y={34} w={100} h={38} label="ASR" tone="p" />
      <FArrow x1={266} y1={53} x2={296} y2={53} c="var(--primary)" />
      <FBox x={298} y={34} w={100} h={38} label="LLM" tone="a" />
      <FArrow x1={398} y1={53} x2={428} y2={53} c="var(--primary)" />
      <FBox x={430} y={34} w={100} h={38} label="TTS" tone="p" />
      <FArrow x1={530} y1={53} x2={560} y2={53} c="var(--primary)" />
      <FBox x={562} y={34} w={86} h={38} label={L("播放", "playout")} tone="ok" />
      <path d="M216,72 C240,102 300,102 330,74" fill="none" stroke="#2e9e6b" strokeWidth="1.3" strokeDasharray="4 3" />
      <path d="M348,72 C372,102 432,102 462,74" fill="none" stroke="#2e9e6b" strokeWidth="1.3" strokeDasharray="4 3" />
      <path d="M480,72 C504,102 560,102 590,74" fill="none" stroke="#2e9e6b" strokeWidth="1.3" strokeDasharray="4 3" />
      <FT x={272} y={108} cls="tn">{L("中间结果预热", "warm on partials")}</FT>
      <FT x={404} y={108} cls="tn">{L("首句即合成", "first sentence early")}</FT>
      <FT x={536} y={108} cls="tn">{L("边合成边播", "play while generating")}</FT>
      <FT x={340} y={204} cls="tm">{L("目标 P95 < 1.5 秒;顾客记住的是最慢的那几次,不是平均值", "target P95 under 1.5 s — customers remember the slowest turns, not the mean")}</FT>
    </FigFrame>
  );
};

/* ---------------- m6 · the supply side ---------------- */
FIGN["m6-arch"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={218} cap={L("供给侧有三种形态,计费维度完全不同:云 API 按分钟、按 token、按字符计费,没有闲置成本但用量一大乘法就很吓人;自建按显卡、电费和运维人天计费,是一条水平线;混合部署让日常量走自建、高峰溢出到云。选型要把自己的硬约束先摆出来——数据能不能出门、有没有会运维的人、高峰并发多少——再谈单价。", "The supply side comes in three shapes with entirely different billing dimensions. Cloud APIs bill per minute, per token and per character: no idle cost, but the multiplication gets frightening at volume. Self-hosting bills in GPUs, power and engineer days, which is a horizontal line. Hybrid serves the baseline yourself and overflows the peak to the cloud. Selection starts by stating the hard constraints — whether data may leave the building, whether someone can operate it, what the peak concurrency is — and only then discusses unit price.")}>
      <FBox x={18} y={26} w={190} h={32} label={L("云 API", "cloud API")} tone="p" />
      <FBox x={244} y={26} w={190} h={32} label={L("自建 GPU", "self-hosted GPU")} tone="a" />
      <FBox x={470} y={26} w={192} h={32} label={L("混合:自建 + 高峰溢出", "hybrid: base + burst")} tone="ok" />
      {[
        { x: 18, items: [L("按分钟(ASR)", "per minute (ASR)"), L("按 token(LLM)", "per token (LLM)"), L("按字符(TTS)", "per character (TTS)"), L("零闲置成本", "no idle cost")] },
        { x: 244, items: [L("显卡折旧 / 租用", "GPU depreciation or rent"), L("电费与机房", "power and hosting"), L("运维人天(最贵)", "engineer days (the big one)"), L("数据不出门", "data stays in")] },
        { x: 470, items: [L("基线自建", "baseline in-house"), L("高峰调云", "peak to the cloud"), L("两套接口要抽象", "abstract both APIs"), L("运维复杂度最高", "highest ops complexity")] },
      ].map((col, i) => col.items.map((t, j) => (
        <g key={`${i}-${j}`}>
          <rect x={col.x} y={68 + j * 27} width={i === 2 ? 192 : 190} height={22} rx="4" fill="var(--surface-2)" stroke="var(--hairline-strong)" />
          <text x={col.x + (i === 2 ? 96 : 95)} y={83 + j * 27} textAnchor="middle" style={{ font: "500 9.5px var(--f-body)", fill: "var(--ink)" }}>{t}</text>
        </g>
      )))}
      <FT x={340} y={196} cls="tm">{L("交叉点通常在每月十万分钟上下:单店买,大连锁可以建", "the crossing is usually around 100k minutes a month: a single shop buys, a large chain can build")}</FT>
      <FT x={340} y={212} cls="tn">{L("先问硬约束(数据出境、运维能力、高峰并发),再谈单价", "state the hard constraints — data residency, operating capacity, peak concurrency — before discussing price")}</FT>
    </FigFrame>
  );
};

/* ---------------- m7 · channels ---------------- */
FIGN["m7-arch"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={220} cap={L("三类入口接进同一个会话层,再由它决定交给 AI 还是交给坐席。电话最实时也最受并发线路约束;微信是复购的主场,有 48 小时窗口这类硬规则;平台 IM 的首响时间直接影响店铺权重。会话层统一做的三件事是:一份共享的对话上下文、一套统一的转人工触发规则、以及一份所有渠道通用的知识与话术——否则同一个问题在三个渠道会得到三个不同的答案。", "Three doorways feed one session layer, which decides whether the AI or an agent takes the conversation. The phone is the most realtime and the most constrained by channel count; WeChat is where repeat business lives and has hard rules such as the 48-hour window; platform IM ranks you by first-response time. The session layer unifies three things: one shared conversation context, one set of handoff triggers, and one body of knowledge and scripts across every channel — otherwise the same question gets three different answers in three places.")}>
      <FBox x={16} y={26} w={150} h={44} label={L("电话 SIP / 呼叫中心", "phone / SIP")} sub={L("实时 · 并发线路", "realtime · channel-limited")} tone="p" />
      <FBox x={16} y={84} w={150} h={44} label={L("微信生态", "WeChat")} sub={L("公众号 · 小程序 · 企微", "account · mini-program · WeCom")} tone="p" />
      <FBox x={16} y={142} w={150} h={44} label={L("平台 IM", "platform IM")} sub={L("美团 · 点评 · 抖音", "Meituan · Dianping · Douyin")} tone="p" />
      {[48, 106, 164].map((y, i) => <FArrow key={i} x1={166} y1={y} x2={202} y2={106} />)}
      <FBox x={204} y={72} w={128} h={68} label={L("统一会话层", "session layer")} sub={L("上下文 · 路由 · 留痕", "context · routing · logging")} tone="a" />
      <FArrow x1={332} y1={92} x2={368} y2={58} />
      <FArrow x1={332} y1={120} x2={368} y2={156} />
      <FBox x={370} y={38} w={136} h={42} label={L("AI 语音 / 文字客服", "AI agent")} tone="ok" />
      <FBox x={370} y={136} w={136} h={42} label={L("人工坐席", "human agent")} tone="warn" />
      <FArrow x1={438} y1={80} x2={438} y2={134} c="#d98a1f" dash />
      <FT x={512} y={110} anchor="start" cls="tn">{L("转人工 + 上下文交接", "handoff + context card")}</FT>
      <FBox x={534} y={38} w={128} h={42} label={L("预约 / 会员系统", "booking / membership")} tone="n" />
      <FArrow x1={506} y1={58} x2={532} y2={58} />
      <FBox x={534} y={136} w={128} h={42} label={L("工单与回访", "tickets and follow-up")} tone="n" />
      <FArrow x1={506} y1={156} x2={532} y2={156} />
      <FT x={340} y={206} cls="tn">{L("一份知识、一套触发规则、一个上下文 —— 否则三个渠道三个答案", "one body of knowledge, one set of triggers, one context — or three channels give three answers")}</FT>
    </FigFrame>
  );
};

/* ---------------- m8 · the operating loop ---------------- */
FIGN["m8-arch"] = function ({ idx }) {
  const L = useL();
  const ring = [
    L("通话记录", "call records"), L("全量转写", "transcribe all"), L("模型质检打分", "model scoring"),
    L("坏案例库", "bad-case library"), L("修复", "fix"), L("灰度 + 回归集", "canary + regression"),
  ];
  return (
    <FigFrame idx={idx} h={222} cap={L("运营模块把整套系统变成一个会自己变好的东西:通话记录进来,全量转写后交给模型逐条打分,坏案例分类归因进案例库,修完之后灰度验证并冻进回归集,然后下一轮。旁边那条竖线是合规审计,它不参与迭代,但拥有一票否决权——录音告知、同意留痕、合成标识、外呼规则、疗效红线、越界拒绝,任何一条不过,整个闭环转得再快也没用。", "The operations module turns the whole system into something that improves itself: call records arrive, everything is transcribed and scored by a model, bad cases are classified and attributed into a library, fixes are validated on a canary and frozen into a regression set, and round it goes. The vertical bar beside it is the compliance audit. It takes no part in the iteration but holds a veto — recording notice, logged consent, synthetic labelling, outbound rules, the efficacy line, out-of-bounds refusal. Fail any one and it does not matter how fast the loop spins.")}>
      {ring.map((t, i) => {
        const x = 16 + (i % 3) * 158;
        const y = i < 3 ? 34 : 106;
        const isBottom = i >= 3;
        return (
          <g key={i}>
            <FBox x={x} y={y} w={140} h={42} label={t} tone={i === 4 ? "a" : i === 2 ? "p" : "n"} />
            {i % 3 < 2 ? <FArrow x1={x + 140} y1={y + 21} x2={x + 156} y2={y + 21} c={isBottom ? "var(--accent)" : "var(--primary)"} /> : null}
          </g>
        );
      })}
      <FArrow x1={402} y1={76} x2={402} y2={104} c="var(--primary)" />
      <path d="M16,127 C-4,100 -4,60 14,58" fill="none" stroke="var(--accent)" strokeWidth="1.3" strokeDasharray="4 3" />
      <FT x={210} y={170} cls="tm">{L("修完必须进回归集,否则下次改提示词会顶掉", "every fix enters the regression set")}</FT>

      <line x1={490} y1={20} x2={490} y2={184} stroke="var(--hairline-strong)" strokeDasharray="4 3" />
      <FBox x={510} y={30} w={152} h={34} label={L("合规审计", "compliance audit")} tone="bad" />
      {[L("录音告知与同意", "notice and consent"), L("合成内容标识", "synthetic labelling"), L("外呼同意与退订", "outbound consent and opt-out"), L("疗效红线与越界拒绝", "efficacy line and refusals")].map((t, i) => (
        <g key={i}>
          <rect x={510} y={72 + i * 28} width={152} height={23} rx="4" fill="var(--surface-2)" stroke="var(--hairline-strong)" />
          <text x={586} y={88 + i * 28} textAnchor="middle" style={{ font: "500 9px var(--f-body)", fill: "var(--ink)" }}>{t}</text>
        </g>
      ))}
      <FT x={586} y={198} cls="ta">{L("一票否决", "holds a veto")}</FT>
      <FT x={210} y={196} cls="tn">{L("上线第一周难看是正常的,原地打转才是失败", "a bad first week is normal; going in circles is the failure")}</FT>
    </FigFrame>
  );
};

/* ---------------- m9 · the case studies ---------------- */
FIGN["m9-arch"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={216} cap={L("最后一个模块把整本书用两个完整案例走一遍。左边是一本账:一家三店连锁的投入产出、月净收益与回收期,每个参数都可以换成你自己门店的数字。右边是一通电话:2 分 17 秒、11 个轮次,逐跳展开耗时、文本、置信度和发生的事情,并且可以往里注入三类典型故障——热词缺失、尾点过短、知识缺条目——看这通电话怎么从「预约成功」滑向「转人工」再滑向「顾客挂断」。", "The last module walks the whole book through two complete cases. On the left, a ledger: one three-shop chain's costs and benefits, monthly net and payback, every parameter replaceable with your own. On the right, one call: 2 minutes 17 seconds across eleven turns, unfolded hop by hop with timings, text, confidence and events — and three classic faults you can inject (a missing hotword, an endpoint threshold that is too short, a missing knowledge entry) to watch it slide from confirmed booking to handoff to a hang-up.")}>
      <FBox x={18} y={24} w={300} h={32} label={L("案例一:三店连锁的账本", "Case one: a three-shop chain's ledger")} tone="p" />
      {[
        L("210 通/天 · 前台 4 人 · 客单 268 元", "210 calls/day · 4 staff · ¥268 ticket"),
        L("收益:挽回漏接 + 省工时 + 唤醒会员", "benefit: recovery + hours + reactivation"),
        L("负项:被 AI 劝退的顾客", "negative: customers deflected"),
        L("月净收益 · 回收期 · 三年累计", "monthly net · payback · three-year total"),
      ].map((t, i) => (
        <g key={i}>
          <rect x={18} y={66 + i * 30} width={300} height={24} rx="4" fill="var(--surface-2)" stroke={i === 2 ? "#c0453f" : "var(--hairline-strong)"} />
          <text x={168} y={82 + i * 30} textAnchor="middle" style={{ font: "500 9.5px var(--f-body)", fill: "var(--ink)" }}>{t}</text>
        </g>
      ))}

      <line x1={340} y1={16} x2={340} y2={200} stroke="var(--hairline-strong)" strokeDasharray="4 3" />

      <FBox x={362} y={24} w={300} h={32} label={L("案例二:一通电话的逐跳复盘", "Case two: one call, hop by hop")} tone="a" />
      {[
        L("2 分 17 秒 · 11 个轮次", "2m17s · 11 turns"),
        L("第 6 跳:技师名听错(热词缺失)", "hop 6: name misheard (hotword missing)"),
        L("第 7 跳:复述确认救了回来", "hop 7: read-back rescued it"),
        L("第 9 跳:知识缺失 → 兜底不编造", "hop 9: no knowledge → fallback, no invention"),
      ].map((t, i) => (
        <g key={i}>
          <rect x={362} y={66 + i * 30} width={300} height={24} rx="4" fill="var(--surface-2)" stroke={i === 1 ? "#c0453f" : i >= 2 ? "#2e9e6b" : "var(--hairline-strong)"} />
          <text x={512} y={82 + i * 30} textAnchor="middle" style={{ font: "500 9.5px var(--f-body)", fill: "var(--ink)" }}>{t}</text>
        </g>
      ))}
      <FT x={340} y={204} cls="tm">{L("两个救命机制都不是模型能力,是你设计出来的流程", "neither rescue is a model capability; both are process you designed")}</FT>
    </FigFrame>
  );
};

/* ---------------- v30 · the three Twilio integration tiers ---------------- */
FIGN["v30-twilio"] = function ({ idx }) {
  const L = useL();
  const rows = [
    { t: L("TwiML 内置", "TwiML built-in"), tone: "warn",
      twilio: L("识别 + 合成", "recognition + synthesis"), yours: L("只有 webhook 返回的 XML", "only the XML you return"),
      note: L("2 天 · 最低可控", "2 d · least") },
    { t: "ConversationRelay", tone: "a",
      twilio: L("识别 + 合成", "recognition + synthesis"), yours: L("大脑:意图 / 知识 / 工具", "the brain: intent, knowledge, tools"),
      note: L("5 天 · 文本可审", "5 d · text") },
    { t: L("Media Streams 自建", "Media Streams (own)"), tone: "p",
      twilio: L("只负责运音频", "carries the audio only"), yours: L("ASR + 大脑 + TTS 全部", "ASR, brain and TTS — all of it"),
      note: L("15 天 · 全可控", "15 d · full") },
  ];
  return (
    <FigFrame idx={idx} h={224} cap={L("Twilio 的三档接入,区别只有一条线:这条线以上是 Twilio 负责的,以下是你负责的。线越往下,要写的代码越多、上手越慢,但可控性、可审计和中文电话适配越好。Media Streams 那一档把音频原样交给你——8 kHz μ-law、每 20 毫秒一帧 160 字节、base64 编码、双向——本书前五个模块讲的一切在这条路上都能直接用上。选哪一档取决于一件事:你的对话里有没有「构成承诺」的句子。要报价、要下单,就必须能在合成之前拦下来。", "Twilio's three tiers differ by exactly one line: above it is Twilio's responsibility, below it is yours. The lower the line, the more code you write and the slower you launch — and the better your control, auditability and fit for Mandarin telephony. The Media Streams tier hands you the audio as it is (8 kHz mu-law, one 160-byte frame every 20 ms, base64, bidirectional), where everything from this book's first five modules applies directly. Which tier you pick turns on one question: do your conversations contain commitments? Quoting a price or taking a booking means you must be able to intercept before synthesis.")}>
      <FT x={112} y={22} cls="tk">{L("接入档位", "tier")}</FT>
      <FT x={300} y={22} cls="ta">{L("Twilio 负责", "Twilio's side")}</FT>
      <FT x={481} y={22} cls="tp">{L("你负责", "your side")}</FT>
      <FT x={606} y={22} cls="tn">{L("代价", "price")}</FT>
      {rows.map((r, i) => {
        const y = 34 + i * 50;
        return (
          <g key={i}>
            <FBox x={18} y={y} w={188} h={38} label={r.t} tone={r.tone} />
            <rect x={214} y={y} width={172} height={38} rx="5" fill="color-mix(in srgb, var(--accent) 13%, transparent)" stroke="var(--accent)" />
            <text x={300} y={y + 23} textAnchor="middle" style={{ font: "500 9.5px var(--f-body)", fill: "var(--ink)" }}>{r.twilio}</text>
            <rect x={392} y={y} width={178} height={38} rx="5" fill="color-mix(in srgb, var(--primary) 13%, transparent)" stroke="var(--primary)" />
            <text x={481} y={y + 23} textAnchor="middle" style={{ font: "500 9.5px var(--f-body)", fill: "var(--ink)" }}>{r.yours}</text>
            <text x={576} y={y + 23} style={{ font: "500 9px var(--f-mono)", fill: "var(--muted)" }}>{r.note}</text>
          </g>
        );
      })}
      <line x1={389} y1={28} x2={389} y2={190} stroke="#c0453f" strokeDasharray="4 3" strokeWidth="1.4" />
      <FT x={340} y={202} cls="tm">{L("音频帧:8 kHz μ-law · 20 ms 一帧 · 160 字节 · base64 · 双向", "frames: 8 kHz mu-law · 20 ms · 160 bytes · base64 · bidirectional")}</FT>
      <FT x={340} y={218} cls="tn">{L("要报价、要下单 → 必须能在合成之前拦下来", "quoting or booking → intercept before synthesis")}</FT>
    </FigFrame>
  );
};

/* ---------------- v31 · baseline A: fixed scripts only ---------------- */
FIGN["v31-base1"] = function ({ idx }) {
  const L = useL();
  const bx = [
    { x: 20,  l: L("顾客", "caller"), s: L("电话 / 微信", "phone / IM") },
    { x: 176, l: L("渠道 · 会话层", "channel · session"), s: L("上下文 / 留痕", "context / logging") },
    { x: 346, l: L("意图识别", "intent"), s: L("关键词规则", "keyword rules") },
    { x: 516, l: L("固定话术表", "fixed script table"), s: L("人写 · 静态", "human-written · static") },
  ];
  return (
    <FigFrame idx={idx} h={206} cap={L("最轻的一档基线:关键词命中就回一条预先写好的话术,没命中就转人工。它的全部优点是确定——答什么是人写的,出不了幻觉,延迟三毫秒,一分钱不花;它的全部问题是覆盖——话术表写到八成命中之后,每多一个百分点都要新增大量规则,而且规则之间开始互相打架。链路上没有任何知识层,价格和空档这些会变的东西只能靠人去改表。", "The lightest baseline: a keyword match returns a pre-written line, and a miss goes to a human. Its entire virtue is certainty — a person wrote the answer, nothing can be hallucinated, latency is three milliseconds and cost is nil. Its entire problem is coverage: past about eighty percent, every additional point costs a pile of rules that begin contradicting one another. There is no knowledge layer anywhere on the path, so prices and availability can only be changed by editing the table by hand.")}>
      {bx.map((b, i) => (
        <g key={i}>
          <FBox x={b.x} y={56} w={126} h={44} label={b.l} sub={b.s} tone={i === 3 ? "n" : "p"} />
          {i < 3 ? <FArrow x1={b.x + 126} y1={78} x2={bx[i + 1].x - 6} y2={78} /> : null}
        </g>
      ))}
      <FT x={161} y={70} cls="tn">{L("问题", "question")}</FT>
      <FT x={324} y={70} cls="tn">{L("文本", "text")}</FT>
      <FT x={494} y={70} cls="tn">{L("命中", "match")}</FT>

      <polyline points="579,100 579,148 83,148 83,112" className="" fill="none" stroke="var(--muted)" strokeWidth="1.3" strokeDasharray="" />
      <FArrow x1={83} y1={130} x2={83} y2={108} />
      <FT x={330} y={142} cls="tn">{L("原样回复", "returned verbatim")}</FT>

      <FArrow x1={409} y1={100} x2={409} y2={162} />
      <FT x={418} y={126} anchor="start" cls="tn">{L("未命中", "no match")}</FT>
      <FBox x={346} y={164} w={126} h={36} label={L("转人工", "hand off")} tone="warn" />

      <FT x={579} y={40} cls="ta">{L("没有知识层", "no knowledge layer")}</FT>
      <line x1={516} y1={46} x2={642} y2={46} stroke="#c0453f" strokeDasharray="4 3" />
    </FigFrame>
  );
};

/* ---------------- v31 · baseline B: scripts plus direct generation ---------------- */
FIGN["v31-base2"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={252} cap={L("在基线一上加了大模型直答和一条通往业务系统的工具调用,覆盖率确实上去了,但代价是把「答不上来」变成了「答得很像但可能是错的」。图上两处标红就是它的两个结构性问题,而且都不在模型里、在拓扑里:虚线框是本该接进来却没有的门店实时资料——模型没有任何证据来源,却仍被要求回答;右上角那行说的是门店差异写死在话术表里,三十家店就是三十份要人工同步的文件。", "Adding direct generation and a tool call to the business system genuinely raises coverage, at the price of turning cannot-answer into sounds-right-but-may-be-wrong. The two marked spots are its structural problems, and neither is in the model — both are in the topology. The dashed box is the shop's live data, which ought to be connected and is not, leaving the model with no evidence source while still being asked to answer. The note top right is the other: shop differences are frozen into the script table, so thirty shops mean thirty files somebody must keep in sync.")}>
      <FBox x={20}  y={96}  w={96}  h={42} label={L("顾客", "caller")} tone="p" />
      <FBox x={148} y={96}  w={124} h={42} label={L("会话 · 意图", "session · intent")} tone="p" />
      <FBox x={306} y={46}  w={150} h={40} label={L("固定话术表", "fixed script table")} tone="n" />
      <FBox x={306} y={124} w={150} h={40} label={L("大模型直答", "direct generation")} sub={L("凭训练记忆", "from memory")} tone="bad" />
      <FBox x={148} y={186} w={124} h={40} label={L("工具调用", "tool call")} tone="n" />
      <FBox x={306} y={186} w={150} h={40} label={L("业务系统", "business system")} sub={L("预约 · 会员", "booking · members")} tone="n" />

      <FArrow x1={116} y1={117} x2={142} y2={117} />
      <polyline points="272,108 290,108 290,66 300,66" fill="none" stroke="var(--muted)" strokeWidth="1.3" />
      <FArrow x1={294} y1={66} x2={302} y2={66} />
      <FT x={288} y={42} cls="tn">{L("命中", "match")}</FT>
      <polyline points="272,126 290,126 290,144 300,144" fill="none" stroke="var(--muted)" strokeWidth="1.3" />
      <FArrow x1={294} y1={144} x2={302} y2={144} />
      <FT x={286} y={172} cls="tn">{L("未命中", "no match")}</FT>
      <FArrow x1={210} y1={138} x2={210} y2={182} />
      <FArrow x1={272} y1={206} x2={302} y2={206} />

      <rect x={490} y={124} width={160} height={40} rx="6" fill="none" stroke="var(--hairline-strong)" strokeDasharray="5 4" />
      <FT x={570} y={149} cls="tn">{L("门店实时资料", "the shop's live data")}</FT>
      <line x1={486} y1={144} x2={466} y2={144} stroke="#c0453f" strokeWidth="1.5" strokeDasharray="5 4" />
      <FT x={570} y={112} cls="ta">{L("✗ 没接进来", "✗ never connected")}</FT>
      <FT x={648} y={40} anchor="end" cls="ta">{L("每开一家店复制一份", "one copy per shop")}</FT>

      <FT x={20} y={242} anchor="start" cls="tm">{L("长尾问题落到无据可依的模型 —— 它不会沉默,它会编一个很合理的价格。", "the tail lands on a model with no evidence — it does not go quiet, it invents a plausible price")}</FT>
    </FigFrame>
  );
};

/* ---------------- v31 · the three-tier ladder ---------------- */
FIGN["v31-tiers"] = function ({ idx }) {
  const L = useL();
  return (
    <FigFrame idx={idx} h={352} cap={L("三层按顺序降级,每一层只负责一件事:FAQ 精确层负责快且准,RAG 检索层负责有据可依,兜底层负责答不上来时不编。顺序不能反——把高频问题送去检索多花三百毫秒、一份 token,还多一次检索错块的机会,而答案本来就是确定的。虚线回边是闭环:今天答不出的问题,下周变成 FAQ 里的一条,这也是这套系统会自己变好的唯一原因。最下面那条纪律独立于三层之外:价格、空档、排班永远走实时工具调用,它们是「此刻的状态」而不是知识,写进任何一层第二天都是过期数据。", "The three tiers degrade in order, each doing one job: the FAQ tier is fast and certain, the retrieval tier is evidenced, and the fallback tier refuses to invent. The order cannot be reversed — routing a high-frequency question through retrieval costs three hundred milliseconds, a token bill and one more chance to fetch the wrong chunk, for an answer that was already determinate. The dashed return edge is the loop: a question you cannot answer today becomes an FAQ entry next week, which is the only reason this system improves on its own. The rule along the bottom sits outside all three tiers: prices, availability and rotas always go through a live tool call, because they are a state right now rather than knowledge, and in any tier they would be stale by tomorrow.")}>
      <FBox x={14} y={142} w={92} h={44} label={L("顾客提问", "the question")} tone="n" />

      <FT x={140} y={28} anchor="start" cls="tp">{L("第一层 · 精确", "tier 1 · exact")}</FT>
      <FBox x={140} y={36}  w={152} h={46} label={L("FAQ 精确层", "FAQ tier")} sub={L("高频问答 · 同义词归并", "curated · synonyms merged")} tone="p" />
      <FBox x={330} y={36}  w={140} h={46} label={L("直接回复", "answer directly")} sub={L("3 ms · 零 token", "3 ms · no tokens")} tone="ok" />

      <FT x={140} y={122} anchor="start" cls="tp">{L("第二层 · 有据", "tier 2 · evidenced")}</FT>
      <FBox x={140} y={130} w={152} h={46} label={L("RAG 检索层", "retrieval tier")} sub={L("向量检索 top-k", "vector search top-k")} tone="p" />
      <FBox x={330} y={130} w={140} h={46} label={L("门店知识库", "knowledge base")} sub={L("按门店隔离", "per-shop namespace")} tone="n" />
      <FBox x={508} y={130} w={158} h={46} label={L("带证据作答", "answer with evidence")} sub={L("只许用检索到的", "only what was retrieved")} tone="ok" />

      <FT x={140} y={216} anchor="start" cls="tp">{L("第三层 · 兜底", "tier 3 · fallback")}</FT>
      <FBox x={140} y={224} w={152} h={46} label={L("兜底层", "fallback tier")} sub={L("明确说不知道", "says so plainly")} tone="p" />
      <FBox x={330} y={224} w={140} h={46} label={L("转人工", "hand off")} sub={L("带上下文交接", "with the context card")} tone="warn" />
      <FBox x={508} y={224} w={158} h={46} label={L("记录缺口", "record the gap")} sub={L("问题 + 门店 + 时间", "question · shop · time")} tone="a" />

      <polyline points="106,156 128,156 128,59 134,59" fill="none" stroke="var(--primary)" strokeWidth="1.5" />
      <FArrow x1={128} y1={59} x2={136} y2={59} c="var(--primary)" />
      <FT x={132} y={112} anchor="start" cls="tp">{L("① 先查", "① first")}</FT>
      <FArrow x1={292} y1={59} x2={326} y2={59} />
      <FT x={309} y={50} cls="tn">{L("命中", "hit")}</FT>

      <FArrow x1={216} y1={82} x2={216} y2={126} c="var(--primary)" />
      <FT x={224} y={108} anchor="start" cls="tn">{L("② 未命中,降级", "② miss, degrade")}</FT>
      <FArrow x1={292} y1={153} x2={326} y2={153} />
      <FT x={309} y={144} cls="tn">{L("检索", "search")}</FT>
      <FArrow x1={470} y1={153} x2={504} y2={153} />
      <FT x={487} y={144} cls="tn">{L("证据", "evidence")}</FT>

      <FArrow x1={216} y1={176} x2={216} y2={220} c="var(--primary)" />
      <FT x={224} y={202} anchor="start" cls="tn">{L("③ 低于阈值,降级", "③ below threshold")}</FT>
      <FArrow x1={292} y1={247} x2={326} y2={247} />
      <FArrow x1={470} y1={247} x2={504} y2={247} />

      <polyline points="587,270 587,284 114,284 114,70 134,70" fill="none" stroke="var(--accent)" strokeWidth="1.4" strokeDasharray="5 4" />
      <FArrow x1={128} y1={70} x2={136} y2={70} c="var(--accent)" />
      <FT x={340} y={278} cls="ta">{L("缺口回补:每周补进 FAQ 与知识库", "the gap loop: into FAQ and the knowledge base weekly")}</FT>

      <polyline points="60,186 60,320 134,320" fill="none" stroke="var(--muted)" strokeWidth="1.3" />
      <FArrow x1={128} y1={320} x2={136} y2={320} />
      <FBox x={140} y={300} w={292} h={40} label={L("实时工具调用", "live tool call")} sub={L("价格 · 空档 · 排班 · 余额", "price · slots · rota · balance")} tone="a" />
      <FT x={444} y={316} anchor="start" cls="ta">{L("永远不从 FAQ 或 RAG 取", "never served from FAQ or RAG")}</FT>
      <FT x={444} y={332} anchor="start" cls="tn">{L("它们是此刻的状态,不是知识", "a state right now, not knowledge")}</FT>
    </FigFrame>
  );
};

/* ---------------- v31 · per-shop configuration inheritance ---------------- */
FIGN["v31-store"] = function ({ idx }) {
  const L = useL();
  const inlets = [
    { y: 24,  l: L("电话号码", "phone number"), s: L("webhook 的被叫号码", "the dialled number") },
    { y: 70,  l: L("微信入口", "WeChat entry"),  s: L("二维码 / 小程序", "QR code / mini-program") },
    { y: 116, l: L("平台 IM", "platform IM"),   s: L("平台店铺 ID", "platform shop id") },
  ];
  return (
    <FigFrame idx={idx} h={334} cap={L("门店差异要靠继承解决,而不是复制:全局层放引擎与护栏,品牌层放连锁统一的价目与音色,门店层只放这家店真正不同的那几行,覆盖优先级是门店大于品牌大于全局。左边那一列是门店标识的来源——它按渠道各不相同:电话靠 webhook 带来的被叫号码(第二十二、三十章),微信靠二维码或小程序入口,平台 IM 靠店铺 ID,三条路解析出同一个门店标识后汇到一起。注意装配器有两个产出:一个是该店的运行时配置,另一个是该店的 TwiML——问候语、音色、流参数都是按店装配的,而 TwiML 里的 Parameter 又把门店标识带进媒体流,让音频链路知道自己在服务哪家店。反方向同样重要:护栏、禁语、合规告知只放在全局层,任何门店都不能覆盖。", "Shop differences are solved by inheritance rather than copying: a global layer holds the engines and guardrails, a brand layer holds the chain's shared prices and voice, and a shop layer holds only the handful of lines genuinely different here, with shop overriding brand overriding global. The left column is where the shop identity comes from, and it differs by channel: telephony reads the dialled number from the webhook (chapters CH1 and CH4), WeChat reads a QR code or mini-program entry, platform IM reads a shop id — three paths resolving to one identity. Note the assembler has two outputs: that shop's runtime configuration, and that shop's TwiML, because the greeting, the voice and the stream parameters are all assembled per shop — and a Parameter inside the TwiML carries the identity into the media stream so the audio path knows which shop it is serving. The other direction matters equally: guardrails, banned phrasing and the compliance notice live only in the global layer, where no shop can override them.")}>
      {inlets.map((c, i) => (
        <g key={i}>
          <FBox x={14} y={c.y} w={132} h={38} label={c.l} sub={c.s} tone="n" />
          <FArrow x1={146} y1={c.y + 19} x2={170} y2={75 + i * 8} />
        </g>
      ))}
      <FT x={80} y={16} cls="tn">{L("门店标识从哪来", "identity source")}</FT>

      <FBox x={176} y={62} w={120} h={42} label={L("解析门店标识", "resolve shop id")} tone="n" />
      <FArrow x1={296} y1={83} x2={314} y2={83} />
      <FBox x={318} y={62} w={116} h={42} label={L("配置装配器", "config assembler")} sub={L("三层合并", "merge three layers")} tone="a" />

      <FArrow x1={434} y1={72} x2={452} y2={45} c="var(--accent)" />
      <FBox x={456} y={24} w={210} h={42} label={L("该店运行时配置", "that shop's runtime config")} sub={L("应答层 + 知识命名空间", "tiers + knowledge namespace")} tone="ok" />
      <FArrow x1={434} y1={94} x2={452} y2={111} c="var(--accent)" />
      <FBox x={456} y={90} w={210} h={42} label={L("该店 TwiML", "that shop's TwiML")} sub={L("问候语 · 音色 · 流参数", "greeting · voice · stream")} tone="ok" />
      <FT x={456} y={150} anchor="start" cls="ta">{L("Parameter 把标识带进媒体流", "identity rides the stream")}</FT>

      <FArrow x1={376} y1={104} x2={376} y2={172} />
      <FBox x={176} y={176} w={400} h={38} label={L("全局层", "global layer")} sub={L("引擎 · 护栏与禁语 · 合规告知", "engines · guardrails · compliance notice")} tone="n" />
      <FBox x={176} y={224} w={400} h={38} label={L("品牌层", "brand layer")} sub={L("连锁价目 · 品牌音色 · 统一优惠", "chain prices · brand voice · promotions")} tone="n" />
      <FBox x={176} y={272} w={400} h={38} label={L("门店层", "shop layer")} sub={L("本店价目 · 技师 · 地址 · 号码", "local prices · therapists · address · number")} tone="a" />

      <FArrow x1={160} y1={296} x2={160} y2={188} c="var(--accent)" />
      <text x={152} y={302} textAnchor="start" className="ta" transform="rotate(-90 152 302)">{L("门店 > 品牌 > 全局", "shop > brand > global")}</text>

      <FT x={588} y={198} anchor="start" cls="tn">{L("改一次全生效", "all shops")}</FT>
      <FT x={588} y={246} anchor="start" cls="tn">{L("连锁统一", "chain-wide")}</FT>
      <FT x={588} y={294} anchor="start" cls="ta">{L("只写不同的", "this shop only")}</FT>

      <FT x={14} y={326} anchor="start" cls="tm">{L("加一家店 = 加一行数据(含它的号码)。护栏只在全局层,门店无法覆盖。", "a new shop is a new row, its number included; guardrails stay global")}</FT>
    </FigFrame>
  );
};
