/* The plan finder's visual language (app/style.css), reused so an agent moving
   between the two tools does not feel they have changed product. Same custom
   properties, same names for .topbar / .pill / .btn / .badge / .seg / .drawer,
   same CJK font stacks, same dark-mode block.

   Two colours are new: --danger-* for erasure, and --pdpo-* for the legal
   panels. Both exist so those blocks cannot be mistaken for ordinary UI. */

/* --- Surface ladder ----------------------------------------------------
   Three layers, three steps of depth. --bg is the page, --panel is a card
   sitting on it, --sunken is a well cut into a card (input rows, the
   "pending" strips). Before this, --field was doing double duty as both
   "a card" and "a well", so a white input on a white panel measured
   nothing at all. Same value in light mode, deliberately: the separation
   below comes from a ring, not from a second grey.

   ⚠️ --field is NOT renamed. 95 declarations reference it and it is also
   the dark-mode override point; a new *token* is the cheap move, a new
   *name* is a 95-site edit with no reader-visible gain. --sunken is an
   alias that follows --field, so dark mode stays correct for free. */
:root{
  --bg:#f4f6f9; --panel:#ffffff; --ink:#16202c; --muted:#556070; --line:#e3e8ef;
  --brand:#0e7490; --brand-ink:#0e7490; --brand-soft:#e0f2f7;
  /* Hover is its own step. It used to be --brand-soft, which is the same
     colour the household chips already use for their fill — so hovering a
     row looked like the row had acquired a chip. Neutral keeps hover
     meaning "the pointer is here" and reserves teal for "this is chosen".
     Measured against --ink this is a passive tint, never a text surface. */
  --row-hover:#eef2f7; --sunken:var(--field); --ring:rgba(16,32,44,.08);
  /* --ok stays as it is: it is also a fill and a border, where 3.09:1 against
     --ok-soft is fine. --ok-ink is the TEXT-only version, because green text on
     its own soft green measured 3.09:1 — the one pair in the palette that was
     not merely marginal but well under the 4.5:1 floor. Named separately so
     that fixing the badge cannot repaint a chart or a border. */
  --ok:#0f9d58; --ok-ink:#0a6b3d; --ok-soft:#e6f4ea;
  --warn:#8a5a12; --warn-soft:#fdf3e0;
  --bad:#c0392b; --bad-soft:#fbe9e7;
  /* 每月總支出個數字(Rex 指定橙色)。--warn 係啡色,唔係橙。 */
  --orange:#ea580c;
  --shadow:0 1px 3px rgba(16,32,44,.08),0 6px 24px rgba(16,32,44,.06);
  --field:#ffffff;
}
@media (prefers-color-scheme:dark){
  :root{ --bg:#0f151c; --panel:#18212c; --ink:#e6edf3; --muted:#93a1b1; --line:#26313d;
    --brand:#22b8cf; --brand-ink:#7fd8e6; --brand-soft:#123039;
    /* The pairs this palette change touched already clear 4.5:1 here (measured:
       --muted 6.97:1 on --bg and 6.17:1 on --panel, --ok 5.70:1 on --ok-soft),
       so --ok-ink is the same green. It is declared anyway rather than left to
       inherit, because a shade tuned for a white background would be unreadable
       on #12301d. Not every dark pair passes: white on --brand is 2.38:1
       (primary buttons, the chosen language, the current step; that step's
       number 1.93:1) and white on --bad 3.27:1 (.solid-danger). Those are older
       than this block and are not fixed by it. */
    --ok:#3fb96b; --ok-ink:#3fb96b; --ok-soft:#12301d;
    --warn:#d6a24a; --warn-soft:#33280f;
    --bad:#e5675a; --bad-soft:#39201c;
    --orange:#fb923c;
    --shadow:0 1px 3px rgba(0,0,0,.4),0 8px 30px rgba(0,0,0,.35);
    --field:#0f151c;
    /* The ladder inverts: --panel (#18212c) is now the LIGHTER surface, so a
       well has to go darker than the page, not lighter. Setting it to the
       page colour is what --field already did. */
    --sunken:#0f151c;
    /* Dark mode does not need a tint to darken — it needs one to lift. */
    --row-hover:#1f2a37; --ring:rgba(255,255,255,.07);}
}
*{box-sizing:border-box}
html,body{margin:0}
/* The root size is the reader's, not ours. Every size below is in rem, so
   a reader who raises their browser's default font size gets a larger page
   instead of the same page with bigger scrollbars. 100% is the UA default
   (16px) stated explicitly — writing 16px here would defeat the whole thing. */
html{font-size:100%}
[hidden]{display:none !important}
body{background:var(--bg);color:var(--ink);
  font:1rem/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,
    "PingFang HK","PingFang TC","Hiragino Sans CNS","Microsoft JhengHei",
    "PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK HK","Noto Sans CJK SC",sans-serif;
  -webkit-font-smoothing:antialiased}
html[lang="zh-Hans-CN"] body{
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,
    "PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC",sans-serif}
html[lang^="zh"] td,html[lang^="zh"] th{letter-spacing:.01em}

/* ------------------------------------------------------------------ chrome */
.topbar{display:flex;justify-content:space-between;align-items:center;gap:16px;
  padding:14px 22px;background:var(--panel);border-bottom:1px solid var(--line);
  position:sticky;top:0;z-index:20;flex-wrap:wrap}
.brand{display:flex;align-items:center;gap:12px}
/* ⚠️ 冇咗 ✚ 嗰個實心方塊（見 index.html 嘅 comment）。而家係一條 3px 品牌色
   直線：一個純色線條冇任何按鈕特徵（冇邊框、冇圓角、冇陰影、冇 hover），
   所以冇人會試撳佢。原本嗰個 ✚ 係 38×38、圓角 10px、填品牌色 —— 同一個
   active 嘅導覽掣（例如語言嘅「繁」）幾乎一模一樣，所以睇落似掣。
   修法唔係加字，係拎走「方塊 + 填色」呢兩個特徵。 */
.rule{width:3px;height:34px;flex:none;background:var(--brand);border-radius:2px}
.brand h1{margin:0;font-size:1.1875rem;letter-spacing:-.2px;line-height:1.2}
/* 「當前頁」標籤。⚠️ 呢個唔止係裝飾：10 條 route 只有 4 條有導覽掣，
   所以喺「保單體檢」／「匯入檔案」／「保留期」呢啲頁面，全個 topbar
   冇任何一粒掣會亮起 —— 呢行字係唯一話你知自己喺邊嘅嘢。 */
.over{display:block;color:var(--muted);font-size:0.6875rem;font-weight:800;
  letter-spacing:.07em;text-transform:uppercase;line-height:1.3}
.topright{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}
.who{color:var(--muted);font-size:0.78125rem;max-width:22ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* ⚠️ 登入頁（#gate）仍然有 .lang 三粒掣 —— 嗰度**一定要**保留 segmented：
   未登入嘅人仲未有偏好 menu（topbar 未出），而佢要改嘅係一份法律文件
   （PDPO 收集聲明）嘅語言。三粒掣一眼睇齊三個選擇，喺呢個情境比 dropdown 好。
   ⚠️ 唔可以因為 topbar 改用咗 .prefs 就刪咗呢條 rule —— 刪咗個登入頁嘅
      語言掣就冇咗 44px 地板（量過：原本 30px），而佢改嘅係法律文件語言。
      （2026-09-27 差啲中：touch_targets.test.js 就係捉呢件事。） */
.lang{display:inline-flex;background:var(--bg);border:1px solid var(--line);border-radius:9px;overflow:hidden;flex:none}
.lang button{font:inherit;font-size:0.8125rem;font-weight:700;border:0;background:transparent;color:var(--muted);
  padding:7px 11px;cursor:pointer;line-height:1.2;min-height:44px;min-width:44px}
.lang button.on{background:var(--brand);color:#fff}
.lang button:hover:not(.on){color:var(--brand-ink)}
/* 分組用嘅細分隔線。⚠️ aria-hidden（見 index.html）—— 佢係純視覺。 */
.sep{width:1px;height:24px;background:var(--line);flex:none;margin:0 1px}
/* 「計劃搜尋器」跳去另一個站（app.js 嘅 openFinderPlain 開新頁）。
   加一個 ↗ 令佢同 app 內嘅 view 掣分得開 —— 之前兩者一模一樣，
   冇人知道撳落去會離開呢個 app。 */
.ext::after{content:"\00a0↗";font-weight:700}
/* Language, text size and account actions each have one named menu button. */
.prefs{position:relative;display:inline-flex;flex:none}
.prefs>button{font:inherit;font-size:0.8125rem;font-weight:700;border:1px solid var(--line);
  background:var(--bg);color:var(--ink);cursor:pointer;line-height:1.2;
  min-height:44px;min-width:44px;padding:7px 10px;border-radius:9px;
  display:inline-flex;align-items:center;gap:6px}
.prefs>button:hover{border-color:var(--brand)}
.prefs>button .caret{font-size:0.6875rem;color:var(--muted)}
.prefs>button[aria-expanded="true"]{border-color:var(--brand);background:var(--brand-soft)}
.prefs-menu{position:absolute;top:100%;right:0;margin-top:6px;background:var(--panel);
  border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);
  padding:5px;min-width:11rem;z-index:30}
.prefs-h{display:block;padding:6px 10px 2px;color:var(--muted);font-size:0.6875rem;
  font-weight:800;letter-spacing:.06em;text-transform:uppercase}
.prefs-menu .prefs-item{display:block;width:100%;text-align:left;font:inherit;
  font-size:0.875rem;font-weight:600;border:0;background:transparent;color:var(--ink);
  padding:9px 10px;border-radius:7px;cursor:pointer;min-height:44px}
.prefs-menu .prefs-item:hover{background:var(--bg)}
.prefs-menu .prefs-item:focus-visible{outline:2px solid var(--brand);outline-offset:-2px}
.prefs-menu .prefs-item[aria-checked="true"]{background:var(--brand-soft);color:var(--brand-ink)}
.prefs-menu .prefs-item[aria-checked="true"]::after{content:" ✓";font-weight:700}
.pill{background:var(--brand-soft);color:var(--brand-ink);padding:5px 11px;border-radius:999px;
  font-size:0.78125rem;font-weight:600;white-space:nowrap}
.pill.due{background:var(--warn-soft);color:var(--warn)}
/* A phone, in English. With 44px language buttons the second header row —
   "Signed in as <name>", EN/繁/简, Sign out — is 361px wide, and a 390px screen
   leaves 346px inside 22px gutters, so Sign out fell onto a row of its own and
   the sticky header grew from 175.8px to 244.8px (measured in ui-lab). The
   language buttons cannot give the width back: their text is narrower than
   44px, so min-width already sets them. The chrome can: the 14px gutter <main>
   already uses at this width, 6px between header controls, and 8px of side
   padding on the header buttons. 8px leaves 設定 and 登出 at exactly 44px, so
   min-width holds the floor instead of trusting the font to. Measured to hold
   at 390 and 375; a longer name still wraps ("Rex Lau" stays on the row at
   both, "King Chung" only at 390), as longer names did before the 44px floor —
   and so does an account with no display name, whose e-mail fills the label
   (390 and 375, EN and 繁, measured). In 繁/简 the "目前登入" label now sits at
   the end of the nav-button row rather than beside the language switch. */
@media (max-width:760px){
  /* Added buttons can move any menu left: anchor all three to the topbar. */
  .prefs{position:static}
  .prefs .prefs-menu{right:14px;min-width:min(11rem,calc(100vw - 28px));max-width:calc(100vw - 28px)}
  .topbar{padding-left:14px;padding-right:14px}
  .topright{gap:6px}
  /* ⚠️ 手機上面個偏好掣同帳戶掣要截短。
     呢兩粒掣嘅面係**文字**（「繁體中文」／登入者個名），而個名可以好長
     （或者係一個 email）—— 一個長名會撐爆 .topright，令個 row 多開一兩行，
     而 topbar 係 flex-wrap 嘅，所以佢會寧願 wrap 都唔會溢出。
     所以喺窄螢幕要俾佢一個上限 + 截字（ellipsis）。
     ⚠️ 用 max-width 而唔係喺 JS 度切字：切嘅位要交返俾瀏覽器（字體、
        語言唔同，切嘅位唔同）。 */
  .prefs>button{max-width:9.5rem}
  /* ⚠️ 掣面嘅文字要收喺一個 span 度才截得到字：一個 flex container 上面
     text-overflow 係冇效嘅（flex item 唔係 inline box）。所以 app.js 會
     砌 <span class="prefs-name">繁體中文</span>。 */
  .prefs>button .prefs-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
  .topright .btn{padding-left:8px;padding-right:8px;min-width:44px}
}

/* ⚠️ 用 rem 唔用 px（King 2026-09-28）。
 * 原本寫死 1080px，但格入面嘅字係 rem，即係跟右上角「字級」掣放大。
 * 「大」(150%)／「超大」(200%) 之下，一行客戶需要多過 1080px，於是客戶列表
 * 個框出橫向捲軸、個框本身卻仍然企喺 1080px 唔郁 —— 睇落就係
 * 「啲字大咗，但個框冇跟」。
 * 1080px ÷ 16px = 67.5rem，所以正常字級之下**個個 px 都一樣**，唔會改變
 * 現有版面；而撳「大／超大」嗰陣個上限跟住 root 一齊放大，個框就自然跟到。
 * ⚠️ 唔可以只改客戶列表嗰個框：1080px 係 `main` 嘅上限，子元素走唔出去，
 * 所以一定要喺呢一層改。其餘版面本來就係同一個上限，跟埋一齊放大係啱嘅
 * （字大咗而框唔跟，喺其他表一樣會出橫向捲軸）。 */
/* ⚠️ rem, not px — the text-size control scales the root, so a px cap would
   hold the frame still while the type inside it grew (PR #121).
   Width: 69rem = 1104px, 24px wider than the 67.5rem this replaces.
   ⚠️ This is not cosmetic. Measured at 1440 with a 1080px frame, the client
   table needs 1053px inside a 1034px card: it overflowed by 19px and the
   trailing 保單回顧 button was clipped. That overflow predates this change
   (verified against the unmodified stylesheet — same 19px), so the old frame
   was already too narrow for its own table. 1104px closes it to 0px.
   ⚠️ 820px still scrolls horizontally (~279px). That is by design, not a
   regression: nine columns of CJK text cannot fit 774px, and .table-wrap
   carries overflow:auto for exactly that case. */
main{max-width:69rem;margin:0 auto;padding:18px 22px 80px}

/* ------------------------------------------------------------------ panels */
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;
  padding:16px 18px;box-shadow:var(--shadow);margin-bottom:16px}
.card h2{margin:0 0 10px;font-size:0.75rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
.card h3{margin:0 0 6px;font-size:0.9375rem}
.finance{scroll-margin-top:190px}
.finance h2{font-size:1.125rem;text-transform:none;letter-spacing:0;color:var(--ink)}
.finance-inputs{margin-top:16px}
.finance-values{margin-top:20px}
/* The two rate lines read as one group with the manual rate above them: they
 * are alternatives, and the rule between them is what separates the two. */
.finance-rate-lines{margin-top:20px;padding-top:16px;border-top:1px solid var(--line)}
.fin-rate-lines-out{font-size:0.875rem;color:var(--muted);margin:10px 0 0;
  font-variant-numeric:tabular-nums}
.finance-value-list{max-height:360px;overflow:auto;margin:10px 0}
.fin-value-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,2fr) auto;
  gap:12px;align-items:end;padding:8px 0}
.fin-value-row label{min-width:0}
.fin-value-row .btn{min-width:44px;min-height:44px}
.finance-toggle{margin-top:14px}
.finance-terms-scroll{max-height:420px;margin:8px 0 14px}
.finance-terms{min-width:0;table-layout:fixed}
.finance-terms th,.finance-terms td{padding:8px;white-space:normal}
.finance-terms th:first-child,.finance-terms td:first-child{width:58px}
.finance-terms td input{width:100%;min-width:0}
.finance-terms tbody tr{cursor:default}
.finance-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:18px 0}
.finance-metric{background:var(--brand-soft);border:1px solid var(--line);border-radius:10px;
  padding:12px;display:flex;flex-direction:column;gap:3px;min-width:0}
.finance-metric span{color:var(--muted);font-size:0.75rem}
.finance-metric strong{font-size:1.125rem;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
.finance-table tbody tr{cursor:default}
.finance-table td:first-child{font-weight:600}
.finance-table .finance-loss{color:var(--bad);font-weight:700}
.finance-chart{margin:22px 0;padding:16px 0;border-top:1px solid var(--line)}
.finance-chart-scroll{max-width:100%;overflow-x:auto;overscroll-behavior-x:contain}
.finance-chart-scroll:focus-visible{outline:2px solid var(--brand-ink);outline-offset:2px}
.finance-chart-scroll svg{display:block;margin:0 auto;max-width:none}
.finance-chart-legend{display:flex;flex-wrap:wrap;gap:10px 22px;margin:14px 0;font-size:0.8125rem}
.finance-chart-legend span{display:inline-flex;align-items:center;gap:7px}
.finance-chart-legend span::before{content:"";width:12px;height:12px;border-radius:2px}
.fin-legend-principal::before{background:var(--brand)}
.fin-legend-profit::before{background:var(--ok-ink)}
.fin-legend-loss::before{background:var(--bad)}
/* Bars get their top corners rounded and a soft shadow beneath, so a column
   of value reads as a solid object rather than a block of flat ink.
   ⚠️ rx/ry is 3px, not the 6px a "modern" chart usually reaches for: the
   bars are only 34px wide, and above ~4px the corner starts to read as a
   lozenge instead of a bar. ⚠️ Rounding applies to the top of the bar only
   in effect — SVG cannot round one end of a <rect>, and the bottom sits on
   the zero line where the square corner is what anchors it. The apparent
   top-only treatment is why this is safe to do with a single rect.
   ⚠️ The principal series is the same value repeated for every year, so it
   is the *reference*, not a result — it is held one step back in weight so
   the profit bar stays the thing the eye lands on. */
.fin-chart-principal{fill:var(--brand);rx:3;ry:3}
.fin-chart-profit{fill:var(--ok-ink);rx:3;ry:3}
.fin-chart-loss{fill:var(--bad);rx:3;ry:3}
/* Hovering a bar names it: the gridline behind it is the only other thing
   in the plot area, so a subtle lift is enough and does not need a tooltip
   layer. cursor:default because these are read-outs, not controls. */
.fin-chart-principal,.fin-chart-profit,.fin-chart-loss{cursor:default;transition:opacity .12s}
.fin-chart-principal:hover,.fin-chart-profit:hover,.fin-chart-loss:hover{opacity:.82}
.fin-chart-grid{stroke:var(--line);stroke-width:1}
.fin-chart-zero{stroke:var(--muted);stroke-width:1.5}
.fin-chart-axis{fill:var(--muted);font-size:0.75rem}
.fin-chart-value{fill:var(--ink);font-size:0.75rem;font-variant-numeric:tabular-nums}
@media (max-width:560px){
  .finance-metrics{grid-template-columns:1fr}
  .fin-value-row{grid-template-columns:minmax(0,1fr) minmax(0,2fr);gap:8px}
  .fin-value-row .btn{grid-column:2;justify-self:end}
}
.row{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;margin-bottom:10px}
.row:last-child{margin-bottom:0}
.spacer{flex:1}
.note{color:var(--muted);font-size:0.78125rem;margin:6px 0 0;line-height:1.55}

/* ------------------------------------------------------------------ inputs */
/* ⚠️ 13px（0.8125rem）。呢個係一個要讀嘅字，唔係一個裝飾 —— 客戶清單頂嗰行
   篩選標籤（搜尋／關係類別／群組／未有保障／排序／次序）就係佢。一個代理要
   望住呢個字才知道自己揀緊咩，所以佢同正文同一個尺。 */
label{font-size:1rem;color:var(--muted);display:flex;flex-direction:column;gap:3px;font-weight:600}
/* ⚠️ type=month 要明寫。一個唔喺呢張單入面嘅 input 唔會執到呢度嘅 padding 同
   邊框 —— 量過:病史嗰兩個年月格得 25px 高,隔籬啲文字格 38px,兩者並排就係
   一行高低唔一嘅格仔。type=url(保單第四步嘅產品連結)一樣:漏咗嗰陣佢係一個
   21px 高、13.33px 字嘅瀏覽器預設格,而 iOS 見到 16px 以下就會 zoom。
   touch_targets.test.js 由 app 嘅碼讀返佢開嘅每個 type 出嚟對住呢張單。 */
input[type=text],input[type=password],input[type=email],input[type=tel],input[type=date],
input[type=month],input[type=search],input[type=number],input[type=url],select,textarea{
  font:inherit;color:var(--ink);background:var(--field);border:1px solid var(--line);
  border-radius:9px;padding:8px 11px;min-width:0;width:100%;
  /* ⚠️ 16px, not the 13px it looks like it should be. iOS Safari zooms the
     whole page in when it focuses a control whose font-size is under 16px, and
     it does not zoom back out — the agent lands on a half-visible form and has
     to pinch. Measured on the 客戶表格: 42 controls were under 16px, so this
     fired on essentially every field. The value is a floor, not a look: the
     server-rendered sizes below stay whatever they are, and 13px text inside
     keeps its own font-size. */
  font-size:1rem;min-height:44px}
/* Everything that is not a free-text field can go back to the smaller sizes the
   layout was drawn with — iOS only zooms for the controls it raises a keyboard
   for, and a checkbox or a colour input never gets one. Kept as a separate rule
   so the reason above is not diluted. */
input[type=checkbox],input[type=radio],input[type=color],input[type=file],input[type=range]{
  font-size:inherit}
/* ⚠️ iPad / iPhone 嘅日期格。iOS Safari 會畀 type=date / type=month 一個佢自己嘅
   固有闊度,而且唔理 width:100% —— 所以喺一格 grid 入面佢會爆出去壓住隔籬嗰欄
   (出生日期壓住年齡),喺最近一次會面日期嗰度會鑽咗入「更新為今日」個掣底。
   喺 iPad 上面睇到嘅,2026-09-18 測試日朝早。
   appearance:none 攞走佢嘅原生外殼,佢先至肯跟 width;撳落去仍然係原生嘅日期
   揀選器。min-height 係因為冇咗外殼之後,一個空嘅日期格喺 iOS 會縮到得一條線。
   佢係 44px 唔係 38px:呢條 rule 同上面共用嗰條欄位 rule 一樣重、又喺佢後面,
   所以呢度寫幾多就係幾多 —— 寫 38 嗰陣,日期格係成張表唯一一格低過 44px 嘅。
   iOS 仲會將日期置中,而其他格全部靠左 —— 所以 text-align 拉返佢埋一邊。 */
input[type=date],input[type=month]{-webkit-appearance:none;appearance:none;
  display:block;min-width:0;max-width:100%;min-height:44px}
input[type=date]::-webkit-date-and-time-value,
input[type=month]::-webkit-date-and-time-value{text-align:left;min-height:1.2em}
input:focus,select:focus,button:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
input[type=checkbox]{width:16px;height:16px;cursor:pointer}
/* The box stays 16px — it is a box, and a 44px box would be absurd. What has
   to be 44px is the thing that gets tapped, and every checkbox on these forms
   is wrapped in its own <label class="chk">, so the label is the target. Padding
   rather than min-height so the row grows symmetrically and the tick stays
   optically centred on its text. */
.chk{flex-direction:row;align-items:center;gap:8px;color:var(--ink);font-weight:500;cursor:pointer;
  min-height:44px;padding:6px 0}
.chk input{flex:none}

/* the four form groups */
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px 14px}
/* WIDTH IS grid-column-END, and that is deliberate. `grid-column: span 4` is
   shorthand for START:span 4 / END:auto — the span rides on the start line. So
   anything that then sets grid-column-start silently throws the width away and
   leaves a one-column field. Written as the end longhand, a start can be set
   beside it and the two compose. */
.f{grid-column-end:span 4}
/* 一格 grid 預設 min-width:auto,即係佢唔肯窄過入面嘅嘢 —— 一個比格仔闊嘅輸入
   格會撐爆條軌,或者直接瀉出去壓住隔籬。min-width:0 令格仔跟條軌,入面嘅嘢跟格仔。 */
.grid > .f{min-width:0}
.f.xs{grid-column-end:span 2}
.f.md{grid-column-end:span 4}
.f.lg{grid-column-end:span 8}
.f.full{grid-column-end:span 12}
/* Start a fresh row. The grid packs whatever fits, so a field that must sit
   BELOW the one before it — rather than beside it when a neighbour happens to
   be hidden — has to say so. 父母退休情況 is the case: 「其他」的說明 collapses
   out of the grid on five of the six 感情/婚姻狀況 answers. Composes with every
   width above, at both breakpoints, because it touches only the start line. */
.f.rowstart{grid-column-start:1}
@media (max-width:760px){
  .f,.f.xs,.f.md,.f.lg{grid-column-end:span 6}
  .f.full{grid-column-end:span 12}
  main{padding:14px 14px 80px}
}

.req{color:var(--bad);font-weight:700}
.hint{color:var(--muted);font-size:0.75rem;font-weight:500;margin-top:3px;line-height:1.45}
/* An error is attached to its field and is not a colour alone: red on grey is
   invisible to a fair number of people, so the message carries the meaning. */
.f .msg{color:var(--bad);font-size:0.75rem;font-weight:600;margin-top:4px;line-height:1.45}
.f.bad input,.f.bad select,.f.bad textarea{border-color:var(--bad);background:var(--bad-soft)}
/* The star marks what the industry form marks. It is NOT .req: exactly one
   field on the policy form blocks a save, and colouring ten of them the same
   red as a real requirement would be the form lying about its own rules. */
.star{color:var(--brand-ink);font-weight:700}
/* Several checkboxes that can all be true — 附加項目. */
/* A rider's cost, beside its tick. Narrow on purpose: it is one figure about
   one benefit, and a full-width box would read as a figure about the policy. */
.f.rider-cost{max-width:190px;margin:0 0 0 4px}
.f.rider-cost>span:first-child{font-size:0.75rem}
.checks{display:flex;flex-wrap:wrap;gap:6px 18px;padding:6px 0 2px}
.checks .chk{font-size:0.8125rem}
textarea{resize:vertical;min-height:74px;line-height:1.55}

/* the derived read-outs — 年齡, 生肖, 星座: outputs, styled so they cannot be
   mistaken for boxes. Nothing here can be typed into and nothing is posted. */
.derived{display:flex;flex-direction:column;gap:3px}
.derived > span:first-child{font-size:0.8125rem;font-weight:600;color:var(--muted)}
.derived .val{font-size:1.375rem;font-weight:700;font-variant-numeric:tabular-nums;
  color:var(--brand-ink);padding:4px 0 0;line-height:1.2}
.derived .val.none{font-size:0.8125rem;font-weight:500;color:var(--muted);font-style:italic;padding-top:12px}
/* 可動用收入 係負數 = 客戶每月使多過佢賺。一個數字,唔係一個警告框 —— 但係要睇得出。 */
.derived .val.neg{color:var(--bad)}
/* 每月總支出個數字:橙色(Rex,測試日)。淨係個數字 —— 未填嗰陣嘅「未填」
   照舊係灰色斜體,唔係一句橙色嘅「未填」。 */
#outgoingsValue.val:not(.none){color:var(--orange)}

/* ------------------------------------------------------------ phone numbers */
/* One list for every number a client has, each with its own type and country
   code. A div rather than a label: a click anywhere in a label is forwarded to
   its first control, which would drag focus out of whichever row was clicked. */
.f.phones{display:flex;flex-direction:column;gap:6px}
.phones-label{font-size:0.8125rem;font-weight:600;color:var(--muted)}
/* At 200% text, even an 820px iPad cannot fit four controls side by side.
   Start stacked and expand only when this list has room at the chosen size;
   browsers without container queries retain the usable stacked layout. */
.phone-rows{display:flex;flex-direction:column;gap:8px;container:phone-rows / inline-size}
.phone-row{display:grid;grid-template-columns:5.5rem minmax(0,1fr) auto;
  grid-template-areas:"t t d" "c n n";gap:8px;align-items:center}
.phone-row .phone-label{grid-area:t}
.phone-row .phone-cc{grid-area:c}
.phone-row .phone-num{grid-area:n}
.phone-row .phone-del{grid-area:d}
@container phone-rows (min-width:30rem){
  .phone-row{grid-template-columns:minmax(8rem,1.1fr) 5.5rem minmax(9rem,2fr) auto;grid-template-areas:none}
  .phone-row .phone-label,.phone-row .phone-cc,.phone-row .phone-num,.phone-row .phone-del{grid-area:auto}
}
.phone-row .phone-cc{text-align:center}
.phone-del{flex:none;padding:8px 11px;line-height:1;color:var(--muted);min-width:44px}
.phone-del:hover:not(:disabled){border-color:var(--bad);color:var(--bad)}
.phone-add{align-self:flex-start;margin-top:2px}
@media (max-width:760px){
  /* The remove button keeps its own column, so a row never has to be scrolled
     sideways to be undone. */
  .phone-row{grid-template-columns:5.5rem minmax(0,1fr) auto;grid-template-areas:"t t d" "c n n"}
  .phone-row .phone-label{grid-area:t}
  .phone-row .phone-cc{grid-area:c}
  .phone-row .phone-num{grid-area:n}
  .phone-row .phone-del{grid-area:d}
}

/* ----------------------------------------------------------------- buttons */
/* min-height:44px on every .btn and every select. This is the one change in
   this commit that is visible at a glance: the 44px floor (WCAG 2.5.5 / Apple
   HIG) is taller than the 38px these were drawn at. It was 44px or keep the
   density, and density that cannot be hit is not density. The buttons that do
   not sit in a row — the ✕ on a phone number, the language switch — are the
   ones that were 27px and 30px, and a miss there costs a save. */
.btn{font:inherit;font-weight:600;font-size:0.8125rem;border:1px solid var(--line);background:var(--panel);
  color:var(--ink);border-radius:9px;padding:8px 14px;cursor:pointer;white-space:nowrap;min-height:44px}
.btn:hover:not(:disabled){border-color:var(--brand);color:var(--brand-ink)}
/* Press feedback. A button that does not move when pressed is the single
   most common thing that makes a web app feel like a document. 1px of
   travel and a darkening is enough; a scale transform would reflow the row
   height on press and make the table jitter under the pointer. */
.btn:active:not(:disabled){transform:translateY(1px);filter:brightness(.97)}
.btn:disabled{opacity:.5;cursor:not-allowed}
/* Keyboard focus has to be visible on every control, and it was not: the
   only focus styling in the sheet belonged to specific widgets. :focus-visible
   keeps it off mouse clicks, where the ring is noise.
   ⚠️ Only list selectors that still exist. The old font-size control was
   replaced by the prefs menu, and `text-size.test.js` asserts that the retired
   class name is entirely absent from this file — writing it into a selector
   list (or even into a comment, which is how this was caught) turns the suite
   red. The live class is `.prefs`. */
.btn:focus-visible,.lang button:focus-visible,.prefs button:focus-visible,
input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{
  outline:2px solid var(--brand);outline-offset:2px}
.btn.primary{background:var(--brand);color:#fff;border-color:var(--brand)}
.btn.primary:hover:not(:disabled){color:#fff;filter:brightness(1.08)}
.btn.danger{color:var(--bad);border-color:var(--bad-soft)}
.btn.danger:hover:not(:disabled){border-color:var(--bad);color:var(--bad)}
.btn.solid-danger{background:var(--bad);border-color:var(--bad);color:#fff}
/* A link-styled button drops the border and the horizontal padding, but NOT
   the height: these carry the client names in the list, measured at 21px tall.
   min-height is re-stated rather than inherited so that lifting the padding
   here cannot quietly take the tap target with it.
   justify-content because a flex box does not centre its text the way a plain
   <button> does: without it every full-width link on the sign-in cards
   (忘記密碼？, 返回登入 …) sat on the left edge. A link sized to its own text
   has no spare width, so for those it changes nothing. */
.btn.link{border:0;background:none;color:var(--brand-ink);padding:4px 0;text-decoration:underline;
  min-height:44px;display:inline-flex;align-items:center;justify-content:center}
.btnrow{display:flex;gap:8px;flex-wrap:wrap;align-items:center}

/* ------------------------------------------------------------------- lists */
.listhead{display:flex;align-items:baseline;gap:14px;margin:2px 2px 10px;flex-wrap:wrap}
/* 未展開嘅行本身嗰排動作掣(King 2026-09-28)。兩粒掣要夠大撳得到,但要細過
   面板入面嗰啲,唔好搶咗個客名嘅注意力。 */
.row-actions{display:flex;gap:8px;justify-content:flex-end}
.row-actions .row-action{min-height:44px;padding:6px 10px;font-size:0.875rem;white-space:nowrap}
.count{font-weight:700;font-size:0.9375rem}
.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:14px;
  overflow:auto;box-shadow:var(--shadow)}
table{width:100%;border-collapse:collapse;font-size:1rem}
/* Row rhythm, not per-cell padding. 15px top/bottom gives a 56px row at the
   default size — comfortable for a mouse, and still tappable on an iPad.
   ⚠️ th/td keep the SAME vertical padding: a sticky header that is a
   different height from its rows makes the table look like it shimmers as
   you scroll past the first row.
   ⚠️ Horizontal padding is 12px, not the 14px the vertical spacing would
   suggest. It was 11px, and 14px overflowed: measured at 1440 the client
   table needs 1089px inside a 1058px card, so the trailing 保單回顧 button
   was clipped by 31px. 12px lands it with room to spare while still being a
   wider gutter than the 11px it replaces. The constraint is the 動作
   column: two buttons plus a gap is 210px and cannot wrap. */
th,td{text-align:left;padding:15px 12px;border-bottom:1px solid var(--line);white-space:nowrap}
th{position:sticky;top:0;background:var(--panel);font-size:1rem;text-transform:uppercase;
  letter-spacing:.05em;color:var(--muted);z-index:1}
/* The header is a rule, not a row: 1px of ink under it separates the label
   band from the data without adding a second colour. */
thead th{border-bottom:1px solid var(--line);box-shadow:inset 0 -1px 0 0 var(--line)}
th.num,td.num{text-align:right;font-variant-numeric:tabular-nums}
/* Last row sits on the card's own corner — dropping its rule stops the
   line from colliding with the 14px radius. */
tbody tr:last-child td{border-bottom:0}
tbody tr{cursor:pointer}
tbody tr:hover{background:var(--row-hover)}
/* Keyboard parity: a row you can click is a row you can Tab to, so it has
   to look reached. :focus-visible (not :focus) so a mouse click does not
   leave a ring behind. */
tbody tr:focus-visible{outline:2px solid var(--brand);outline-offset:-2px}
tbody td:first-child{font-weight:600}
td.dim{color:var(--muted)}
/* Dates and money are columns of comparable numbers — they read as a grid
   only if the digits line up. Chinese dates still contain 年月日 so this is
   a no-op for the format itself; it aligns the arabic numerals inside them. */
td .when,td time{font-variant-numeric:tabular-nums}
.empty{padding:52px 24px;text-align:center;color:var(--muted)}
.empty p{margin:0 0 6px}
.empty p:first-child{color:var(--ink);font-weight:600}
/* An empty list is the state a new agent meets first, and it used to be one
   line of grey text in a large box. A dashed edge reads as "nothing here
   yet" rather than as a row that failed to load.
   ⚠️ The fill is --bg, not --sunken. In light mode --sunken aliases --field,
   which is #ffffff — the same white as the card it sits on, so the empty
   state would have measured nothing at all. --bg (#f4f6f9) is the recessed
   colour on a light root. */
/* renderList keeps #listEmpty in the DOM and hides it when rows exist. Only
   the visible empty state should change the table's surface. */
.table-wrap:has(.empty:not([hidden])){background:var(--bg);border-style:dashed}

/* Nine nowrap columns pushed 保單回顧 beyond the frame even at normal text
   size. Keep all values in a labelled, reflowing row until the container has
   room for a table AT THE CHOSEN TEXT SIZE. A viewport-only breakpoint misses
   the 150% / 200% setting. Older browsers keep the usable stacked fallback. */
#clientTableWrap{container:client-list / inline-size}
#clientTable,#clientRows{display:block}
#clientTable thead{position:absolute;width:1px;height:1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap}
#clientRows > .client-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,12rem),1fr));
  gap:12px 20px;padding:16px;border-bottom:1px solid var(--line)}
#clientRows > .client-row > td{display:block;min-width:0;padding:0;border:0;
  white-space:normal;overflow-wrap:anywhere;text-align:left}
#clientRows > .client-row > td::before{content:attr(data-label);display:block;
  margin-bottom:4px;font-size:0.8125rem;font-weight:600;color:var(--muted)}
#clientRows > .client-row > td:first-child,
#clientRows > .client-row > td:last-child{grid-column:1 / -1}
#clientRows .name-toggle,#clientRows .hh-pick{max-width:100%;white-space:normal;overflow-wrap:anywhere}
#clientRows .type-tag{display:inline-block;max-width:100%;white-space:normal;overflow-wrap:anywhere}
#clientRows .row-actions{flex-wrap:wrap;justify-content:flex-start}
#clientRows .row-action{flex:1 1 10rem;min-width:0;white-space:normal;overflow-wrap:anywhere}
#clientRows > .review-row{display:block}
#clientRows > .review-row > td{display:block;white-space:normal}

@container client-list (min-width:58rem){
  #clientTable{display:table;table-layout:fixed}
  #clientRows{display:table-row-group}
  #clientTable thead{position:static;display:table-header-group;width:auto;height:auto;
    overflow:visible;clip-path:none;white-space:normal}
  #clientTable th{padding:10px 8px;white-space:normal;overflow-wrap:anywhere;letter-spacing:0}
  #thName{width:14%} #thType{width:7%} #thHousehold{width:12%}
  #thAge,#thGender{width:6%} #thUpdated{width:16%}
  #thLastMet{width:14%} #thRetention{width:13%} #thActions{width:12%}
  #clientRows > .client-row{display:table-row;padding:0}
  #clientRows > .client-row > td{display:table-cell;padding:12px 8px;
    border-bottom:1px solid var(--line);vertical-align:middle}
  #clientRows > .client-row > td.num{text-align:right}
  #clientRows > .client-row > td::before{display:none}
  #clientRows .row-actions{flex-direction:column;gap:8px}
  #clientRows .row-action{flex:none;width:100%}
  #clientRows > .review-row{display:table-row}
  #clientRows > .review-row > td{display:table-cell}
}

/* ---------------------------------------------------------- households -- */
/* Eight pairs, defined for both themes rather than one set dimmed by opacity:
   a translucent tint over a dark panel goes muddy and two of these stop being
   distinguishable, which defeats the only job the colour has. The ink token is
   for the chip's text; the mark token is the bar on the row. */
:root{
  --hh-rose:#b83280;   --hh-rose-soft:#fdeaf4;
  --hh-amber:#8a5a12;  --hh-amber-soft:#fdf3e0;
  --hh-lime:#4d7c0f;   --hh-lime-soft:#eef7e0;
  --hh-teal:#0e7490;   --hh-teal-soft:#e0f2f7;
  --hh-sky:#1d4ed8;    --hh-sky-soft:#e6edfd;
  --hh-violet:#6d28d9; --hh-violet-soft:#f0e9fd;
  --hh-slate:#475569;  --hh-slate-soft:#eef1f5;
  --hh-stone:#78716c;  --hh-stone-soft:#f2f0ee;
  --hh-crimson:#be123c; --hh-crimson-soft:#ffe4ea;
  --hh-orange:#c2410c;  --hh-orange-soft:#ffece0;
  --hh-olive:#5c6b1f;   --hh-olive-soft:#f1f4e0;
  --hh-emerald:#047857; --hh-emerald-soft:#dcf5ec;
  --hh-cyan:#0891b2;    --hh-cyan-soft:#ddf4fb;
  --hh-indigo:#4338ca;  --hh-indigo-soft:#e7e8fc;
  --hh-fuchsia:#a21caf; --hh-fuchsia-soft:#fbe6fd;
  --hh-brown:#78350f;   --hh-brown-soft:#f6ece0;
}
@media (prefers-color-scheme:dark){
  :root{
    --hh-rose:#f07ab8;   --hh-rose-soft:#3a1229;
    --hh-amber:#d6a24a;  --hh-amber-soft:#33280f;
    --hh-lime:#9ccc65;   --hh-lime-soft:#1d2b12;
    --hh-teal:#22b8cf;   --hh-teal-soft:#123039;
    --hh-sky:#7aa2f7;    --hh-sky-soft:#141e3a;
    --hh-violet:#b39ddb; --hh-violet-soft:#241a3a;
    --hh-slate:#9fb0c4;  --hh-slate-soft:#1b232d;
    --hh-stone:#c0b8b1;  --hh-stone-soft:#262220;
    --hh-crimson:#f4778f; --hh-crimson-soft:#3c1119;
    --hh-orange:#f59e6b;  --hh-orange-soft:#3a1d0c;
    --hh-olive:#b7c96a;   --hh-olive-soft:#25290f;
    --hh-emerald:#5ecfa0; --hh-emerald-soft:#0d2b1f;
    --hh-cyan:#4dd0e1;    --hh-cyan-soft:#0d2a31;
    --hh-indigo:#8f9bf5;  --hh-indigo-soft:#171a3a;
    --hh-fuchsia:#e07ae8; --hh-fuchsia-soft:#33113a;
    --hh-brown:#c9a288;   --hh-brown-soft:#2b1d13;
  }
}

/* The row itself. A bar down the leading edge rather than a wash across the row:
   a tinted background fights the zebra striping and the focus ring, and it makes
   text on the row harder to read for the sake of a fact that is not about the
   text. */
tr.hh{box-shadow:inset 3px 0 0 0 var(--hh-mark)}
tr.hh > td:first-child{padding-left:11px}
.hh-rose{--hh-mark:var(--hh-rose);--hh-ink:var(--hh-rose);--hh-bg:var(--hh-rose-soft)}
.hh-amber{--hh-mark:var(--hh-amber);--hh-ink:var(--hh-amber);--hh-bg:var(--hh-amber-soft)}
.hh-lime{--hh-mark:var(--hh-lime);--hh-ink:var(--hh-lime);--hh-bg:var(--hh-lime-soft)}
.hh-teal{--hh-mark:var(--hh-teal);--hh-ink:var(--hh-teal);--hh-bg:var(--hh-teal-soft)}
.hh-sky{--hh-mark:var(--hh-sky);--hh-ink:var(--hh-sky);--hh-bg:var(--hh-sky-soft)}
.hh-violet{--hh-mark:var(--hh-violet);--hh-ink:var(--hh-violet);--hh-bg:var(--hh-violet-soft)}
.hh-slate{--hh-mark:var(--hh-slate);--hh-ink:var(--hh-slate);--hh-bg:var(--hh-slate-soft)}
.hh-stone{--hh-mark:var(--hh-stone);--hh-ink:var(--hh-stone);--hh-bg:var(--hh-stone-soft)}
.hh-crimson{--hh-mark:var(--hh-crimson);--hh-ink:var(--hh-crimson);--hh-bg:var(--hh-crimson-soft)}
.hh-orange{--hh-mark:var(--hh-orange);--hh-ink:var(--hh-orange);--hh-bg:var(--hh-orange-soft)}
.hh-olive{--hh-mark:var(--hh-olive);--hh-ink:var(--hh-olive);--hh-bg:var(--hh-olive-soft)}
.hh-emerald{--hh-mark:var(--hh-emerald);--hh-ink:var(--hh-emerald);--hh-bg:var(--hh-emerald-soft)}
.hh-cyan{--hh-mark:var(--hh-cyan);--hh-ink:var(--hh-cyan);--hh-bg:var(--hh-cyan-soft)}
.hh-indigo{--hh-mark:var(--hh-indigo);--hh-ink:var(--hh-indigo);--hh-bg:var(--hh-indigo-soft)}
.hh-fuchsia{--hh-mark:var(--hh-fuchsia);--hh-ink:var(--hh-fuchsia);--hh-bg:var(--hh-fuchsia-soft)}
.hh-brown{--hh-mark:var(--hh-brown);--hh-ink:var(--hh-brown);--hh-bg:var(--hh-brown-soft)}

.hh-swatch{display:inline-block;width:11px;height:11px;border-radius:3px;
  background:var(--hh-mark);vertical-align:-1px;margin-right:6px}

.type-tag{font-size:0.75rem;font-weight:700;padding:2px 8px;border-radius:6px;
  background:var(--bg);color:var(--muted);border:1px solid var(--line)}
/* Pills are inset, not pasted on. A hairline in the pill's own ink at low
   alpha gives the flat fill an edge — this is what separates a designed
   status chip from a rounded rectangle. currentColor means one rule works
   for ok/warn/bad/brand without repeating the border four times, and it
   cannot drift out of sync when a variant is added. */
.badge{font-size:0.75rem;font-weight:700;padding:3px 9px;border-radius:999px;display:inline-block;
  box-shadow:inset 0 0 0 1px color-mix(in srgb, currentColor 22%, transparent)}
.badge.ok{background:var(--ok-soft);color:var(--ok-ink)}
.badge.warn{background:var(--warn-soft);color:var(--warn)}
.badge.bad{background:var(--bad-soft);color:var(--bad)}

/* -------------------------------------------------------------- PDPO panels */
/* The collection statement is its OWN DIALOG and it blocks: DPP1(3) is a duty
   to tell the client at the time of collection, and a panel on the form with an
   optional tick beside it discharged nothing. See #picsOverlay in index.html.

   The app chrome stays above the scrim on purpose — an agent sitting with a
   client who reads 简体 has to be able to switch language while the statement is
   on screen, and the form behind is held by `inert`, not by the scrim. */
/* The scrim sits where every other overlay sits (60). The chrome is lifted just
   above it while the statement is up, and stays below the toasts (70) so a
   message about the dialog is never hidden by the bar at the top of it. */
body.picsgate .topbar{z-index:65}
.pdpo-modal{border:1px solid var(--brand)}
.pdpo-modal .modal-head{background:var(--brand-soft);gap:10px;justify-content:flex-start;
  flex-wrap:wrap}
.pdpo-modal .modal-head .badge{background:var(--brand);color:#fff;flex:none}
.pdpo-modal .modal-head h2{color:var(--brand-ink)}
.pdpo-modal .modal-body p{font-size:0.84375rem}
.pdpo-modal .modal-foot{align-items:flex-end}
.pdpo-modal .legal{border-top:1px solid var(--line);margin-top:10px;padding-top:6px}
.pdpo-modal .legal h2{font-size:0.9375rem;margin:14px 0 6px}
.pdpo-modal .fail{color:var(--bad);font-weight:600}

/* The line the form keeps once the dialog is gone: when the client was told,
   how, and a way to put the statement back on screen for them. */
.pdpo-stamp{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 14px;
  padding:8px 12px;border:1px solid var(--line);border-radius:11px;background:var(--panel);
  color:var(--muted);font-size:0.78125rem;line-height:1.5}
.pdpo-stamp.ok{background:var(--brand-soft);border-color:var(--brand);color:var(--brand-ink)}
/* Consent taken against a notice that has since been revised. Declared AFTER
   .ok because both land on the same element — the strip is still "shown", it is
   just shown for a document the client no longer agreed to, and a green tick on
   that is the misreading this class exists to stop. */
.pdpo-stamp.stale{background:var(--warn-soft);border-color:var(--warn);color:var(--warn)}
.pdpo-stamp .btn.link{margin-left:auto}

.legal{max-width:70ch}
.legal h2{font-size:1.25rem;text-transform:none;letter-spacing:0;color:var(--ink);margin:22px 0 8px}
.legal h1{font-size:1.5rem;margin:0 0 4px}
.legal p{font-size:1rem;line-height:1.7;margin:0 0 10px}
.legal .updated{color:var(--muted);font-size:0.78125rem}

/* --------------------------------------------------------------- overlays */
.overlay{position:fixed;inset:0;background:rgba(16,32,44,.5);z-index:60;
  display:grid;place-items:center;padding:20px;overflow:auto}
.modal{background:var(--panel);border-radius:16px;width:min(620px,96vw);max-height:92vh;
  display:flex;flex-direction:column;box-shadow:var(--shadow);overflow:hidden}
.modal-head{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:14px 18px;border-bottom:1px solid var(--line)}
.modal-head h2{margin:0;font-size:1.0625rem;text-transform:none;letter-spacing:0;color:var(--ink)}
.modal-body{overflow:auto;padding:16px 18px}
.modal-body p{margin:0 0 10px;font-size:0.84375rem;line-height:1.6}
.modal-foot{display:flex;gap:8px;justify-content:flex-end;padding:12px 18px;
  border-top:1px solid var(--line);flex-wrap:wrap}
.modal.danger .modal-head{background:var(--bad-soft)}
.modal.danger .modal-head h2{color:var(--bad)}
.ref{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.8125rem;
  background:var(--bg);border:1px solid var(--line);border-radius:7px;padding:2px 7px;
  user-select:all;letter-spacing:.06em}
.urlbox{display:block;font-size:0.75rem;color:var(--muted);word-break:break-all;
  background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:8px 10px;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.kv{width:100%;border-collapse:collapse;font-size:0.84375rem;margin:4px 0 12px}
.kv th{position:static;text-transform:none;letter-spacing:0;font-size:0.8125rem;
  font-weight:600;color:var(--muted);width:38%;white-space:normal;vertical-align:top}
.kv td{white-space:normal;vertical-align:top}
.kv td.none{color:var(--muted);font-style:italic}

/* ------------------------------------------------------------------ toasts */
.toasts{position:fixed;left:50%;transform:translateX(-50%);bottom:20px;z-index:70;
  display:flex;flex-direction:column;gap:8px;align-items:center;pointer-events:none;
  max-width:min(560px,94vw)}
.toast{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--ok);
  border-radius:10px;padding:10px 16px;box-shadow:var(--shadow);font-size:0.84375rem;
  pointer-events:auto;line-height:1.5}
.toast.bad{border-left-color:var(--bad)}
.toast.warn{border-left-color:var(--warn)}

/* ------------------------------------------------------------------ signin */
/* The first screen an agent sees, and the only one with no data on it — so
   it has to carry the product on composition alone.
   The background is two very wide, very low-contrast teal radial washes
   anchored off-canvas top-left and bottom-right. They give the page a light
   direction and stop a 1400px-tall viewport reading as an empty grey field.
   ⚠️ This is deliberately NOT a gradient hero banner: 5% alpha at this size
   is atmosphere, and it keeps the whole thing inside the existing two-colour
   palette rather than introducing a third brand colour. */
.gate{max-width:430px;margin:8vh auto 0;padding:0 20px 60px;position:relative}
.gate::before{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:
    radial-gradient(1200px 620px at 8% -12%, color-mix(in srgb, var(--brand) 9%, transparent), transparent 70%),
    radial-gradient(900px 520px at 104% 112%, color-mix(in srgb, var(--brand) 6%, transparent), transparent 72%)}
/* The card gets a touch more presence here than elsewhere: a ring in the
   brand colour at low alpha, so the panel reads as lifted rather than as a
   white rectangle that happens to sit on a lighter grey. */
.gate .card{padding:26px 26px 24px;border-color:color-mix(in srgb, var(--brand) 16%, var(--line));
  box-shadow:var(--shadow),0 18px 48px rgba(14,116,144,.10)}
.gate h2{font-size:1.1875rem;text-transform:none;letter-spacing:0;color:var(--ink);margin:0 0 6px}
.gate label{margin-bottom:12px}
.gate .btn{width:100%;padding:11px}
.gate .swap{text-align:center;margin-top:12px}
/* The sign-in button is the single most-used control in the product, and it
   was a flat fill. A one-pixel top highlight is the whole difference between
   a coloured rectangle and a surface. */
.gate .btn.primary{box-shadow:inset 0 1px 0 rgba(255,255,255,.22),0 2px 6px rgba(14,116,144,.28)}
.gate .brand{margin-bottom:18px}
/* The three language buttons sit as a group above the card. Nudging them to
   the right edge lines their outer edge up with the card's, so the column
   has one edge instead of two. */
.gate .lang{display:flex;justify-content:flex-end;margin-bottom:10px;
  background:transparent;border:0;gap:6px}
.gate .lang button{border:1px solid var(--line);background:var(--panel);border-radius:9px;
  color:var(--muted);font-weight:700}
.gate .lang button.on{background:var(--brand);border-color:var(--brand);color:#fff}
.formerr{background:var(--bad-soft);color:var(--bad);border-radius:9px;padding:10px 12px;
  font-size:1rem;font-weight:600;margin-bottom:12px;line-height:1.5}

/* ------------------------------------------------------------- two-factor */
/* The setup key and the recovery codes are read off a screen and typed into a
   phone, so they get a monospace face, generous tracking and select-all: an
   agent transcribing "0" as "O" is the failure mode this styling exists for. */
.mono,.secret,.uri,.codes code{
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace}
.secret{font-size:1.0625rem;letter-spacing:.10em;background:var(--brand-soft);color:var(--brand-ink);
  border-radius:9px;padding:11px 13px;margin:6px 0 0;word-break:break-all;user-select:all}
.uri{font-size:0.75rem;color:var(--muted);background:var(--bg);border:1px solid var(--line);
  border-radius:9px;padding:9px 11px;margin:6px 0 0;word-break:break-all;user-select:all}
/* The QR carries its own white ground and a four-module quiet zone. A dark
   theme tinting it, or a background showing through, is exactly what makes a
   code a camera will not lock on to — so the white is set here as well, and
   nothing is allowed to crop it. */
/* Natural size is six pixels a module (default issuer): 342px for a very short
   address, 366px for a typical one, up to 510px for the longest address registration accepts.
   max-height is what keeps the WHOLE code on screen under the intro on a small
   laptop: measured in Chrome at a 594px viewport, the natural size left the
   bottom 38px (and 82px at 546px) below the fold — a camera needs the quiet zone
   too. 45vh there is ~4.5px a module for a typical address and ~2.9 at the very
   longest; zxing read these from a 2.5px render. */
/* 伺服器話係邊一格錯咗,就真係標住嗰格 —— 一句叫人睇「標示的欄位」嘅
   訊息,旁邊冇嘢標住,等於冇講。 */
input[aria-invalid="true"]{border-color:var(--bad);box-shadow:0 0 0 2px color-mix(in srgb,var(--bad) 25%,transparent)}
.qr{display:block;width:auto;height:auto;max-width:100%;max-height:45vh;margin:8px 0 0;
  background:#fff;border-radius:9px}
.codes{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:8px;margin:10px 0 0}
.codes code{font-size:0.9375rem;letter-spacing:.08em;background:var(--bg);border:1px solid var(--line);
  border-radius:8px;padding:9px 6px;text-align:center;user-select:all}
.modal .fail,p.fail{color:var(--bad);font-weight:600;font-size:0.8125rem;margin:8px 0 0}
.modal h3{font-size:1.0625rem;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);
  margin:16px 0 0}
#twofaState{font-weight:600;margin:10px 0 0}
#twofaCard .btnrow{margin-top:12px}

/* --------------------------------------------------------------- record bar */
.recbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:14px}
.recbar h2{margin:0;font-size:1.1875rem;text-transform:none;letter-spacing:0;color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;max-width:32ch;white-space:nowrap}

.savebar{position:sticky;bottom:0;background:var(--panel);border-top:1px solid var(--line);
  margin:0 -18px -16px;padding:12px 18px;display:flex;gap:8px;align-items:center;
  flex-wrap:wrap;border-radius:0 0 14px 14px}

/* ------------------------------------------------------ the policy wizard */
/* Four steps, in the industry form's own order. Every one of them is reachable
   at any time: an agent copying a schedule reads it in the order it is printed,
   which is not necessarily this one. */
.steps{display:flex;gap:8px;margin:0 0 14px;flex-wrap:wrap}
.steps .step{display:flex;align-items:center;gap:8px;font:inherit;font-size:1rem;font-weight:600;
  background:var(--panel);color:var(--muted);border:1px solid var(--line);border-radius:999px;
  padding:7px 15px 7px 8px;cursor:pointer;min-height:44px}
.steps .step:hover{border-color:var(--brand);color:var(--brand-ink)}
.steps .stepnum{width:22px;height:22px;flex:none;display:grid;place-items:center;border-radius:50%;
  background:var(--bg);color:var(--muted);font-size:0.75rem;font-variant-numeric:tabular-nums}
.steps .step.on{background:var(--brand);border-color:var(--brand);color:#fff}
.steps .step.on .stepnum{background:rgba(255,255,255,.25);color:#fff}
/* A field rejected on one step is invisible from another without this. */
.steps .step.bad{border-color:var(--bad);color:var(--bad)}
.steps .step.bad .stepnum{background:var(--bad);color:#fff}
.steps .step.on.bad{background:var(--bad);border-color:var(--bad);color:#fff}
.stepnav{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:14px;
  padding-top:12px;border-top:1px solid var(--line)}
/* A boxed group inside a step. 詳情 opens with 貨幣 / 人壽保障額 / 保費 in one of
   these: the three figures the whole contract is measured in, read as a heading
   to the step rather than as three more entries in it. Flatter than a client
   card, because it sits INSIDE a panel that already has a border of its own. */
.pcard{margin:0 0 14px;padding:12px 14px;box-shadow:none}
.pcard h2{margin:0 0 8px}

.steppanel{min-height:120px}

/* ------------------------------------------- the two searchable comboboxes */
/* The reason the feature exists: 計劃名稱 is a free-text box with 1,604
   published plan names behind it. The list ACCELERATES and never constrains,
   so it is styled as a suggestion sheet and never as a validation state.
   保險公司 above it wears the same clothes — one search key per company
   holding both names and the code, so AIA reaches 友邦 in a Chinese UI. It is
   a CLOSED list, but it is still a suggestion sheet: the field it cannot hold
   is 保險公司（按保單印本）, not a red box here. */
/* A div, not a label: a click anywhere inside a label is forwarded to its
   control, which would put focus back in the box before a picked suggestion
   was read. The caption is a real <label for> and has to be told to lay out
   as a row, since the generic label rule stacks its children. */
.f.ac{display:flex;flex-direction:column;gap:3px;position:relative}
.f.ac > label{flex-direction:row;align-items:baseline;gap:0}
.ac-pop{position:absolute;top:100%;left:0;right:0;z-index:30;margin-top:4px;
  background:var(--panel);border:1px solid var(--line);border-radius:11px;
  box-shadow:var(--shadow);overflow:hidden}
.ac-list{list-style:none;margin:0;padding:4px;max-height:min(46vh,340px);overflow:auto}
/* An industry heading inside the 職業 list. Sticky, because the whole manual is
   in one scroller and knowing which trade you are looking at matters. */
.ac-group{position:sticky;top:-4px;z-index:1;background:var(--bg);color:var(--muted);
  font-size:0.75rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  padding:7px 10px 4px;margin:2px -4px 0;border-bottom:1px solid var(--line);cursor:default}
.ac-group:first-child{margin-top:-2px}
.ac-row{padding:7px 10px;border-radius:8px;cursor:pointer;font-weight:500;color:var(--ink)}
.ac-row:hover,.ac-row.on{background:var(--brand-soft)}
.ac-name{font-size:1rem;font-weight:600;line-height:1.35}
/* The other language, so an agent holding an English schedule recognises the
   Chinese entry and the other way round. */
.ac-alt{font-size:0.78125rem;color:var(--muted);line-height:1.35;margin-top:1px}
/* 未有記錄 at the top of the insurer list — an answer, but not a company, so it
   is not dressed as one. */
.ac-name.none{color:var(--muted);font-weight:500;font-style:italic}
.ac-tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:5px;align-items:center}
.ac-foot{margin:0;padding:8px 12px;border-top:1px solid var(--line);background:var(--bg);
  color:var(--muted);font-size:0.875rem;line-height:1.5}
.ac-foot:empty{display:none}

/* Keep the MPF list in flow: at 200% text / above a tablet keyboard it can be
   scrolled into view instead of falling beneath the viewport or over a button. */
.mpf-scheme-input{display:flex;gap:6px;min-width:0}
.mpf-scheme-input input{flex:1;min-width:0;width:0}
.mpf-scheme-input button{flex:none;min-width:44px;min-height:44px}
.mpf-scheme .ac-pop{position:static;margin-top:4px}
.mpf-scheme .ac-row{min-height:44px;overflow-wrap:anywhere}
.mpf-scheme .hint{overflow-wrap:anywhere}

/* --------------------------------------------------- policies on a record */
#policyBox .table-wrap{box-shadow:none;margin-bottom:12px}
#policyBox td .type-tag{margin-right:6px}
.type-tag.pending{background:var(--warn-soft);color:var(--warn);border-color:var(--warn-soft);
  margin-left:8px}

/* ------------------------------------------------------------------- print */
/* The DPP6 copy is a document that gets handed to a person, so it prints as
   one: no chrome, no buttons, black on white. */
@media print{
  .topbar,.toasts,.noprint,.btn,.savebar{display:none !important}
  :root{--bg:#fff;--panel:#fff;--ink:#111;--muted:#444;--line:#bbb;--brand-soft:#eef7fa}
  body{background:#fff}
  main{max-width:none;padding:0}
  .card,.pdpo-stamp{box-shadow:none;border:0;padding:0;margin-bottom:14px}
  .kv th,.kv td{padding:5px 6px}
  table{page-break-inside:auto}
  tr{page-break-inside:avoid}
}
.printonly{display:none}
@media print{.printonly{display:block}}

/* A suggestion outside the category the agent chose. Still offered — the
   catalogue accelerates and never gates — but visibly not the one asked for,
   so a row sitting below the hoisted ones is explained rather than odd. */

/* 職業 loading warning. Amber, not red: this is a thing to tell the client
   about, not an error in the form — the record saves either way. */

/* 興趣 rows. One box and a delete per row, stacked; the phone rows' grid does
   not apply because there is nothing to line up against. */
.f.hobbies{grid-column:span 12}
.hobby-rows{display:flex;flex-direction:column;gap:6px;margin-top:4px}
.hobby-row{display:flex;gap:6px;align-items:center}
.hobby-text{flex:1 1 auto;min-width:0}
.hobby-del{flex:0 0 auto;padding:6px 10px;line-height:1;color:var(--muted);min-width:44px}
.hobby-del:hover{color:var(--bad);border-color:var(--bad)}
.hobby-add{margin-top:8px}

/* 逐項病史。一項佔兩行六格,每格上面有自己嘅標題:
       疾病名稱 │ 何時發現 │ 客戶所述
       最後複診 │ 現時情況 │ 曾否索償
   成項圍一個框,唔係就兩行睇落似兩項。窄過 760px 一格一行疊落去 —— 三格擠喺
   一部電話嘅闊度,每格得幾個字,寫唔到「客戶所述」嗰種句子。 */
.f.conditions{grid-column:span 12;display:flex;flex-direction:column;gap:6px}
.conditions-label{font-size:0.8125rem;font-weight:600;color:var(--muted)}
.cond-rows{display:flex;flex-direction:column;gap:10px;margin-top:4px}
.cond-row{display:grid;gap:8px 10px;align-items:end;
  grid-template-columns:repeat(3,minmax(0,1fr)) auto;
  padding:10px;border:1px solid var(--line);border-radius:10px}
/* 一格 = 個標題 + 個輸入格。 */
.cond-cell{display:flex;flex-direction:column;gap:3px;min-width:0}
.cond-cap{font-size:0.75rem;color:var(--muted)}
/* 冇呢句,格仔會用自己嘅預設闊度撐爆一行,連 ✕ 都撳唔到(喺電話上面尤其明顯)。 */
.cond-row input,.cond-row select{min-width:0;width:100%}
/* 個 ✕ 企喺右邊跨住兩行 —— 一項一個,唔係一行一個。佢喺 DOM 度排最後(打字
   嘅次序:六格順住行,最後先到刪除),所以一定要明寫位置。 */
.cond-row .cond-del{grid-column:4;grid-row:1 / span 2;align-self:center}
.cond-del{flex:none;padding:8px 11px;line-height:1;color:var(--muted);min-width:44px}
.cond-del:hover:not(:disabled){border-color:var(--bad);color:var(--bad)}
.cond-add{align-self:flex-start;margin-top:2px}
@media (max-width:760px){
  /* 一格一行,✕ 企喺「疾病名稱」隔籬 —— 唔使捲到最底先撤到一項。 */
  .cond-row{grid-template-columns:1fr auto}
  .cond-row .cond-cell{grid-column:1}
  .cond-row .cond-del{grid-column:2;grid-row:1;align-self:end}
}

/* 受益人。保單表印出嚟就係咁:一行一個人 —— 姓名 │ 關係 │ 份額 │ 可否撤銷,
   最右邊一個 ✕。同逐項病史一樣一行圍一個框,唔係就幾行睇落似一大堆散格。
   姓名佔闊啲(2fr):香港人一個名連英文可以好長,而份額最多得「100」三個字。
   窄過 760px 一格一行疊落去 —— 四格擠喺一部電話嘅闊度,個姓名格得返幾個字位。 */
.f.beneficiaries{grid-column:span 12;display:flex;flex-direction:column;gap:6px}
.beneficiaries-label{font-size:0.8125rem;font-weight:600;color:var(--muted)}
.ben-rows{display:flex;flex-direction:column;gap:10px;margin-top:4px}
.ben-row{display:grid;gap:8px 10px;align-items:end;
  grid-template-columns:minmax(0,2fr) minmax(0,1.2fr) minmax(0,.8fr) minmax(0,1.2fr) auto;
  padding:10px;border:1px solid var(--line);border-radius:10px}
/* 一格 = 個標題 + 個輸入格。 */
.ben-cell{display:flex;flex-direction:column;gap:3px;min-width:0}
.ben-cap{font-size:0.75rem;color:var(--muted)}
/* 冇呢句,格仔會用自己嘅預設闊度撐爆一行,連 ✕ 都撳唔到(喺電話上面尤其明顯)。 */
.ben-row input,.ben-row select{min-width:0;width:100%}
/* 個 ✕ 企喺四格之後。佢喺 DOM 度排最後(打字嘅次序:四格順住行,最後先到刪除),
   所以要明寫位置,唔係靠 DOM 次序。 */
.ben-row .ben-del{grid-column:5;align-self:center}
.ben-del{flex:none;padding:8px 11px;line-height:1;color:var(--muted);min-width:44px}
.ben-del:hover:not(:disabled){border-color:var(--bad);color:var(--bad)}
.ben-add{align-self:flex-start;margin-top:2px}
@media (max-width:760px){
  /* 一格一行,✕ 企喺「姓名」隔籬 —— 唔使捲到最底先撤到一個受益人。 */
  .ben-row{grid-template-columns:1fr auto}
  .ben-row .ben-cell{grid-column:1}
  .ben-row .ben-del{grid-column:2;grid-row:1;align-self:end}
}

/* 逐年現金價值。保險公司建議書上面嗰張推算表就係咁:一行一年 —— 保單年度 │
   年份 │ 現金價值,最右邊一個 ✕。同受益人一樣一行圍一個框。
   金額佔闊啲(2fr):一個七位數連逗號要好多字位,而保單年度最多得兩個字。
   窄過 760px 一格一行疊落去,同受益人一樣。 */
.f.cash-values{grid-column:span 12;display:flex;flex-direction:column;gap:6px}
.cash-values-label{font-size:0.8125rem;font-weight:600;color:var(--muted)}
.cv-rows{display:flex;flex-direction:column;gap:10px;margin-top:4px}
.cv-row{display:grid;gap:8px 10px;align-items:end;
  grid-template-columns:minmax(0,.7fr) minmax(0,.9fr) minmax(0,2fr) auto;
  padding:10px;border:1px solid var(--line);border-radius:10px}
/* 一格 = 個標題 + 個輸入格。 */
.cv-cell{display:flex;flex-direction:column;gap:3px;min-width:0}
.cv-cap{font-size:0.75rem;color:var(--muted)}
/* 冇呢句,格仔會用自己嘅預設闊度撐爆一行,連 ✕ 都撳唔到(喺電話上面尤其明顯)。 */
.cv-row input{min-width:0;width:100%}
/* 個 ✕ 企喺三格之後。佢喺 DOM 度排最後(打字嘅次序:三格順住行,最後先到刪除),
   所以要明寫位置,唔係靠 DOM 次序。 */
.cv-row .cv-del{grid-column:4;align-self:center}
.cv-del{flex:none;padding:8px 11px;line-height:1;color:var(--muted);min-width:44px}
.cv-del:hover:not(:disabled){border-color:var(--bad);color:var(--bad)}
/* ⚠️ scroll-margin-bottom 唔係裝飾。個 savebar 係 position:sticky;bottom:0,
   會蓋住下面嘅嘢;而呢個＋每加一行就向下行一行,加到十幾行就會鑽咗入個
   savebar 下面 —— 撳落去嗰一下會變成撳咗「儲存」。真瀏覽器度大概每四下
   撞一次,而 DOM harness 永遠見唔到(佢冇版面亦冇捲動)。
   呢個數大過個 savebar 高度,所以 scrollIntoView({block:'nearest'}) 之後
   個掣一定企喺個 bar 之上。 */
.cv-add{align-self:flex-start;margin-top:2px;scroll-margin-bottom:96px}
@media (max-width:760px){
  /* 一格一行,✕ 企喺「保單年度」隔籬 —— 唔使捲到最底先刪到一行。 */
  .cv-row{grid-template-columns:1fr auto}
  .cv-row .cv-cell{grid-column:1}
  .cv-row .cv-del{grid-column:2;grid-row:1;align-self:end}
}

/* One party per row: 保單持有人 / 受保人 / 受益人, each with its relation, its
   name and — only when the relation is 他人 — what they are to the client. The
   three columns are fixed, so the empty third column keeps the three rows
   aligned with each other instead of the next party sliding up into the gap. */
.prow{grid-column:span 12;display:grid;grid-template-columns:repeat(3,1fr);gap:12px 14px;align-items:start}
.prow > .f{grid-column:auto}
/* Two-field rows get two columns, not three with a hole. The three-column rule
   above exists so the THREE party rows line up with each other; these two are
   a pair on their own, and a plan name is long enough to want the half. */
.prow-kind,.prow-plan,.prow-names,.prow-benefit{grid-template-columns:repeat(2,1fr)}
/* 受保人 carries one more thing than the other two parties — a birth date,
   when the life insured is somebody other than the client. Four cells, so
   its row no longer lines up with 保單持有人 and 受益人 above and below it.
   That is the cost of the field; the alternative was a fourth column on all
   three rows standing empty on two of them. */
.prow-insured{grid-template-columns:repeat(4,1fr)}
@media (max-width:760px){
  /* Stacked, because three boxes across a phone is three unreadable boxes. */
  .prow{grid-template-columns:1fr}
}

/* A note about two fields together rather than about either one, so it sits
   under the row instead of inside a field's own .msg. Amber: it is something to
   check, not something the form refuses. */
/* 計劃類型 = 附加 and the plan it attaches to: one question asked in two boxes
   several rows apart, so both wear the same mark while that answer stands. A
   rule AND a tint, never hue alone — and both are painted from the same tokens
   that carry the light and dark palettes, so the pair reads in either. */
.f.paired{position:relative;background:var(--brand-soft);border-radius:8px;
  padding:6px 8px 6px 12px;margin-left:-12px}
.f.paired::before{content:'';position:absolute;left:0;top:6px;bottom:6px;width:3px;
  border-radius:2px;background:var(--brand)}
.f.paired>span:first-child{color:var(--brand-ink)}
.rowwarn{grid-column:span 12;margin:-2px 0 2px;padding:8px 10px;border-radius:6px;
  font-size:0.78125rem;line-height:1.55;font-weight:600;background:var(--warn-soft);
  color:var(--warn);border:1px solid var(--warn)}

/* 加費 —— 一個淺藍色嘅「參考」框。Rex 2026-09-23 叫由琥珀轉藍。
   --brand-soft 係成個 app 講嘢嗰陣用嘅底色:.pill、.f.paired、.pdpo-stamp.ok
   全部都係佢。琥珀(--warn-soft)喺呢度係 .rowwarn 嘅顏色,即係「有嘢等你
   處理」。呢個框冇嘢等你處理 —— 佢淨係話俾你知上面嗰個保費已經包咗呢筆錢。
   以前兩者共用同一塊底,靠邊同字重分開,即係要人先睇到第二層先分到第一層。
   而家唔使:兩塊底色根本唔同,一眼就分到。
   分開咗之後仲淨返咩:.rowwarn 係 --warn 做邊同做字、粗體;呢個框係 --line
   做邊、--muted 做字、正常粗幼。所以就算有人將來調色調到兩塊底撞埋,邊同
   字重都仲係兩種講法 —— 呢一層冇拆,只係唔再係唯一嗰層。
   量過(WCAG 2.1 relative luminance):--muted 喺 --brand-soft 上面淺色模式
   5.53:1、深色模式 5.29:1,兩邊都過 4.5:1。揀 --muted 係因為佢係成張表其他
   .hint 嘅顏色,顏色一樣就睇得出係同一類嘢。框入面嘅 <label>、.hint 一樣係
   --muted;.star 係 --brand-ink,喺同一塊底上面 4.65(淺)/ 8.54:1(深)——
   而家佢同塊底同一家族,係當初揀 --brand-soft 嘅其中一個原因。
   整格佔足 12 欄:個框入面要放得落兩格加一句,而且佢係一句關於上面嗰個保費
   嘅說明 —— 縮喺一角就變咗某一格嘅附註。 */
.f.pnote,.pnote{grid-column:span 12;display:grid;gap:8px 14px;
  grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;
  background:var(--brand-soft);border:1px solid var(--line);border-radius:10px;
  padding:10px 12px}
/* 入面嘅格唔跟外面 12 欄嗰套 —— 呢度得兩欄。 */
.pnote > .f{grid-column:auto;min-width:0}
/* 句說明打橫佔晒,企喺兩格下面:佢講嘅係嗰一對,唔係其中一格。 */
.pnote-line{grid-column:1 / -1;margin:0;color:var(--muted);
  font-size:0.75rem;line-height:1.55}
/* 呢個框入面嘅錯誤訊息要換一塊底,唔係換一個顏色。
   轉咗做藍底之後重新量過,結論一樣,數字唔同:--bad 直接踩喺 --brand-soft
   上面係 4.72:1(淺)、4.26:1(深) —— 深色嗰邊照舊差過 WCAG 要嘅 4.5:1。
   (琥珀嗰陣係 4.94 / 4.42:1,即係轉色冇救到呢一句,亦都冇整衰佢。)
   而呢一句偏偏就係「加咗個數,未揀單位」嗰句,即係個表格唯一一次主動開口同
   你講嘢。所以規矩一個字都唔郁(.f .msg 照樣係 --bad,照樣係寫出嚟嘅一句字,
   唔係淨靠個紅色),郁嘅係佢企喺邊:
   --bad 喺 --panel 上面係 5.44:1(淺)、4.97:1(深),兩個色盤都過。
   align-self 係要佢淨係包住啲字。上面個 .f 係 flex column,唔講嘅話佢會撐到成
   格咁闊 —— 一條橫跨成格嘅實色帶讀落係多咗一個欄位,唔係嗰格下面嗰句錯誤。
   :not(:empty) 係因為每一格都常駐一個空嘅 .msg(paintPolicyErrors 靠佢喺度先
   填得到字);冇呢句,冇錯嗰陣都會見到一粒無端端嘅色塊。 */
.pnote .msg:not(:empty){align-self:flex-start;background:var(--panel);
  border-radius:6px;padding:3px 8px}
@media (max-width:760px){
  /* 一格一行。呢個註解本來話因為「每千元保額」太長會斷三行 —— 嗰個單位
     2026-09-23 已經冇咗,但係一格一行呢件事冇變:一格得半個電話闊度,係擺俾
     人打銀碼同揀單位嘅兩個 44px 控制項,唔係擺嚟迫兩格並排。 */
  .f.pnote,.pnote{grid-template-columns:1fr}
}

/* 最近一次會面日期 — its own box above the name, because it is the state of the
   relationship rather than a detail of the client. */
.meetcard{display:flex;flex-wrap:wrap;align-items:flex-end;gap:10px 14px}
.meetcard .f{margin:0;flex:0 1 220px;min-width:0;max-width:100%}
.meetcard .hint{flex:1 1 100%;margin:0}
.meet-today{flex:0 0 auto;margin-bottom:2px}

/* A rider reads under the plan it hangs off: indented, with a mark that does
   not depend on the indent being noticed. */
.polrow-rider > td:first-child{padding-left:26px}
.rider-mark{color:var(--muted);margin-right:6px;font-weight:400}
.type-tag.warn{background:var(--warn-soft);color:var(--warn);
  border-color:var(--warn)}
/* 加費 喺保費隔籬。同 .pnote 一套講法,所以個框轉咗淺藍,呢個 tag 一齊轉 ——
   一個代理喺表格度見到藍框、喺清單度見到琥珀 tag,係同一件事著兩件衫。
   佢唔改動嗰一格嘅數,亦都唔改動上面個總保費,所以佢唔可以著住 .warn 嗰件衫;
   而家連底色都唔同,同上面個 .type-tag.warn 分得更加開。
   ⚠️ 但個 tag 唔可以用 --brand-soft 做底:`tbody tr:hover` 就係嗰隻色,滑鼠一放
   上去成個 chip 就溶咗入行度,睇落變咗散字。所以佢用 --panel 做底、--brand-ink
   做邊 —— 藍係喺個邊度講,唔係喺底色度講,而底色點都唔會同 hover 撞。
   量過:--muted 喺 --panel 上面 6.38:1(淺)/ 6.17:1(深)。 */
.type-tag.ref{background:var(--panel);color:var(--muted);
  border-color:var(--brand-ink)}

/* ------------------------------------------------------------- 概覽 ------ */
/* Sits between the section heading and the table: read the four figures, then
   read the rows they came from. */
.overview{border:1px solid var(--line);border-radius:12px;background:var(--bg);
  padding:12px 14px;margin:0 0 14px}
.ov-figs{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}
.ov-fig{min-width:0}
.ov-label{color:var(--muted);font-size:0.75rem;font-weight:700;margin-bottom:2px}
/* Tabular figures so the columns line up down the card, and wrapping allowed:
   a long total must never be clipped into a shorter, wrong-looking number. */
.ov-amount{font-size:1.0625rem;font-weight:750;letter-spacing:-.01em;
  font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
.ov-amount + .ov-amount{margin-top:1px}
.ov-alt{display:block;color:var(--warn);font-size:0.75rem;font-weight:600}
.ov-none{font-size:1.0625rem;font-weight:750;color:var(--muted)}
.ov-cats{margin-top:12px;display:flex;flex-direction:column;gap:6px}
.ov-cat{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}
.ov-cat-label{color:var(--muted);font-size:0.75rem;font-weight:700;flex:none;min-width:5em}
.ov-tags{display:flex;gap:5px;flex-wrap:wrap}
.badge.held{background:var(--ok-soft);color:var(--ok-ink)}
/* 未有保障 is a fact, not an alarm: the muted treatment is deliberate, because
   a client with no 年金 is not in trouble and a red row would say they were. */
.badge.missing{background:transparent;color:var(--muted);border:1px dashed var(--line)}
.ov-caveats{margin:10px 0 0;color:var(--muted);font-size:0.75rem;line-height:1.5}
/* 類別 on a policy row: the one field 概覽 does arithmetic with, so it reads
   before 基本／儲蓄 and is never simply absent. */
.type-tag.cat{background:var(--ok-soft);color:var(--ok-ink)}
.ov-cat-note{color:var(--warn);font-size:0.75rem}
/* The reason a total is smaller than the rows above it, said at the total. */
.ov-fig-note{color:var(--warn);font-size:0.75rem;font-weight:600;margin-top:2px}

/* The colour IS the label. Naming it as well ("玫紅", "琥珀") spent a column on
   a word nobody reads once they can see the swatch — but the name still has to
   exist for a screen reader, and for anyone who cannot tell two of these apart,
   so it lives in title/aria-label rather than on screen. */
/* ------------------------------------------------------------- the column -- */

/* The cell is the control. A button rather than a click handler on a <td> so it
   is reachable by keyboard and announces itself, and stopPropagation in the
   handler because the row underneath opens the client. */
.hh-cell{white-space:nowrap}
.hh-pick{display:inline-flex;align-items:center;gap:6px;max-width:180px;
  padding:2px 8px;border-radius:6px;border:1px solid transparent;cursor:pointer;
  font-size:0.75rem;font-weight:700;background:var(--hh-bg);color:var(--hh-ink);
  min-height:32px}
/* 32px, not 44: these sit in a dense comparison grid where every cell is a
   picker, and a 44px floor would push the rows apart until the grid stops
   reading as a grid. 32px clears WCAG 2.5.8's AA floor of 24px, and it is
   50% taller than the 21px it measured. The trade is stated rather than
   silently made — if the grid is ever re-laid-out, revisit it. */
.hh-pick .hh-pick-name{overflow:hidden;text-overflow:ellipsis}
.hh-pick.hh-none{background:transparent;color:var(--muted);border-color:var(--line);font-weight:600}
.hh-pick:hover{border-color:var(--hh-mark)}
.hh-pick:focus-visible{outline:2px solid var(--brand);outline-offset:2px}

/* One picker, positioned over whichever cell asked for it. */
.hh-pop{position:absolute;z-index:60;min-width:260px;max-width:300px;
  background:var(--panel);border:1px solid var(--line);border-radius:11px;
  box-shadow:var(--shadow);padding:10px;display:flex;flex-direction:column;gap:6px}
.hh-pop-head{display:flex;flex-direction:column;gap:6px}
.hh-pop-rule{height:1px;background:var(--line);margin:2px 0}
.hh-pop-row{display:flex;align-items:center;gap:8px;justify-content:flex-start;
  padding:3px 4px;border-radius:6px;font-size:1rem}
/* A row is a .btn.link too, and that rule outweighs the one above and centres
   its label. These are a list — swatch, then name — so they keep the left edge,
   said at the weight that wins. */
.btn.link.hh-pop-row{justify-content:flex-start}
.hh-pop-row:hover{background:var(--hh-bg)}
.hh-pop-row .hh-swatch{background:var(--hh-mark)}
.hh-pop-new{display:flex;flex-direction:column;gap:6px}
.hh-pop .formerr{margin:2px 0 0}

.hh-dots{display:flex;gap:3px;flex:0 0 auto}
.hh-dot{width:17px;height:17px;padding:0;border-radius:5px;cursor:pointer;
  background:var(--hh-mark);border:1px solid rgba(0,0,0,.12)}
@media (prefers-color-scheme:dark){.hh-dot{border-color:rgba(255,255,255,.16)}}
.hh-dot[aria-pressed="true"]{box-shadow:0 0 0 2px var(--panel),0 0 0 4px var(--ink)}
.hh-dot:focus-visible{outline:2px solid var(--brand);outline-offset:2px}

/* ------------------------------------------------- the review, in the list -- */

/* The name is a control now. It has to keep looking like the name — an agent
   scans this column, and a column of buttons reads as a toolbar. Which is also
   why the tap target is bought with padding and a min-height rather than by
   drawing a box: measured 42x21 before, and this is how a record gets opened. */
.name-toggle{background:none;border:0;padding:0;font:inherit;color:inherit;
  font-weight:700;cursor:pointer;text-align:left;
  min-height:44px;min-width:44px;display:inline-flex;align-items:center}
.name-toggle:hover{text-decoration:underline}
.name-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
.name-toggle[aria-expanded="true"]{text-decoration:underline}

.review-row > td{background:var(--bg);padding:14px 16px}
.review-head{display:flex;align-items:center;justify-content:space-between;gap:12px;
  margin:0 0 10px}
.review-head h3{margin:0;font-size:1.0625rem;text-transform:uppercase;letter-spacing:.04em;
  color:var(--muted)}
/* 新增保單 + 保單回顧,一組靠右。窄屏嗰陣兩個一齊落行,唔會拆開。 */
.review-actions{display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end}
.review-plans{list-style:none;margin:10px 0 0;padding:0;display:flex;
  flex-direction:column;gap:4px}
.review-plans li{display:flex;align-items:center;gap:8px;font-size:1rem}
.review-plans .review-plan{font-weight:600}
.review-plans .review-amt{margin-left:auto;color:var(--muted);white-space:nowrap}

/* =================================================================== 保單回顧 */

/* The document an agent hands a client. Print is the primary medium — an A4
   sheet across a table — so the screen view is the print view, and the only
   things that change are the chrome that would be meaningless on paper. */

.review-sheet{max-width:820px;margin:0 auto;background:var(--panel);
  border:1px solid var(--line);border-radius:14px;padding:34px 38px 26px;
  box-shadow:var(--shadow)}

.rv-head{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;
  padding-bottom:18px;border-bottom:2px solid var(--ink)}
.rv-kicker{font-size:0.75rem;font-weight:700;letter-spacing:.18em;text-transform:uppercase;
  color:var(--brand)}
.rv-client{margin:4px 0 0;font-size:1.5625rem;line-height:1.15;letter-spacing:-.01em}
.rv-by{text-align:right;font-size:0.875rem;color:var(--muted);line-height:1.6}
.rv-agency{font-weight:700;color:var(--ink)}
.rv-date{margin-top:2px}

/* The answer, before anything else. Four figures, sized so the eye lands on the
   number rather than the label — the reverse of the table this replaces, where
   the totals were the last row of the last block. */
/* TWO BY TWO, not four across, and the number is the biggest thing on the page.
   At four columns each cell was 154px of usable width and the figure had to stay
   at 19px to fit — smaller than the client's own name above it, on a document
   whose entire purpose is those figures. Two columns give each number room to be
   read from across a table, which is where this page is actually used. */
.rv-figs{display:grid;grid-template-columns:repeat(2,1fr);gap:2px;margin:22px 0 0;
  background:var(--line);border:1px solid var(--line);border-radius:10px;overflow:hidden}
.rv-fig{background:var(--panel);padding:15px 18px}
.rv-fig-label{font-size:0.75rem;font-weight:700;color:var(--muted);letter-spacing:.02em}
.rv-fig-value{font-size:1.75rem;font-weight:700;margin-top:3px;line-height:1.25;
  font-variant-numeric:tabular-nums}
/* One currency per line. They are never converted into each other, so they are
   never run together into one string either — a middot join broke mid-number
   the moment two currencies did not fit. */
.rv-fig-amt{display:block}
.rv-fig-amt + .rv-fig-amt{font-size:.82em;color:var(--muted)}
.rv-fig-extra{font-size:0.75rem;color:var(--muted);margin-top:3px}

.review-sheet h2{font-size:0.75rem;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted);margin:28px 0 10px}

/* Held and missing in one picture. Missing is stated, not alarmed: a gap is a
   conversation, and eight red badges make the whole page read as a warning. */
.rv-cats{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
.rv-cat{border:1px solid var(--line);border-radius:8px;padding:9px 11px}
.rv-cat-name{font-size:1rem;font-weight:700}
.rv-cat-state{font-size:0.875rem;margin-top:3px;font-variant-numeric:tabular-nums}
.rv-cat.has{border-color:var(--ok);background:var(--ok-soft)}
.rv-cat.has .rv-cat-state{color:var(--ok-ink)}
.rv-cat.lacks{background:var(--bg)}
.rv-cat.lacks .rv-cat-name,.rv-cat.lacks .rv-cat-state{color:var(--muted)}
/* Neither covered nor a gap. Dashed, so it reads as a question the book has
   not answered rather than an answer of 'no'. */
.rv-cat.unsure{background:var(--bg);border-style:dashed}
.rv-cat.unsure .rv-cat-name,.rv-cat.unsure .rv-cat-state{color:var(--muted)}
.rv-note{font-size:0.875rem;color:var(--muted);margin:8px 0 0}

/* One row per policy. The table it replaces split each one across two grids and
   left the reader matching them by position. */
/* FIXED LAYOUT, and this is the fix rather than a preference. With the default
   auto layout the first cell holds 基本資料 — 504-6772397 · 生效 2014年11月29日 ·
   保障 終身 · 繳款 20 年 · 受保人 … — and the column grows to fit it, pushing 保費
   and 狀況 past the right edge of the sheet. On screen they were clipped; printed,
   they were gone. Fixed layout makes the widths the ones declared here and the
   long line wraps inside its cell, which is what it should have been doing. */
.rv-table{width:100%;table-layout:fixed;border-collapse:collapse;font-size:0.8125rem}
/* Reserve six Chinese characters plus padding; the plan absorbs the remainder. */
.rv-table col.c-plan{width:auto}
.rv-table col.c-insurer{width:calc(6.5em + 16px)}
.rv-table col.c-cat{width:9%}
.rv-table col.c-sav{width:7%}
.rv-table col.c-sum{width:14%}
.rv-table col.c-prem{width:12%}
.rv-table col.c-status{width:8%}
/* AND IT HAS TO BE ALLOWED TO WRAP. The app-wide `th,td{white-space:nowrap}`
   reaches in here and beat the overflow-wrap above it, so the 基本資料 line did
   not wrap at all — it simply happened to fit at 820px, and the comment above
   describing it as wrapping was describing something that was not happening.
   Off a wide screen it ran straight out of the cell. Money keeps nowrap: a
   figure broken across two lines is worse than a narrow column. */
.rv-table td{overflow-wrap:anywhere;white-space:normal}
.rv-table td.num,.rv-table th.num{white-space:nowrap}
.rv-table th{text-align:left;font-size:0.875rem;font-weight:700;color:var(--muted);
  text-transform:uppercase;letter-spacing:.04em;padding:0 8px 6px;
  border-bottom:1px solid var(--line)}
.rv-table th.num,.rv-table td.num{text-align:right;font-variant-numeric:tabular-nums}
.rv-table td{padding:9px 8px;border-bottom:1px solid var(--line);vertical-align:top}
.rv-plan{font-weight:600;display:block}
.rv-flag{display:inline-block;margin-top:3px;font-size:0.75rem;font-weight:700;
  padding:1px 6px;border-radius:5px;background:var(--warn-soft);color:var(--warn)}
/* A rider is not a policy of its own; it hangs off the plan above it. */
.rv-rider td:first-child{padding-left:22px;position:relative}
.rv-rider td:first-child::before{content:"└";position:absolute;left:8px;
  color:var(--muted);font-size:0.875rem}
.rv-rider .rv-plan{font-weight:500}

.rv-notes ul{margin:0;padding-left:18px;font-size:1rem;line-height:1.75}
.rv-foot{margin-top:26px;padding-top:12px;border-top:1px solid var(--line);
  font-size:0.8125rem;line-height:1.6;color:var(--muted)}

/* A PHONE IS WHERE HALF OF THESE ARE READ. There was no narrow layout at all:
   two four-column grids and a seven-column fixed table on a 390px screen, where
   「HKD 1,050,000」 broke across two lines and the last three columns of the
   table were unreachable. */
@media (max-width:760px){
  .review-sheet{padding:20px 16px 18px;border-radius:10px}
  .rv-head{flex-direction:column;gap:8px}
  .rv-by{text-align:left}
  .rv-cats{grid-template-columns:repeat(2,1fr)}

  /* One card per policy. A seven-column table cannot be shrunk into 390px, and
     scrolling it sideways hides the money — which is the column the client is
     looking for. Each value keeps the label its column header carried, so the
     card reads as the same document rather than a list of bare values. */
  .rv-table,.rv-table tbody,.rv-table tr,.rv-table td{display:block;width:auto}
  .rv-table thead,.rv-table colgroup{display:none}
  .rv-table tr{border:1px solid var(--line);border-radius:9px;
    padding:11px 13px;margin-bottom:9px}
  .rv-table td{border:0;padding:3px 0}
  .rv-table td[data-label]{display:flex;justify-content:space-between;gap:14px}
  .rv-table td[data-label]::before{content:attr(data-label);color:var(--muted);
    font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;
    flex:none;padding-top:2px}
  .rv-table td.num{text-align:right}
  .rv-rider{margin-left:14px}
  .rv-rider td:first-child{padding-left:0}
  .rv-rider td:first-child::before{content:none}
}
@media (max-width:560px){
  .rv-figs{grid-template-columns:1fr}
  .rv-cats{grid-template-columns:1fr}
  /* 150px + 118px of minimums plus the gaps left 17px for the plan name. */
  .rv-exp-line{flex-wrap:wrap;gap:2px 12px}
  .rv-exp-when{min-width:0;flex:1 1 100%}
  .rv-exp-what{flex:1 1 auto}
  .rv-exp-sum{min-width:0}
}

@media print{
  .review-sheet{max-width:none;border:0;border-radius:0;box-shadow:none;padding:0}
  .rv-figs{break-inside:avoid}
  .rv-cats{break-inside:avoid}
  .rv-table thead{display:table-header-group}
  .rv-table tr{break-inside:avoid}
  .rv-notes,.rv-foot{break-inside:avoid}
}

/* Category colour. The reference export uses a coloured icon per type down the
   left rail, which is the one thing about it that genuinely helps: an agent
   flipping between two clients recognises 危疾 by colour before reading it.
   Muted tints rather than saturated ones — eight full-strength colours on one
   page compete with the figures, which are what the page is for. */
.cat-life{--c:#1d4ed8;--c-soft:#e6edfd}
.cat-critical_illness{--c:#b83280;--c-soft:#fdeaf4}
.cat-medical{--c:#0e7490;--c-soft:#e0f2f7}
.cat-accident{--c:#8a5a12;--c-soft:#fdf3e0}
.cat-savings{--c:#4d7c0f;--c-soft:#eef7e0}
.cat-annuity{--c:#6d28d9;--c-soft:#f0e9fd}
.cat-ilas{--c:#0f766e;--c-soft:#e0f2f1}
.cat-other{--c:#475569;--c-soft:#eef1f5}
@media (prefers-color-scheme:dark){
  .cat-life{--c:#7aa2f7;--c-soft:#141e3a}
  .cat-critical_illness{--c:#f07ab8;--c-soft:#3a1229}
  .cat-medical{--c:#22b8cf;--c-soft:#123039}
  .cat-accident{--c:#d6a24a;--c-soft:#33280f}
  .cat-savings{--c:#9ccc65;--c-soft:#1d2b12}
  .cat-annuity{--c:#b39ddb;--c-soft:#241a3a}
  .cat-ilas{--c:#4db6ac;--c-soft:#10302d}
  .cat-other{--c:#9fb0c4;--c-soft:#1b232d}
}

/* PRINT TAKES THE LIGHT VALUES, WHATEVER THE SCREEN IS SET TO.
 *
 * The dark block above leaves --c-soft dark, and the print :root sets
 * --ink:#111. A held cell paints --c-soft behind --ink, so a client whose
 * agent runs a dark desktop and ticks "Background graphics" gets black on
 * near-black — a contrast ratio of 1.15:1 on 人壽, which is to say invisible.
 *
 * IT DOES NOT PRINT THAT WAY TODAY, and that is exactly why the order of these
 * two things matters. print-color-adjust is set nowhere in this repo, so
 * browsers drop the backgrounds by default and the defect only appears when a
 * reader opts into them. Adding print-color-adjust:exact FIRST — which is the
 * obvious-looking fix, and was the one written down — would turn an occasional
 * defect into an unconditional one. So the tokens are neutralised here, and any
 * later decision to force backgrounds through starts from a page that survives
 * it. */
@media print{
  .cat-life{--c:#1d4ed8;--c-soft:#e6edfd}
  .cat-critical_illness{--c:#b83280;--c-soft:#fdeaf4}
  .cat-medical{--c:#0e7490;--c-soft:#e0f2f7}
  .cat-accident{--c:#8a5a12;--c-soft:#fdf3e0}
  .cat-savings{--c:#4d7c0f;--c-soft:#eef7e0}
  .cat-annuity{--c:#6d28d9;--c-soft:#f0e9fd}
  .cat-ilas{--c:#0f766e;--c-soft:#e0f2f1}
  .cat-other{--c:#475569;--c-soft:#eef1f5}
}

/* Held cells carry their category's colour; gaps stay grey, because eight
   colours AND eight reds is a page that shouts. */
.rv-cat.has{border-color:var(--c);background:var(--c-soft)}
.rv-cat.has .rv-cat-name{color:var(--c)}
.rv-cat.has .rv-cat-state{color:var(--ink)}
.rv-cat-detail{font-size:0.875rem;color:var(--c);margin-top:2px}

.rv-chip{display:inline-block;font-size:0.75rem;font-weight:700;padding:1px 7px;
  border-radius:5px;background:var(--c-soft);color:var(--c);white-space:nowrap}
.rv-sav{color:var(--muted);font-size:0.78125rem}
.rv-sav.is{color:var(--ok-ink);font-weight:700}

/* 基本資料 on the same row as the money, because it is the same policy. The
   reference export gives it a table of its own and leaves the reader matching
   two grids by position. */
.rv-meta{display:block;font-size:0.75rem;color:var(--muted);margin-top:2px;line-height:1.5}
.rv-note-line{display:block;font-size:0.75rem;color:var(--muted);margin-top:3px;
  padding-left:8px;border-left:2px solid var(--line);line-height:1.5}

/* Cover with a leaving date. Set apart from the grid above it because it says
   something the figures there cannot: that a total shown as HKD 1,000,000 today
   is not HKD 1,000,000 for ever. */
.rv-expiry{margin-top:6px}
.rv-exp-line{display:flex;align-items:baseline;gap:14px;padding:6px 0;
  border-bottom:1px solid var(--line)}
.rv-exp-line:first-of-type{border-top:1px solid var(--line)}
.rv-exp-when{flex:0 0 auto;min-width:150px}
.rv-exp-date{font-weight:700;font-size:0.875rem;font-variant-numeric:tabular-nums}
.rv-exp-in{font-size:0.8125rem;color:var(--muted);margin-left:8px;white-space:nowrap}
.rv-exp-in.soon{color:var(--warn);font-weight:700}
.rv-exp-what{flex:1 1 auto;min-width:0;display:flex;align-items:baseline;gap:8px}
.rv-exp-plan{font-size:1rem;font-weight:600}
.rv-exp-sum{flex:0 0 auto;font-size:0.875rem;font-weight:700;text-align:right;
  font-variant-numeric:tabular-nums;min-width:118px}
@media print{ .rv-expiry{break-inside:avoid} }

/* ======================================================== 加保機會 ======= */
/* A WORKLIST, NOT A DOCUMENT. Denser than the review, and it leads with the
   unflattering column — the whole page exists to find the client whose cover is
   worth less than their own paperwork says. */
.opp-table{width:100%;border-collapse:collapse;font-size:0.84375rem;margin-top:4px}
.opp-table th{text-align:left;font-size:0.75rem;font-weight:700;color:var(--muted);
  text-transform:uppercase;letter-spacing:.04em;padding:0 10px 7px;
  border-bottom:1px solid var(--ink);white-space:nowrap}
/* The name column takes the slack so the four figure columns hug the right and
   sit near each other — a row is read across, and 400px of nothing between two
   numbers is 400px the eye has to cross to compare them. */
.opp-table th:first-child,.opp-table td:first-child{width:100%}
.opp-table th:not(:first-child),.opp-table td:not(:first-child){white-space:nowrap}
.opp-table th.num,.opp-table td.num{text-align:right;font-variant-numeric:tabular-nums}
.opp-table td{padding:10px;border-bottom:1px solid var(--line);vertical-align:top;
  white-space:normal;overflow-wrap:anywhere}
.opp-name{font-weight:600}
/* The gap is the number the page is for, so it is the one that carries weight.
   Not coloured red: a gap is a conversation, not an alarm — the same reason the
   review's 未有保障 cells are grey. */
.opp-gap{font-weight:700}
.opp-before{color:var(--muted)}
.opp-unsure{color:var(--warn);font-weight:700}
.opp-gap-amt{display:block}
.opp-flag{display:block;font-size:0.75rem;font-weight:400;color:var(--muted)}
.opp-foot{margin-top:18px;padding-top:12px;border-top:1px solid var(--line);
  font-size:0.75rem;line-height:1.7;color:var(--muted)}
.opp-foot p{margin:0 0 4px}

@media (max-width:760px){
  .opp-table,.opp-table tbody,.opp-table tr,.opp-table td{display:block;width:auto}
  .opp-table thead{display:none}
  .opp-table tr{border:1px solid var(--line);border-radius:9px;padding:11px 13px;margin-bottom:9px}
  .opp-table td{border:0;padding:3px 0}
  .opp-table td[data-label]{display:flex;justify-content:space-between;gap:14px}
  .opp-table td[data-label]::before{content:attr(data-label);color:var(--muted);
    font-size:0.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;flex:none}
  .opp-table td.num{text-align:right}
  .opp-table td:first-child,.opp-table td:not(:first-child){width:auto;white-space:normal}
}
.opp-stale{color:var(--warn);font-weight:600}

/* ======================================================== 保單體檢 ======= */
.chk-group{margin:22px 0 0}
.chk-group h3{font-size:0.875rem;font-weight:700;margin:0 0 3px}
.chk-why{font-size:0.75rem;color:var(--muted);margin:0 0 8px;max-width:70ch;line-height:1.6}
.chk-list{list-style:none;margin:0;padding:0}
.chk-list li{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;
  padding:7px 0;border-bottom:1px solid var(--line);font-size:0.84375rem}
.chk-who{font-weight:600;min-width:7em}
.chk-plan{color:var(--ink)}
.chk-detail{color:var(--muted);font-size:0.78125rem;margin-left:auto;
  font-variant-numeric:tabular-nums;white-space:nowrap}
/* A check that could not run is not a check that passed. */
.chk-degraded{color:var(--warn);font-weight:600;font-size:0.78125rem;margin:6px 0 0}

/* THE IMPORT PREVIEW'S "what is in the way" COLUMN HOLDS SENTENCES, and line 170
   sets white-space:nowrap on every td in the application — which is what makes
   the tables scan, and what silently turns an explanatory cell into one very
   long line that pushes the table off the side of its own scroll box. .kv td
   already carries the same escape hatch for the same reason. This bug has been
   shipped once before, in the plan finder's .rv-table. */
.imp-why{white-space:normal;overflow-wrap:anywhere;min-width:26ch}
.imp-name{white-space:normal;overflow-wrap:anywhere;max-width:24ch;font-weight:600}
.imp-map{margin-top:18px}

/* ---- 匯入保單檔案 ---------------------------------------------------------
   一版嘢由上而下係:邊幾個檔 → 讀到邊幾個人 → 每個人下面,如果入會開乜。
   所以「一個人」要圍得住 —— 唔圍嘅話,一個經紀捲到中間就唔知手上呢張保單
   係邊個嘅,而呢一版全部嘅風險就係「入錯咗落第二個人度」。 */
/* 三嚿嘢由上而下:邊幾個檔 → 總數 → 邊幾個人。冇呢句,個標題就會黐住上面
   嗰嚿,睇落似係上面嗰嚿嘅一部分。 */
/* ⚠️ `.card h3` 係 (0,1,1),所以一個淨係得一個 class 嘅規則贏唔到佢個
   margin:0 —— 第一版寫成 .fil-head 完全冇效,而張圖同改之前一個 byte 都冇差。 */
.card .fil-head{margin-top:20px}
/* 使用說明。圍住佢,係因為佢要睇落似一塊釘喺版頂嘅嘢,唔係開場白嘅第二段 ——
   一段同 .note 一模一樣嘅字,個代理捲過去嗰下就當咗係介紹,而入面有一句係
   佢剔之前一定要讀嘅。max-width 係為咗讀得順:一行過咗 ~40 個中文字,眼就
   會跳行。 */
.fil-how{border:1px solid var(--line);border-radius:12px;padding:10px 14px 12px;margin:14px 0;
  max-width:44em;font-size:0.78125rem;line-height:1.6;color:var(--ink)}
.card .fil-how-head{margin:2px 0 6px;font-size:0.875rem}
.fil-how p{margin:6px 0 0}
/* 最後嗰句同 .fil-gap 一樣係 --warn:佢唔係一句補充,佢係「呢一版嘅配對企喺
   乜嘢之上」—— 同逐行嗰幾句警告係同一件事,所以要同一個顏色。 */
.fil-how-guess{color:var(--warn);font-weight:500}
.fil-person{border:1px solid var(--line);border-radius:12px;padding:12px 14px;margin:14px 0}
.fil-who{font-size:0.9375rem;margin:0 0 2px;overflow-wrap:anywhere}
/* 一件「剔咗就入」嘅嘢,再喺人入面圍多一層:一個人可以有強積金 + 幾份保單,
   而剔嘅係其中一件,唔係成個人。 */
.fil-item{border-left:3px solid var(--line);padding:2px 0 2px 12px;margin:10px 0}
.fil-pick{font-weight:600}
.fil-sub{font-size:0.78125rem;color:var(--muted);margin:8px 0 2px}
.fil-cands{margin:4px 0 0;padding-left:20px;font-size:0.78125rem;color:var(--ink);line-height:1.6}
.fil-cands li{overflow-wrap:anywhere}
/* 一句「入之前要望多眼」:呢格讀唔到、呢個配對企喺乜之上、受保人係另一個人。
   同 .chk-degraded 一樣係 --warn —— 三句都唔係補充說明,而係入落去之前要有人
   睇過嘅嘢。 */
.fil-gap{color:var(--warn);font-size:0.78125rem;margin:6px 0 0;line-height:1.55;overflow-wrap:anywhere}
/* 同 .imp-why 一模一樣嘅理由:line 170 將成個 app 嘅 td 設成 nowrap,而呢欄
   入面係計劃名同一句句子。 */
.fil-why{white-space:normal;overflow-wrap:anywhere}
/* 揀檔案嗰個掣係瀏覽器自己畫嘅,唔喺共用嗰條欄位規則入面 —— 唔寫,佢就係
   一個十幾 px 高嘅細掣,喺電話上面撳極都唔中。 */
input[type=file]{font-size:1rem;min-height:44px;padding:8px 0;max-width:100%}
/* 「寫入邊度」。.f 係一格 grid 入面嘅欄位,而呢度唔係 grid —— 唔寫呢句,佢個
   span 冇人理,結果係一個貼住邊嘅 <select>。擺喺一個人嗰嚿嘅最底,同「客戶簿:
   有幾個似」嗰句貼住:嗰句係問題,呢個係答案。 */
.card .fil-destwrap{margin:10px 0 0;max-width:34em}
.fil-dest{max-width:100%}
/* 個「寫入」掣自己一行。同啲剔隔開,因為佢係最後嗰下 —— 貼住上面嗰格剔,就
   會有人以為佢係嗰格剔嘅一部分。 */
.fil-bar{margin:12px 0 0}
/* 寫完之後逐件講。一件一行:乜嘢 + 點樣,窄嘅時候自己斷落下一行,因為一個
   計劃名加一句「入唔到:…」喺 390px 度一定放唔低一行。 */
.fil-res{list-style:none;margin:10px 0 0;padding:0}
.fil-res li{display:flex;flex-wrap:wrap;gap:2px 10px;padding:6px 0;border-top:1px solid var(--line);
  font-size:0.78125rem;line-height:1.55}
.fil-res li.bad{color:var(--bad)}
.fil-res-what{font-weight:600;overflow-wrap:anywhere}
.fil-res-how{color:var(--muted)}
.fil-res li.bad .fil-res-how{color:var(--bad)}
/* 差異嗰幾格上面嗰句。同 .fil-kept 一樣佔一整行,但係淺色 —— 佢係一個標題,
   唔係一個要人撳嘅決定,兩者用同一個顏色就分唔出邊句要做嘢。 */
.fil-kept-head{flex-basis:100%;margin:6px 0 0;font-size:0.78125rem;color:var(--muted);
  line-height:1.55;overflow-wrap:anywhere}
/* 一格差異。佢要自己佔一整行 —— 佢係一個要人撳嘅決定,唔係上面嗰行嘅補充。 */
.fil-kept{flex-basis:100%;align-items:flex-start;font-size:0.78125rem;font-weight:500;
  color:var(--warn);overflow-wrap:anywhere}
.fil-progress{margin:8px 0 0}
@media (max-width:560px){
  .chk-list li{gap:2px 10px}
  .chk-detail{margin-left:0;flex-basis:100%;white-space:normal}
}

/* ---- 保障缺口 -------------------------------------------------------------
   The table is six columns before the network split and eight after, which is
   wider than a phone and wider than some laptops. It scrolls inside its own
   box rather than widening the page: a document whose body scrolls sideways
   loses its left edge, and the left edge here is the operation's name. */
.tablewrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
.rv-gap-tier{font-size:0.8125rem;margin:14px 0 4px;color:var(--muted)}
.rv-gap-table{table-layout:auto}
.rv-gap-table th{white-space:nowrap}
.rv-gap-table td:first-child,.rv-gap-table th:first-child{white-space:normal;min-width:9em}
.rv-gap-detail>td{padding:0;border-top:0}
.rv-gap-more{margin:0 0 10px;font-size:0.8125rem}
.rv-gap-more>summary{cursor:pointer;color:var(--muted);padding:4px 0}
.rv-gap-more>summary:hover{color:var(--ink)}
.rv-gap-more .rv-table{font-size:0.75rem;table-layout:auto}
.rv-gap-assume{margin:6px 0 0;padding-left:18px;color:var(--muted);font-size:0.75rem}
.rv-gap-intro,.rv-gap-note{color:var(--muted);font-size:0.75rem;margin:4px 0}

@media print{
  /* Each plan's gap starts its own page: it is the part of the review a client
     reads on its own, and a table split across a fold is one nobody adds up. */
  .rv-gap{page-break-before:always}
  /* The per-hospital disclosure is a screen affordance. On paper a collapsed
     <details> prints as a row of nothing, so it is left out entirely and the
     bad-case column carries the same point instead. */
  .rv-gap-more{display:none}
  .tablewrap{overflow:visible}
}

/* Below the shared .f.md width rule so the phone override wins in the real
   cascade. The sticky header wraps to ~237px at 390px, hence the larger
   scroll margin when the calculator is opened from its button. */
@media (max-width:560px){
  .finance{scroll-margin-top:270px}
  .finance-inputs .f.md{grid-column-end:span 12}
}

/* Expandable calculation explanations use the same type and focus colours as
   the form. Keep the whole summary a 44px target, including on touch devices. */
.finance-breakdown details{border-top:1px solid var(--line);padding:4px 0}
.finance-breakdown summary{min-height:44px;padding:10px 8px;cursor:pointer}
.finance-breakdown summary:focus-visible{outline:2px solid var(--brand-ink);outline-offset:2px}
.finance-breakdown p{overflow-wrap:anywhere;margin:8px}

/* Standalone financing workspace; saved names never appear outside its view. */
.finance-versions{margin:1.2rem 0;padding:1rem;border:1px solid var(--line);border-radius:12px}
.finance-versions summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}
.finance-actions{display:flex;flex-wrap:wrap;align-items:center;gap:12px;margin:16px 0}
.finance-actions .hint{flex:1 1 16rem}
.finance-comparison{margin:20px 0;min-width:0}
.finance-comparison:empty{display:none}
.finance-curve{margin:20px 0;min-width:0}
.fin-rate-path{fill:none;stroke:var(--brand-ink);stroke-width:3}
.fin-rate-point{fill:transparent;stroke:transparent;cursor:pointer}
.fin-rate-dot{fill:var(--brand-ink)}
.fin-rate-point:focus{outline:none;stroke:var(--brand-ink);stroke-width:2}
.fin-report-table{width:100%;border-collapse:collapse}
.fin-report-table th,.fin-report-table td{padding:8px;text-align:left;border-bottom:1px solid var(--line)}
@media print{
  @page financing{size:A4 landscape;margin:12mm}
  html.financing-print,body.financing-print,body.financing-print #main,body.financing-print #viewFinancing{page:financing}
  body.financing-print > :not(main){display:none!important}
  body.financing-print #financePanel{display:none!important}
  body.financing-print .financing-report{display:block!important;page:financing;color:#18212b;background:#fff;font-size:10pt}
  .financing-report h1{font-size:20pt}.financing-report h2{font-size:14pt;break-after:avoid}
  .financing-report h3{font-size:12pt;break-after:avoid}
  .financing-report p{font-size:10pt;line-height:1.5}
  .financing-report .table-wrap{overflow:visible;border:0;box-shadow:none}
  .financing-report table{width:100%;min-width:0;table-layout:auto;font-size:9pt}
  .financing-report th,.financing-report td{white-space:normal;overflow-wrap:anywhere;padding:5px}
  .financing-report thead{display:table-header-group}
  .fin-print-chart{break-inside:avoid;margin:12px 0}
  .fin-print-chart svg{display:block;width:100%;height:auto;max-height:85mm}
  .financing-report .fin-rate-point{fill:var(--brand-ink)}
  .financing-report{-webkit-print-color-adjust:exact;print-color-adjust:exact}
}

.finance-versions summary::before{content:'▸';margin-right:8px}
.finance-versions[open] summary::before{content:'▾'}
.finance-versions summary:focus-visible{outline:2px solid var(--brand-ink);outline-offset:2px}

/* Each benchmark keeps its base and spread together, even at narrow widths. */
.finance-rate-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 14px;margin-top:12px}
.finance-rate-row .f.md{grid-column:auto;min-width:0}
/* Short Chinese brands move together; long hand-entered names still fit the cell. */
.rv-insurer-name{display:block;min-width:0;max-width:100%}
.rv-insurer-zh{display:inline-block;white-space:nowrap;overflow-wrap:normal}
/* Locale-independent typed dates, with the native calendar still available. */
.client-date-input{display:flex;gap:8px;min-width:0}
.client-date-input>input:first-child{flex:1;min-width:0;width:0}
.client-date-input>.client-date-picker{flex:0 0 44px;width:44px;min-width:44px;min-height:44px;padding:8px;color:transparent}
.client-date-picker::-webkit-datetime-edit{display:none}
.client-date-picker::-webkit-calendar-picker-indicator{margin:0;padding:4px;opacity:1}
@media (max-width:560px){
  .grid>.client-date{grid-column:span 12}
}
