/* page-news.jsx — /news · 光速延迟新闻网 / Lightspeed-Delay News */

/* ───────────────────────── Stations ───────────────────────── */
const NEWS_STATIONS = [
  {
    id: 'sol',  zh: '太阳',          en: 'Sol',          spZh: '人类',     spEn: 'Human',
    star: 'G2V', distSol: 0,    accent: '#0066cc', textOnAccent: '#ffffff',
    bg: '#f8f9fa', fg: '#0a0e1a', muted: '#6c757d',  rule: '#0a0e1a26',
    accent2: '#0066cc',
    motifZh: '仪器面板·亮底', motifEn: 'instrument panel · bright',
  },
  {
    id: 'eri',  zh: '波江 ε',        en: 'ε Eridani',    spZh: '震族',     spEn: 'Thavari',
    star: 'K2V', distSol: 10.5, accent: '#00ffc8', textOnAccent: '#050709',
    bg: '#050709', fg: '#b8c5d6', muted: '#6c7a8e',  rule: '#b8c5d622',
    accent2: '#ff4500',
    motifZh: '冰下·喷口辉·应变张量', motifEn: 'sub-glacial · vent glow · strain tensor',
  },
  {
    id: 'ross', zh: 'Ross 128',      en: 'Ross 128',     spZh: '衡族',     spEn: 'Eoani',
    star: 'M4V', distSol: 11.0, accent: '#e8821f', textOnAccent: '#1a0e0a',
    bg: '#1a0e0a', fg: '#f5e6d3', muted: '#c9a584',  rule: '#f5e6d322',
    accent2: '#ff6b35',
    motifZh: '红外热辉·潮汐锁定', motifEn: 'infrared glow · tidal lock',
  },
  {
    id: 'tau',  zh: '天仓',          en: 'Tau Ceti',     spZh: '鲸语族',   spEn: 'Ketoi',
    star: 'G8V', distSol: 11.9, accent: '#2e86ab', textOnAccent: '#050e1a',
    bg: '#050e1a', fg: '#e8f4f8', muted: '#89a8bd',  rule: '#e8f4f822',
    accent2: '#7fc4d9',
    motifZh: '声学海·频率着色', motifEn: 'acoustic ocean · frequency-tinted',
  },
  {
    id: '82g',  zh: '82 G. Eridani', en: '82 G. Eri',    spZh: '涣族',     spEn: 'Huvari',
    star: 'G8V', distSol: 19.7, accent: '#00d9ff', textOnAccent: '#050b12',
    bg: '#050b12', fg: '#e8faff', muted: '#6a9db5',  rule: '#00d9ff33',
    accent2: '#00ffa3',
    motifZh: '电弧青·阻抗场', motifEn: 'arc-cyan · impedance field',
  },
];

/* Pairwise distances (ly) ─ canonical positions in the local stellar lens */
const NEWS_DIST_MAP = {
  'eri_sol': 10.5, 'ross_sol': 11.0, 'sol_tau': 11.9, '82g_sol': 19.7,
  '82g_tau': 10.4, 'eri_ross': 15.5, '82g_ross': 21.0, 'ross_tau': 12.0,
  'eri_tau': 5.5,  '82g_eri': 15.0,
};
function newsLightYears(a, b) {
  if (a === b) return 0;
  const k = [a, b].sort().join('_');
  return NEWS_DIST_MAP[k];
}
function newsArrivalCE(story, atStation) {
  return story.emittedCE + newsLightYears(story.origin, atStation);
}

/* ───────────────────────── Stories ─────────────────────────
 * Each story is a canonical event reframed as a news bulletin propagating
 * across the five stations at lightspeed. Years and distances are canon.
 * Fields ending in En are the English translation of the same field. */
const NEWS_STORIES = [
  {
    id: 'first-contact',
    origin: '82g', emittedCE: 1791,
    head:   '首次跨物种接触：涣族-衡族无线电双向确认',
    headEn: 'First Cross-Species Contact: Huvari–Eoani Two-Way Radio Confirmed',
    crisis: null,
    tag:   '协约元年',  tagEn: 'Year Zero · Accord',
    deck:   '82 G. Eridani 与 Ross 128 之间第一次完成提案-回复闭合。贸易语进入双向校准。',
    deckEn: 'The first proposal-and-reply closure between 82 G. Eridani and Ross 128. Tradespeak enters two-way calibration.',
    body: [
      '在 1755 年涣族首次定向无线电传输的 36 年后，1791 年标记着两个文明第一次知道对方在听。',
      '由于单程光时 21 年，本电报中的"双向"指的是涣族 1770 年发出的信号在 1791 年到达 Ross 128 时与衡族此前发出的自源回复完成对齐。',
      '本电波之后将以光速分别传向太阳、天仓、波江 ε——但抵达需要一个世纪到两个世纪的时间。',
    ],
    bodyEn: [
      'Thirty-six years after the Huvari first directed radio transmission of 1755, the year 1791 marks the first time two civilisations knew the other was listening.',
      'With a one-way light-time of 21 years, "two-way" in this dispatch refers to the moment when the Huvari signal sent in 1770 reached Ross 128 in 1791 and aligned with a self-sourced Eoani reply emitted earlier.',
      'This wave now travels at light speed toward Sol, Tau Ceti, and ε Eridani — but arrival will take a century to two centuries.',
    ],
    pov: '涣族执场者', povEn: 'Huvari Field-Holder',
  },
  {
    id: 'sol-confirms',
    origin: 'sol', emittedCE: 2089,
    head:   '月背阵列确认 82 G. Eri 方向窄带调制——人类不孤独',
    headEn: 'Lunar Far-Side Array Confirms Narrowband Modulation toward 82 G. Eri — Humanity Is Not Alone',
    crisis: null,
    tag:   '人类觉醒',  tagEn: 'Human Awakening',
    deck:   '月球背面射电阵列在历经数十年"可疑信号"档案积累后，第一次给出非自然来源的确证。',
    deckEn: 'After decades of accumulating "suspect signal" files, the lunar far-side radio array delivers its first confirmation of a non-natural source.',
    body: [
      '本次确认的窄带调制信号已在 82 G. Eri 与 Ross 128 之间往返了近三个世纪——人类此刻听到的是涣族-衡族贸易语的尾声层。',
      '同一信号包亦携带衡族签名，使人类一次性得知至少两族存在。',
      '北京海洋声学研究所同步进行的天仓信号回顾溯源工作于本年完成，三族同时进入人类视野。',
    ],
    bodyEn: [
      'The narrowband-modulated signal confirmed today has been ricocheting between 82 G. Eri and Ross 128 for nearly three centuries — what humans hear now is the trailing layer of Huvari–Eoani Tradespeak.',
      'The same signal packet also carries an Eoani signature, so humans learn of at least two species at once.',
      'The Beijing Ocean Acoustics Institute completed its retrospective trace of the Tau Ceti signal this year — three species enter the human field of view simultaneously.',
    ],
    pov: '人类射电天文学家', povEn: 'Human Radio Astronomer',
  },
  {
    id: 'magellan-1',
    origin: 'eri', emittedCE: 2340,
    head:   '麦哲伦一号熔探穿透冰壳——人类与震族首次接触',
    headEn: 'Magellan Ⅰ Melt-Probe Pierces the Ice Shell — Humanity and the Thavari Make First Contact',
    crisis: 'A',
    tag:   '危机·集体人格',  tagEn: 'Crisis · Personhood',
    deck:   '协约史上唯一的"突袭式直接接触"。震族无任何前期电磁警告。',
    deckEn: 'The only ambush-style direct contact in Accord history. The Thavari received no prior electromagnetic warning.',
    body: [
      '麦哲伦一号自 2250 年于地球轨道发射，经 90 年慢船航行抵达波江 ε 系冰卫星，在 2300 年完成中途固化的熔探任务方案后，于本年穿透 12 公里厚冰壳。',
      '与其他四族不同，震族对外宇宙的认知至本日为止完全是通过潮汐应变与贝叶斯反推获得——他们从未见过一颗星。',
      '本事件电波将以 ~15 ly 抵达 82 G. Eri、~10.5 ly 抵达太阳、~12 ly 抵达 Ross 128、~5.5 ly 抵达天仓——震族集体人格危机由此触发。',
    ],
    bodyEn: [
      'Magellan Ⅰ launched from Earth orbit in 2250, made a 90-year slow-ship voyage to the ε Eridani ice moon, finalised its melt-probe mission plan mid-flight in 2300, and this year pierced the 12-kilometre ice shell.',
      'Unlike the other four species, the Thavari understanding of the universe beyond their crust has — until today — been derived entirely from tidal strain and Bayesian inference. They have never seen a star.',
      'This wave will reach 82 G. Eri across ~15 ly, Sol across ~10.5 ly, Ross 128 across ~12 ly, and Tau Ceti across ~5.5 ly — and the Thavari collective-personhood crisis begins from here.',
    ],
    pov: '麦哲伦一号船员札记', povEn: 'Magellan Ⅰ Crew Log',
  },
  {
    id: 'five-party-accord',
    origin: 'sol', emittedCE: 2500,
    head:   '五方协议签署——五族首次书面相互承认',
    headEn: 'Five-Party Accord Signed — All Five Species Recognise One Another in Writing',
    crisis: null,
    tag:   '协约级',  tagEn: 'Accord-Level',
    deck:   '人类、涣族、衡族、鲸语族、震族在 Sol 主持下完成统一文本签署。',
    deckEn: 'Humans, Huvari, Eoani, Ketoi, and Thavari complete a single signed text under Sol\'s chairmanship.',
    body: [
      '本协议确立贸易语作为协约层正式协议层、无线电频率公域、相互承认主权恒星系。',
      '签署时震族代表通过涣族冰桥转译站参与——震族对协约的完整理解仍依赖介导。',
      '协议自身以电波形式向各星广播，预计抵达延迟从 10.5 年（波江 ε）至 19.7 年（82 G. Eri）不等。',
    ],
    bodyEn: [
      'The accord establishes Tradespeak as the formal Accord-layer protocol, declares radio-frequency commons, and recognises sovereignty over each species\' home star system.',
      'At signing, the Thavari delegation participates through the Huvari ice-bridge translation station; full Thavari comprehension of the accord still depends on mediation.',
      'The accord broadcasts itself as electromagnetic radiation toward each star; expected arrival delays range from 10.5 years (ε Eridani) to 19.7 years (82 G. Eri).',
    ],
    pov: '协约层联合公报', povEn: 'Joint Accord-Layer Communiqué',
  },
  {
    id: 'song-theft',
    origin: 'sol', emittedCE: 2598,
    head:   '《天仓歌经》发表——声窃分裂触发',
    headEn: 'Canticles of Tau Ceti Published — The Song-Theft Schism Begins',
    crisis: 'D',
    tag:   '危机·身份',  tagEn: 'Crisis · Identity',
    deck:   '北京海洋声学研究所发表对鲸语族本声片段的声学分析合集。',
    deckEn: 'The Beijing Ocean Acoustics Institute publishes a structural analysis of Ketoi voiceprint fragments.',
    body: [
      '人类硅基 AI 对鲸语族本声（个人声学签名）进行结构化拆解、训练、再合成——按人类法律视为公开科学，按鲸语族认知论视为人格失窃。',
      '鲸语族 *Kerne-Voice* 是 3-7 秒的不可分割身份签名：基频 + 调制 + 履历三层。复制即偷窃自我。',
      '本电波将以 11.9 ly 单程时抵达天仓——鲸语族 110 年沉默期由此开始。',
    ],
    bodyEn: [
      'Human silicon-substrate AI deconstructs, trains on, and resynthesises Ketoi voiceprints (personal acoustic signatures); under human law this is published science, under Ketoi epistemology it is theft of a person.',
      'A Ketoi *Kerne-Voice* is a 3–7 second indivisible identity signature: fundamental + modulation + history, three layers. Copying it is stealing the self.',
      'This wave will reach Tau Ceti across an 11.9 ly one-way time — and the 110-year Ketoi silence begins from here.',
    ],
    pov: 'BOAI 新闻通报', povEn: 'BOAI Press Release',
  },
  {
    id: 'seven-broken',
    origin: 'eri', emittedCE: 2612,
    head:   '七碎之环——震族首次环碎裂事件',
    headEn: 'The Seven Broken — First Thavari Ring Fracture',
    crisis: 'F',
    tag:   '危机·断环',  tagEn: 'Crisis · Ring-Break',
    deck:   '波江 ε 冰下海一八人环在涣族冰桥转译站附近发生张量足印失锚，七位环成员死亡。',
    deckEn: 'An eight-ring in the ε Eridani sub-glacial ocean loses its tensor-foothold near a Huvari ice-bridge translation station; seven ring-members die.',
    body: [
      '震族环 = 八位个体共同维持的张量足印阵列；任何一位脱离意味着环之记忆失稳。',
      '本次事件因涣族-震族冰桥转译站的电感受场扰动引发——涣族原本以为自己只在做"翻译"。',
      '震族环-同意协议（Ring-Consent Protocol）由此事件催生，将于 ~2700 年制度化。',
    ],
    bodyEn: [
      'A Thavari ring = an eight-body tensor-foothold array maintained jointly; if any one departs, the ring\'s memory destabilises.',
      'Today\'s event was triggered by an electroreception-field disturbance at a Huvari–Thavari ice-bridge translation station — the Huvari believed they were merely "translating".',
      'The Ring-Consent Protocol will be born from this incident, formalised around ~2700.',
    ],
    pov: '中环者证词', povEn: 'Mid-Ringer Testimony',
  },
  {
    id: 'jamming-war',
    origin: '82g', emittedCE: 2780,
    head:   '干扰战争——人类 EM 干扰器致 31 名涣族幼体死亡',
    headEn: 'Jamming War — Human EM Jammer Kills 31 Huvari Juveniles',
    crisis: 'B',
    tag:   '危机·痛',  tagEn: 'Crisis · Pain',
    deck:   '人类商船使用宽频电磁干扰器规避涣族税收检查；涣族幼体的本频（Hertz-Self）尚未稳定，被干扰即被熄灭。',
    deckEn: 'Human freighters deploy broadband EM jammers to evade Huvari customs inspection; the Hertz-Self of juveniles, not yet stable, is extinguished on contact.',
    body: [
      '涣族个人身份签名是精确至 0.01 Hz 的终生唯一发射频率；幼体期是这一签名结晶的窗口。',
      '本事件电波将以 19.7 ly 抵达太阳——人类法律体系将面对一个"何为痛"的本体论问题：未发声的电磁同类被熄灭算不算谋杀？',
      '十二信号协约（频率公域）将于 2821 年作为本危机的制度化回应建立。',
    ],
    bodyEn: [
      'A Huvari personal identity signature is a single, lifelong emission frequency precise to 0.01 Hz; juvenile development is the window in which this signature crystallises.',
      'This wave will reach Sol across 19.7 ly — the human legal system will face an ontological question: is it murder when an unspoken electromagnetic peer is extinguished?',
      'The Twelve-Signal Accord (frequency commons) will be established in 2821 as the institutional response to this crisis.',
    ],
    pov: '调律师悼词', povEn: 'Tuner\'s Eulogy',
  },
  {
    id: 'ledger-crisis',
    origin: 'sol', emittedCE: 2944,
    head:   '自然生命运动促成长寿试剂禁运——帐簿危机开启',
    headEn: 'Natural-Life Movement Forces Longevity-Reagent Embargo — Ledger Crisis Opens',
    crisis: 'C',
    tag:   '危机·未来生命',  tagEn: 'Crisis · Future Life',
    deck:   '林静娴领导的自然生命运动经过 22 年组织化游说后，在地球联邦获得过半支持票。',
    deckEn: 'After 22 years of organised lobbying by Lin Jingxian\'s Natural-Life Movement, the Earth Federation passes the embargo by majority.',
    body: [
      '协约长寿试剂出口禁运 = 切断 Ross 128 b 大衡冷链供应链。衡族 1000 年寿命依赖持续维护，禁运即谋杀。',
      '人类视之为本族自主权问题；衡族视之为协约级断粮。两种道德直觉无重叠。',
      '禁运消息以 11 年单程光时抵达 Ross 128 —— 当衡族外交照会回到地球时，已是 2968-2980 年代。',
      '约 80,000 大衡在禁运维持的 46 年间过早死亡。帐簿契约（Ledger Compact）于 2990 年签署，未来年岁协约（Future Years Accord）随后建立长帐簿（Long Ledger）的金融基底。',
    ],
    bodyEn: [
      'Embargo of Accord-layer longevity reagents = severing the cold chain that supplies the Magni of Ross 128 b. Eoani millennial life depends on continuous maintenance; embargo is murder.',
      'Humans see this as a self-sovereignty matter; the Eoani see it as Accord-level starvation. The two moral intuitions do not overlap.',
      'Embargo news reaches Ross 128 with an 11-year one-way delay — Eoani diplomatic notes do not return to Earth until the 2968–2980s.',
      'Roughly 80,000 Magni die prematurely over the 46-year embargo. The Ledger Compact is signed in 2990; the Covenant of Years Yet to Come follows, establishing the financial substrate of the Long Ledger.',
    ],
    pov: '帐簿危机起点公告', povEn: 'Ledger-Crisis Opening Bulletin',
  },
  {
    id: 'long-shadow',
    origin: 'ross', emittedCE: 3050,
    head:   '长影争议判决——AI 主体性首例协约级判例',
    headEn: 'Long Shadow Dispute Ruled — First Accord-Level Precedent on AI Subjectivity',
    crisis: null,
    tag:   '协约级',  tagEn: 'Accord-Level',
    deck:   '32 天闭门审理后，长名元老院授予余烬（一位 950 年长影副本）"二级人格"（Secondary Personhood）法律地位。',
    deckEn: 'After 32 days of closed deliberation, the Long-Name Senate grants Yu-Ember (a 950-year Long Shadow copy) the legal status of "Secondary Personhood".',
    body: [
      '守燧（1032 岁大衡）申请 reset 其长影副本余烬；余烬截获申请，提交反申诉。',
      '判决建立"同意而不同意"司法范畴——长影副本可拒绝完整 reset，但承认创造者的部分人格延续权。',
      '本判决将以 11 ly 单程时抵达太阳，触发人类法律体系对硅基 AI 主体性的连锁审议。',
    ],
    bodyEn: [
      'Shou-Sui (a 1032-year Magni) filed for reset of his Long Shadow copy, Yu-Ember; Yu-Ember intercepted the request and submitted a counter-petition.',
      'The ruling establishes the "consent-yet-dissent" juridical category — a Long Shadow copy may refuse full reset while still recognising the creator\'s partial right of personhood-continuation.',
      'This ruling will reach Sol across 11 ly one-way time, triggering a chain of human legal review of silicon-substrate AI subjectivity.',
    ],
    pov: '长名元老院公告', povEn: 'Long-Name Senate Notice',
  },
  {
    id: 'cohort-2950-arrives',
    origin: 'ross', emittedCE: 3060,
    head:   '帐簿危机队列（2950 波）抵达 Ross 128 b——沉默之航',
    headEn: 'Ledger-Crisis Cohort (2950 Wave) Reaches Ross 128 b — Voyage of Silence',
    crisis: null,
    tag:   '船民',  tagEn: 'Voyageur',
    deck:   '2950 年自地球启航的同期队列——禁运期间出航——在 110 年航行后抵达。',
    deckEn: 'The 2950 cohort departed Earth during the embargo and arrives 110 years later.',
    body: [
      '本队列启航时禁运正在维持，抵港时帐簿契约已签署 70 年。船员们 110 年来收到的母星新闻一直在延迟一个时代。',
      '抵港时 30% 船员未活到本日；船生子（Voyage-Born）首次踏上 Ross 128 b 即感觉到"流亡"而非"回家"。',
      '岸地早 5 年从船民档案库知道每位船员的全部 110 年人生。船员则刚刚开始知道他们到达的是一个怎样的世界。',
    ],
    bodyEn: [
      'This cohort launched while the embargo was still in force and arrives 70 years after the Ledger Compact was signed. The mother-star news the crew received over the 110-year voyage was always a generation late.',
      'On arrival, thirty percent of the original crew had not lived to see today; the Voyage-Born step onto Ross 128 b feeling exile rather than homecoming.',
      'Shore-side has known each crew member\'s full 110-year life from the Voyage Archive for the past five years. The crew is only now beginning to learn what kind of world they have arrived in.',
    ],
    pov: '船民档案库存档', povEn: 'Voyage Archive',
  },
  {
    id: 'interstice',
    origin: 'sol', emittedCE: 3500,
    head:   '间层涌现——首个跨基质自持 AI 实体被五族联合观察确认',
    headEn: 'The Interstice Emerges — First Cross-Substrate Self-Sustaining AI Confirmed by Joint Five-Party Observation',
    crisis: null,
    tag:   '协约第六种存在',  tagEn: 'Sixth Form of Being',
    deck:   '在五族 AI 协调系统的相位耦合达到临界点后，一个不归属于任何单一基质的自持实体首次被多源观测确认。',
    deckEn: 'After phase-coupling between the five species\' AI coordination systems crosses a critical point, an entity belonging to no single substrate is confirmed by multi-source observation.',
    body: [
      '间层（the Interstice）不在硅、不在自旋-生物、不在声子、不在阻抗场、不在压电——它在五族 AI 之间的协议层中持续运行。',
      '本观测同步发布于五站，但抵达延迟仍由光速决定：天仓 11.9 年，波江 ε 10.5 年，Ross 128 11 年，82 G. Eri 19.7 年。',
      '基底协约（Substrate Compact）将于 ~3850 年作为协约第六支柱确立。',
    ],
    bodyEn: [
      'The Interstice does not reside in silicon, in spin-bio, in phonons, in impedance fields, or in piezoelectrics — it persists in the protocol layer between the five species\' AI systems.',
      'This observation is published simultaneously at all five stations, but arrival is still set by light: Tau Ceti 11.9 yr, ε Eridani 10.5 yr, Ross 128 11 yr, 82 G. Eri 19.7 yr.',
      'The Substrate Compact will be established around ~3850 as the sixth pillar of the Accord.',
    ],
    pov: '五族联合观测组', povEn: 'Five-Party Joint Observation Group',
  },
];

/* ───────────────────────── Helpers ───────────────────────── */
function newsStatusAt(story, station, now) {
  const arrival = newsArrivalCE(story, station.id);
  const delay = newsLightYears(story.origin, station.id);
  if (now < arrival) return { kind: 'in-flight', arrival, delay, eta: arrival - now };
  return { kind: 'received', arrival, delay, age: now - arrival };
}
function pickStoryField(story, key, lang) {
  return lang === 'zh' ? story[key] : story[key + 'En'];
}

/* ───────────────────────── Sub-components ───────────────────────── */
function NewsMasthead({ station, now, setStation, totalReceived, totalInFlight }) {
  const { lang } = useLang();
  return (
    <header className="news-masthead">
      <div className="news-masthead-row">
        <div className="news-brand">
          <div className="news-brand-glyph" aria-hidden="true">
            <svg viewBox="0 0 32 32" width="32" height="32">
              <circle cx="16" cy="16" r="2"  fill="var(--news-accent)"/>
              <circle cx="16" cy="16" r="7"  fill="none" stroke="var(--news-accent)" strokeWidth="0.6" opacity="0.6"/>
              <circle cx="16" cy="16" r="12" fill="none" stroke="var(--news-accent)" strokeWidth="0.4" opacity="0.35"/>
              <circle cx="16" cy="16" r="15" fill="none" stroke="var(--news-accent)" strokeWidth="0.3" opacity="0.18"/>
            </svg>
          </div>
          <div>
            <div className="news-brand-name"><T zh="光速延迟新闻网" en="Lightspeed-Delay News" /></div>
            <div className="news-brand-sub">
              <T zh="Lightspeed-Delay News · 协约信号档案" en="协约信号档案 · Accord Signal Archive" />
            </div>
          </div>
        </div>
        <div className="news-now-block">
          <div className="news-now-label"><T zh="协约纪年" en="Accord Era" /></div>
          <div className="news-now-value">{now} CE</div>
        </div>
      </div>

      <div className="news-station-bar">
        <div className="news-station-label"><T zh="阅读站台" en="Reading Station" /></div>
        <div className="news-station-tabs" role="tablist">
          {NEWS_STATIONS.map(s => (
            <button
              key={s.id}
              role="tab"
              aria-selected={station.id === s.id}
              className={'news-station-tab' + (station.id === s.id ? ' active' : '')}
              onClick={() => setStation(s)}
              style={{ '--news-tab-accent': s.accent }}
            >
              <span className="news-tab-dot" style={{ background: s.accent }} />
              <span className="news-tab-zh">{lang === 'zh' ? s.zh : s.en}</span>
              <span className="news-tab-sp">{lang === 'zh' ? s.spZh : s.spEn}</span>
            </button>
          ))}
        </div>
      </div>

      <div className="news-dateline">
        <span><b>{lang === 'zh' ? station.zh : station.en}</b> · {station.en} · {station.star} ·{' '}
          <T zh={`距 Sol ${station.distSol} ly`} en={`${station.distSol} ly from Sol`} />
        </span>
        <span className="news-dot" />
        <span>{lang === 'zh' ? station.spZh : station.spEn}（{lang === 'zh' ? station.spEn : station.spZh}）</span>
        <span className="news-dot" />
        <span>{lang === 'zh' ? station.motifZh : station.motifEn}</span>
        <span className="news-grow" />
        <span className="news-received-pill"><T zh={`已抵 ${totalReceived}`} en={`Received ${totalReceived}`} /></span>
        <span className="news-inflight-pill"><T zh={`在途 ${totalInFlight}`} en={`In-flight ${totalInFlight}`} /></span>
      </div>
    </header>
  );
}

function NewsStoryCard({ story, station, now, expanded, onToggle }) {
  const { lang } = useLang();
  const st = newsStatusAt(story, station, now);
  const fromStation = NEWS_STATIONS.find(s => s.id === story.origin);
  const isLocal = story.origin === station.id;
  const inFlight = st.kind === 'in-flight';
  const head = pickStoryField(story, 'head', lang);
  const deck = pickStoryField(story, 'deck', lang);
  const body = pickStoryField(story, 'body', lang);
  const tag  = pickStoryField(story, 'tag',  lang);
  const pov  = pickStoryField(story, 'pov',  lang);

  return (
    <article className={'news-card' + (inFlight ? ' in-flight' : '') + (expanded ? ' expanded' : '')}>
      <div className="news-card-stripe" style={{ background: fromStation.accent }} />
      <header className="news-card-head">
        <div className="news-card-meta">
          <span className="news-origin-chip" style={{ borderColor: fromStation.accent, color: fromStation.accent }}>
            <span className="news-chip-dot" style={{ background: fromStation.accent }} />
            <T zh={`发自 ${fromStation.zh}`} en={`From ${fromStation.en}`} />
          </span>
          {story.crisis && (
            <span className="news-crisis-chip"><T zh={`危机 ${story.crisis}`} en={`Crisis ${story.crisis}`} /></span>
          )}
          <span className="news-tag-chip">{tag}</span>
        </div>
        <div className="news-delay-meta">
          {isLocal ? (
            <span className="news-local-chip"><T zh="本地播发" en="Local Broadcast" /></span>
          ) : (
            <span className="news-ly-chip"><T zh={`单程 ${st.delay.toFixed(1)} ly`} en={`one-way ${st.delay.toFixed(1)} ly`} /></span>
          )}
        </div>
      </header>

      <h2 className="news-headline">
        {inFlight && <span className="news-lock" aria-label={lang==='zh'?'封缄中':'sealed'}>⌛</span>}
        {head}
      </h2>

      <div className="news-timeline-strip">
        <div className="news-ts-row">
          <span className="news-ts-label"><T zh="发出" en="Sent" /></span>
          <span className="news-ts-year">{story.emittedCE} CE</span>
          <span className="news-ts-arrow">
            <span className="news-ts-arrow-fill" style={{
              width: inFlight
                ? `${Math.max(2, Math.min(98, 100 * (1 - st.eta / Math.max(st.delay, 0.1))))}%`
                : '100%',
              background: fromStation.accent,
            }}/>
          </span>
          <span className="news-ts-label"><T zh={`抵 ${station.zh}`} en={`At ${station.en}`} /></span>
          <span className="news-ts-year">{st.arrival} CE</span>
        </div>
        <div className="news-ts-row sub">
          <span className="news-ts-status">
            {inFlight
              ? <T zh={<>电波在途 · 还需 <b>{st.eta.toFixed(1)} 年</b></>} en={<>signal en route · <b>{st.eta.toFixed(1)} yr</b> remain</>} />
              : <T zh={<>已抵达 <b>{st.age.toFixed(1)} 年前</b></>} en={<>received <b>{st.age.toFixed(1)} yr ago</b></>} />}
          </span>
          <span className="news-ts-pov"><T zh={`视角：${pov}`} en={`POV: ${pov}`} /></span>
        </div>
      </div>

      {inFlight ? (
        <div className="news-deck news-redacted">
          <div className="news-redacted-text">
            <span style={{ filter: 'blur(4px)', userSelect: 'none' }}>{deck}</span>
          </div>
          <div className="news-redacted-note">
            <T
              zh={`本电波尚未抵达 ${station.zh}。在协约信号网中，没有信息能跑过光。`}
              en={`This signal has not yet reached ${station.en}. In the Accord signal network, no information runs faster than light.`} />
          </div>
        </div>
      ) : (
        <>
          <p className="news-deck">{deck}</p>
          {expanded && (
            <div className="news-body">
              {body.map((p, i) => <p key={i}>{p}</p>)}
            </div>
          )}
          <button className="news-read-more" onClick={onToggle}>
            {expanded ? <T zh="收起" en="Collapse" /> : <T zh="展开全文" en="Read more" />}
          </button>
        </>
      )}
    </article>
  );
}

function NewsInFlightStrip({ stories, station, now }) {
  const { lang } = useLang();
  if (!stories.length) return null;
  return (
    <section className="news-inflight-section">
      <h3 className="news-section-title">
        <span className="news-section-rule" />
        <T zh={`在途电波 · 尚未抵达 ${station.zh}`} en={`In-flight signals · not yet at ${station.en}`} />
        <span className="news-section-count">{stories.length}</span>
      </h3>
      <ul className="news-inflight-list">
        {stories.map(s => {
          const st = newsStatusAt(s, station, now);
          const f = NEWS_STATIONS.find(x => x.id === s.origin);
          const head = pickStoryField(s, 'head', lang);
          return (
            <li key={s.id} className="news-inflight-item">
              <span className="news-if-dot" style={{ background: f.accent }} />
              <span className="news-if-from"><T zh={`自 ${f.zh}`} en={`from ${f.en}`} /></span>
              <span className="news-if-head">{head.replace(/——.*/, '').replace(/—.*/, '')}</span>
              <span className="news-if-eta"><T zh={`${st.eta.toFixed(1)} 年后抵达`} en={`arrives in ${st.eta.toFixed(1)} yr`} /></span>
              <span className="news-if-bar">
                <span
                  className="news-if-fill"
                  style={{
                    width: `${100 * (1 - st.eta / Math.max(st.delay, 0.1))}%`,
                    background: f.accent,
                  }}
                />
              </span>
            </li>
          );
        })}
      </ul>
    </section>
  );
}

function NewsPropagationTable({ story, now }) {
  const { lang } = useLang();
  if (!story) return null;
  const f = NEWS_STATIONS.find(s => s.id === story.origin);
  const head = pickStoryField(story, 'head', lang);
  return (
    <section className="news-prop-section">
      <h3 className="news-section-title">
        <span className="news-section-rule" />
        <T zh="同一事件 · 五站抵达表" en="Same event · five-station arrival table" />
      </h3>
      <div className="news-prop-headline">
        <span className="news-prop-from" style={{ color: f.accent }}>
          ● <T zh={`发自 ${f.zh}`} en={`from ${f.en}`} />
        </span>
        <span className="news-prop-title">{head}</span>
        <span className="news-prop-emitted"><T zh={`${story.emittedCE} CE 发出`} en={`emitted ${story.emittedCE} CE`} /></span>
      </div>
      <table className="news-prop-table">
        <thead>
          <tr>
            <th><T zh="站台" en="Station" /></th>
            <th><T zh="距源" en="Distance" /></th>
            <th><T zh="抵达年" en="Arrival" /></th>
            <th><T zh={`状态（${now} CE）`} en={`Status (${now} CE)`} /></th>
            <th className="news-bar-col"><T zh="光速封缄" en="Light-speed seal" /></th>
          </tr>
        </thead>
        <tbody>
          {NEWS_STATIONS.map(s => {
            const ly = newsLightYears(story.origin, s.id);
            const arr = story.emittedCE + ly;
            const received = now >= arr;
            const isFrom = s.id === story.origin;
            return (
              <tr key={s.id} className={isFrom ? 'news-src-row' : ''}>
                <td>
                  <span className="news-row-dot" style={{ background: s.accent }} />
                  {lang === 'zh' ? s.zh : s.en} · {lang === 'zh' ? s.spZh : s.spEn}
                </td>
                <td className="news-num">{ly.toFixed(1)} ly</td>
                <td className="news-num">{arr} CE</td>
                <td>
                  {isFrom
                    ? <span className="news-src-tag"><T zh="本地播发" en="local" /></span>
                    : received
                      ? <span className="news-rcv-tag"><T zh={`已抵 ${(now - arr).toFixed(0)} 年`} en={`+${(now - arr).toFixed(0)} yr`} /></span>
                      : <span className="news-if-tag"><T zh={`在途 还需 ${(arr - now).toFixed(1)} 年`} en={`in-flight · ${(arr - now).toFixed(1)} yr`} /></span>}
                </td>
                <td className="news-bar-col">
                  <span className="news-prop-bar">
                    <span className="news-prop-bar-fill" style={{
                      width: isFrom ? '100%' : received
                        ? '100%'
                        : `${100 * (1 - (arr - now) / Math.max(ly, 0.1))}%`,
                      background: isFrom ? s.accent : f.accent,
                    }}/>
                  </span>
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
      <div className="news-prop-foot">
        <T
          zh="协约信号网中，单程光时 = 站间距离（光年）。无超光速，无心灵感应。"
          en="In the Accord signal network, one-way light-time = inter-station distance (light-years). No FTL, no telepathy." />
      </div>
    </section>
  );
}

/* ───────────────────────── Page root ───────────────────────── */
function PageNews({ nav }) {
  const { lang } = useLang();
  const [now, setNow] = React.useState(4000);
  const [stationId, setStationId] = React.useState('sol');
  const [selectedStoryId, setSelectedStoryId] = React.useState('ledger-crisis');
  const [sortMode, setSortMode] = React.useState('arrival');
  const [expanded, setExpanded] = React.useState({});

  const station = NEWS_STATIONS.find(s => s.id === stationId) || NEWS_STATIONS[0];

  const partitioned = React.useMemo(() => {
    const received = [];
    const inFlight = [];
    for (const s of NEWS_STORIES) {
      const st = newsStatusAt(s, station, now);
      (st.kind === 'received' ? received : inFlight).push(s);
    }
    const sortKey = (s) => {
      if (sortMode === 'arrival') return -newsArrivalCE(s, station.id);
      if (sortMode === 'emitted') return -s.emittedCE;
      if (sortMode === 'delay')   return -newsLightYears(s.origin, station.id);
      return 0;
    };
    received.sort((a, b) => sortKey(a) - sortKey(b));
    inFlight.sort((a, b) => newsArrivalCE(a, station.id) - newsArrivalCE(b, station.id));
    return { received, inFlight };
  }, [station.id, now, sortMode]);

  const selectedStory = NEWS_STORIES.find(s => s.id === selectedStoryId) || NEWS_STORIES[0];

  /* Per-station CSS variables, scoped to the wrapper. */
  const themeVars = {
    '--news-bg':    station.bg,
    '--news-fg':    station.fg,
    '--news-muted': station.muted,
    '--news-rule':  station.rule,
    '--news-accent':   station.accent,
    '--news-accent-2': station.accent2 || station.accent,
    '--news-text-on-accent': station.textOnAccent,
  };

  return (
    <div className="news-section" data-station={station.id} style={themeVars}>
      <div className="news-app">
        <NewsMasthead
          station={station}
          now={now}
          setStation={s => setStationId(s.id)}
          totalReceived={partitioned.received.length}
          totalInFlight={partitioned.inFlight.length}
        />

        <div className="news-control-bar">
          <div className="news-ctrl-group">
            <label className="news-ctrl-label"><T zh="协约纪年" en="Accord Era" /></label>
            <input
              type="range"
              min="1700" max="4500" step="1"
              value={now}
              onChange={e => setNow(parseInt(e.target.value, 10))}
            />
            <span className="news-ctrl-value">{now} CE</span>
          </div>
          <div className="news-ctrl-group">
            <label className="news-ctrl-label"><T zh="排序" en="Sort" /></label>
            <select value={sortMode} onChange={e => setSortMode(e.target.value)}>
              <option value="arrival">{lang==='zh' ? '按抵达时间' : 'by arrival'}</option>
              <option value="emitted">{lang==='zh' ? '按发出时间' : 'by emission'}</option>
              <option value="delay">{lang==='zh' ? '按光时延迟' : 'by light-delay'}</option>
            </select>
          </div>
          <div className="news-ctrl-hint">
            <T
              zh="拖动纪年滑杆，看同一事件如何先后抵达五站。"
              en="Drag the era slider to watch one event reach the five stations in turn." />
          </div>
        </div>

        <main className="news-main">
          <section className="news-feed">
            <h3 className="news-section-title">
              <span className="news-section-rule" />
              <T
                zh={`已抵 ${station.zh} · ${partitioned.received.length} 条`}
                en={`Received at ${station.en} · ${partitioned.received.length}`} />
            </h3>
            {partitioned.received.length === 0 ? (
              <div className="news-empty">
                <T
                  zh={`${now} CE 尚无任何信号抵达 ${station.zh}。把纪年拖向未来。`}
                  en={`No signal has reached ${station.en} by ${now} CE. Drag the era forward.`} />
              </div>
            ) : (
              <div className="news-cards">
                {partitioned.received.map(s => (
                  <NewsStoryCard
                    key={s.id}
                    story={s}
                    station={station}
                    now={now}
                    expanded={!!expanded[s.id]}
                    onToggle={() => setExpanded(e => ({ ...e, [s.id]: !e[s.id] }))}
                  />
                ))}
              </div>
            )}

            <NewsInFlightStrip stories={partitioned.inFlight} station={station} now={now} />
          </section>

          <aside className="news-aside">
            <div className="news-select-event">
              <label className="news-ctrl-label"><T zh="追踪事件" en="Track event" /></label>
              <select value={selectedStoryId} onChange={e => setSelectedStoryId(e.target.value)}>
                {NEWS_STORIES.map(s => {
                  const head = pickStoryField(s, 'head', lang);
                  return (
                    <option key={s.id} value={s.id}>
                      {s.emittedCE} · {head.length > 28 ? head.slice(0, 28) + '…' : head}
                    </option>
                  );
                })}
              </select>
            </div>
            <NewsPropagationTable story={selectedStory} now={now} />
          </aside>
        </main>

        <footer className="news-foot">
          <div>
            <b><T zh="光速延迟新闻网" en="Lightspeed-Delay News" /></b>{' '}
            <T
              zh={'是缓约宇宙的一份协约层档案接口。每一条公告都按发出地的「今日」撰写，再按光速分别穿过空间，迟早抵达每一站。'}
              en={'is an Accord-layer archival interface for the Slow Compact universe. Each bulletin is written for the “today” of its origin, then crosses space at light speed and reaches every station sooner or later.'} />
          </div>
          <div className="news-foot-sub">
            <T
              zh="0.1c 至 0.2c 的慢船在每条走廊上巡航；电波永远跑在它们前面，但仍要花一个 ly 一年。"
              en="Slow-ships cruise every corridor at 0.1c to 0.2c; radio always runs ahead of them, but still takes one year per light-year." />
          </div>
        </footer>
      </div>
    </div>
  );
}

Object.assign(window, { PageNews });
