/* ============================================================================
 * SGS — design tokens. THE single source of truth for colour and type.
 *
 * Loaded FIRST on every page, before any other stylesheet or page-local <style>.
 * styles/prism-ds.css and styles/article.css do not define their own palettes any
 * more — their historical variable names (--sgs-*, --bg-primary, --accent…) are
 * thin aliases over the tokens below, so the hundreds of existing var() call
 * sites keep working untouched and the theme flips from one place.
 *
 * Direction: light canvas, white cards, deep-green ink. The deep green is no
 * longer a full-screen background — it is the ink colour and the fill for
 * deliberately dark elements (e.g. the hub's two live cards).
 * ========================================================================== */

:root {
  /* ---- palette (the 9 brand tokens) --------------------------------------- */
  --canvas:        #f4f7f5;   /* page background */
  --surface:       #ffffff;   /* card face */
  --ink:           #153a2c;   /* headings; also the fill for dark elements */
  --body:          #33413c;   /* body copy */
  --muted:         #8a9691;   /* de-emphasised — see --muted-strong before using on text */
  --accent:        #2f8467;   /* links, buttons, LIVE */
  --accent-light:  #8cc8b4;   /* hover — ON DARK ELEMENTS ONLY (see contrast note) */
  --border:        #dbe4df;
  --soft:          #e9efeb;   /* light fill */

  /* ---- accessible steps of the same hues ---------------------------------
   * Measured against --canvas (#f4f7f5), the worst-case light background:
   *   --accent       4.22:1  -> fails AA for body-size text
   *   --muted        2.84:1  -> fails AA outright
   *   --accent-light 1.76:1  -> unusable as text on any light surface
   * These two are NOT new brand colours; they are darker steps of --accent and
   * --muted for the one job the originals can't do — small text on a light
   * background. Use the originals for fills, borders, large display type, and
   * anything sitting on --ink.
   *   --accent-strong 4.62:1 | --muted-strong 4.71:1  —— 均按 --soft(#e9efeb)算
 *   ⚠️ 基准是 --soft 不是 --canvas:--soft 才是本站最暗的浅底（相对亮度 0.8505 vs
 *   canvas 0.9235)。按 canvas 算会高估约 0.35,--accent-strong 曾因此以 4.45 混过
 *   合格线。test/contrast.test.mjs 会算这件事，改任何颜色前先跑它。
   * --accent-light stays the hover colour on DARK elements (6.59:1 on --ink);
   * on light surfaces, hover to --accent-strong instead.
   * -------------------------------------------------------------------------- */
  --accent-strong: #2a775d;   /* accent used as text/links on a light surface */
  --muted-strong:  #5f6c66;   /* captions and small print on a light surface */

  /* Derived surfaces. Kept as tokens so pages never hand-mix an alpha green.
   * Task 16(design-language-unification): these were themselves hand-expanded rgb triples —
   * the same bug the rest of Task 16 exists to fix, just at the definition site instead of a
   * call site. Rewritten as color-mix() over the base token so changing --accent/--ink/--on-ink
   * actually reaches them; the resolved colour is unchanged (verified: color-mix(in srgb,
   * var(--x) N%, transparent) with a fully-opaque var(--x) reproduces rgba(x.r,x.g,x.b,N/100)
   * exactly — confirmed live via getComputedStyle before/after, see task-16-report.md). */
  --accent-soft:   color-mix(in srgb, var(--accent) 8%, transparent);   /* tinted fill (selected, highlight) */
  --accent-line:   color-mix(in srgb, var(--accent) 28%, transparent);  /* tinted hairline */
  --ink-soft:      color-mix(in srgb, var(--ink) 6%, transparent);      /* neutral-green wash */
  --on-ink:        #ffffff;                    /* text on an --ink fill (12.55:1) */
  --on-ink-2:      color-mix(in srgb, var(--on-ink) 80%, transparent);  /* secondary text on an --ink fill (~9.4:1) */
  --on-accent:     #ffffff;                    /* text on an --accent fill */

  /* Dark card fill — one step lighter than --ink, for large dark panels (the hub's live cards)
   * where full --ink reads too heavy against the canvas. Deliberately NOT --ink itself: headings
   * and small dark elements keep the darker value. On this fill: white 10.07:1, --on-ink-2
   * 7.12:1, --accent-light 5.29:1 — all clear of AA with margin, and it stays 2.21:1 apart from
   * --accent so the two greens don't collapse into one another.
   * Ceiling for this slot is about #1e523f; by #215b46 --accent-light (the eyebrow, CTA and LIVE
   * badge) falls to 4.17:1 and breaks AA. */
  --ink-card:      #1c4a38;
  --on-ink-card-2: color-mix(in srgb, var(--on-ink) 82%, transparent);  /* secondary text on --ink-card */

  /* ---- 数据色 -------------------------------------------------------------
   * 这三个色来自住宅图表， 是全站唯一已经通过调色板校验的一层（最差相邻色差 20.8)。
   * 升格为标准数据色。被它们取代的 --chart-1..5 实测 FAILED: 两色彩度不足读起来发灰，
   * 且 #6b8f2e 与 #2f8467 常人可辨色差只有 9.7(下限 15)—— 正常视力也分不开。
   * ⛔ 不做色盲改善（Eddie 三次确认）, 配色以视觉协调与专业美观为首要。
   * ------------------------------------------------------------------------ */
  --data-1: #2a78d6;   /* = js/resi-market.js 的 C_MARKET(正常成交） */
  --data-2: #eda100;   /* = C_PREFORECLOSURE(业主违约自售） */
  --data-3: #c2521c;   /* = C_FORECLOSURESALE(银行转售） */
  /* ⛔ 不许为了"提高对比度"压暗其中任何一个： 它们是同一根堆叠柱的三段， 靠色相区分。
     实测把 #eda100 压暗成 #bd7400 后， 相邻对比度反而从 2.04 掉到 1.19, 同时 ΔE 从
     20.8 掉到 8.1 —— 两段糊在一起。冻结文件 js/resi-market.js 的注释也写了同一条纪律
     (试过用红色表示银行转售， 与橙红 ΔE 只有 7.1)。 */

  /* 有序数据用顺序色阶， 不用分类色 —— **人口年龄段**(Under 18 / 18-34 / 35-54 / 55-64 / 65+)
     是有序变量， 用五个任意色相反而把顺序藏起来了。这五档是 --accent 的明度阶。
     ⚠️ 2026-08-20 更正： 此处原写「建成年代」是错的 —— js/neighborhood.js 的 cardAge()
     读的是 d.brackets 与 d.median_age(人口年龄段）。「住房年代」是另一张卡， 单值无配色。
     ⚠️ 前三档比最初稿更深： validate_palette.js --ordinal --surface "#ffffff" 实测
     原 age-1(#d6e8e0)在白底只有 1.27:1(门槛 2:1)。只压暗 age-1 不够 —— 门槛还要求
     相邻档 OKLCH ΔL >= 0.06, 而原 age-2(#a8cfc0, L=0.822)比过关所需的 age-1(L<=0.78)
     还浅， 单档改不出合法级差， 于是级联压暗 age-1/2/3(同色相同彩度， 只降明度）;
     age-4/5 未动， 数值与原稿一致。校验器四项全绿， 最暗档 2.03:1。 */
  --age-1: #a8bab2;
  --age-2: #84aa9c;
  --age-3: #609b84;
  --age-4: #3f8a6c;
  --age-5: #1f5f47;

  /* 「不知道 / 样本不足」。原本这个含义在三处各写死一份。 */
  --no-data: #b9c4bf;

  /* Status. Darkened from the old dark-theme values so they read on white. */
  --up:            #217a58;
  --down:          #b4552f;
  --warn:          #8a6d1f;
  --danger:        #b03b3b;

  /* ---- typography ---------------------------------------------------------
   * Display/wordmark = Playfair Display (serif). Body + UI = Inter (sans).
   * IBM Plex Mono is retained as the third family: every Prism metric value,
   * KV row and tracked label depends on its tabular alignment.
   * -------------------------------------------------------------------------- */
  --font-display: 'Playfair Display', Georgia, 'Times New Roman',
                  'Songti SC', 'Noto Serif SC', 'Source Han Serif SC', serif;
  --font-body:    'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono:    'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Type scale — 7 named roles (6 original + --fs-hero). */
  --fs-display: 40px;
  --fs-h1:      28px;
  --fs-h2:      20px;
  --fs-body:    16px;
  --fs-small:   13px;
  --fs-caption: 11px;
  --fs-hero:    56px;   /* 唯一新增的一档。Task 4 之前仅首页 hero; --sgs-fs-hero
                           别名到这里之后 /prism/sfr 的估值大数字也落在这一档上。见 spec §3 */

  /* On a light background, 300-weight body copy reads washed out — the optical
     compensation runs the other way from a dark theme. Body baseline is 400. */
  --fw-body:    400;
  --fw-medium:  500;
  --fw-semi:    600;

  --lh-tight:   1.2;
  --lh-body:    1.65;
  --track-label: 1px;    /* letter-spacing for tracked mono labels */

  /* ---- 间距 (4px 基） -------------------------------------------------------
   * 实测全站 44 种不同 px 值 / 779 处， 其中 57.5% 已经落在 4 的倍数上。
   * 这十档就是 prism-ds.css 里已有的八档 + 48/64 —— 一档新的都不发明。
   * 要收的是 6/10/14/18px 那 194 处奇数档（Task 17, 单独一个 Task 且必须逐页实测）。
   * ------------------------------------------------------------------------ */
  --sp-1: 4px;   --sp-2: 8px;   --sp-3: 12px;  --sp-4: 16px;  --sp-5: 20px;
  --sp-6: 24px;  --sp-8: 32px;  --sp-10: 40px; --sp-12: 48px; --sp-16: 64px;

  /* ---- 版心 ---------------------------------------------------------------
   * 实测 24 种 max-width 写法 / 37 处。最刺眼的是： 入口 hub 内容区 1120px, 而两个站
   * 都是 1160px —— 从入口点进去， 每一条内容边界都跳 40px。这比 1px 的字号差异明显得多。
   * ------------------------------------------------------------------------ */
  --w-wide:   1160px;  /* 面板页内容区 */
  --w-narrow:  720px;  /* 表单 / 卡片组 */
  --w-read:    640px;  /* 导语与正文行长 */

  /* ---- 层级 ---------------------------------------------------------------
   * 实测 8 种 / 10 处（100 / 950 / 1000 / 1200 / 3000 / 9999 / 10000), 无标准，
   * 浮层谁盖谁是试出来的。
   * ------------------------------------------------------------------------ */
  --z-base:    1;
  --z-sticky:  100;
  --z-overlay: 900;    /* 图上浮层 */
  --z-nav:     1000;   /* 顶栏 */
  --z-modal:   2000;   /* 弹框 / 登录浮层 */

  /* ---- 动效 ---------------------------------------------------------------
   * 实测 13 种写法 / 67 处， 归一后 11 个档位。`.3s` 与 `0.3s` 是同一个值的两种写法（27 处）。
   * 就近归位后零可见变化： 150/180 -> fast, 200/220 -> 默认， 300 -> slow。
   * ≥400ms 的 11 处是内容动画（播放头、脉冲）, 不收进交互档位。
   * ------------------------------------------------------------------------ */
  --dur-fast: 150ms;
  --dur:      200ms;
  --dur-slow: 300ms;

  /* ---- shape + depth ------------------------------------------------------ */
  --radius:     6px;
  --radius-sm:  3px;
  --radius-pill: 999px;   /* 胶囊 / 药丸。正圆点仍用 50% */
  --gutter:     24px;
  /* Shadows are soft and neutral-green on a light canvas; the old heavy black
     drop shadows read as dirt against white. Task 16: colour component rewritten as
     color-mix() over --ink for the same reason as --accent-soft/--ink-soft above —
     resolved colour unchanged, verified live. */
  --shadow-sm:  0 1px 2px color-mix(in srgb, var(--ink) 5%, transparent);
  --shadow:     0 4px 16px -4px color-mix(in srgb, var(--ink) 10%, transparent);
  --shadow-lg:  0 18px 44px -12px color-mix(in srgb, var(--ink) 20%, transparent);
  /* 焦点环与脉冲 —— 这些手写阴影是功能性的， 收成 token 而不是消灭掉。
     ⚠️ styles/mf-market.css 里那处 rgba(0,0,0,.08) 是纯黑， 与本文件自己写的
     「黑色重投影在白底上读起来像脏污」直接矛盾。Task 16 换成 --ink(上一版注释误写
     "Task 17" —— 间距收敛的 Task 17 与色彩无关， 这处本就该是做 rgba→color-mix 的这个
     Task 自己去改， 已在这次一并修正）。 */
  --ring:        0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
  --ring-strong: 0 0 0 4px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* ============================================================================
 * Task 19(design-language-unification): breakpoint standard, established but not enforced
 * on existing code.
 *
 * The four steps — this is documentation, not tokens. @media queries cannot read var(), so
 * these can't be CSS custom properties; the values below are the standard by convention only.
 *
 *   420px   smallest phones
 *   640px   phones / narrow tablets
 *   768px   tablets
 *   1024px  small laptops / large tablets
 *
 * Measured inventory (2026-08-20): 26 max-width @media declarations across the site (one more
 * rule, prism-sfr.html, is prefers-reduced-motion — not a width breakpoint, untouched). Of the
 * 26, 11 distinct px values appear; three of them (420/640/768) already match the standard
 * above. The other 8 values — 480/520/560/620/720/760/860/900 — do not, and are grandfathered
 * as pre-existing rather than collapsed onto the four steps. test/design-roles.test.mjs
 * enumerates every one of those 11 occurrences by file so a future collapse has the list ready.
 *
 * Why grandfathered instead of collapsed: the only real way to verify a breakpoint change is to
 * look at the rendered result on an actual narrow viewport, and this project has never once
 * measured anything below 1440px — resize_window caps around 606x706, and an earlier attempt to
 * measure at 390px silently returned a 1585px viewport, producing numbers that were quietly
 * worthless. Collapsing existing breakpoints with no way to see the result would be changing
 * responsive behaviour blind. Deferred to the usability line of work, which has to buy real
 * device measurement anyway for touch targets and focus states. New code is held to the four
 * steps starting now; the cleanup of the 11 pre-existing off-standard occurrences is not in this
 * Task's scope.
 * ========================================================================== */

/* Task 13(design-language-unification): .footnote/.disclosure retired. They were one class
   doing two jobs — "provenance / small print" bundled source lines and model caveats into the
   same look, which is exactly the ambiguity Task 13 exists to remove. Every real use has been
   re-classified by what question it actually answers (source → styles/roles.css .a-source,
   caveat → .a-flag, sample/exclusions → .a-scope) and .disclosure's own heavier legal-block look
   (padding/background/border-left) moved onto .a-flag's/.a-source's sibling role, .a-disclosure,
   in styles/roles.css — see that file's Task 13 comment. Nothing in this repo references either
   name anymore (test/design-roles.test.mjs's "16 个旧标注类名已全部消失" pins that down); a
   dead rule with no caller left in the file that no longer serves it is the same kind of drift
   the roles layer exists to prevent, so it's deleted rather than kept "just in case". */

/* Base page defaults. Individual pages still set their own layout; this only
   establishes the canvas, the body font and the reading colour so a page that
   forgets to is still on-theme rather than white-on-white. */
html { background: var(--canvas); }
body {
  background: var(--canvas);
  color: var(--body);
  font-family: var(--font-body);
  font-weight: var(--fw-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
