/* /css/tokens.css */
/* Design tokens: the only place colours, type and spacing are defined
   (context/brand-guide.md). Dark is the default; light overrides below. */
:root {
  --red: #d90202;
  --maroon: #390707;
  --brand-deep: #4f0000;
  --bg: #121212;
  --surface: #1a1a1a;
  --surface-2: #292929;
  --line: #3d3d3d;
  --text: #f2f2f2;
  --text-muted: #9a9a9a;
  --link: #bbc2fc;
  --text-dim: #9a9a9a;
  --on-red: #ffffff;
  --glow: rgb(217 2 2 / 0.85);
  --line-active: rgb(57 7 7 / 0.55);
  --scrim: rgb(0 0 0 / 0.55);
  /* Areas are told apart by tone, space and shadow, not by rules (spec 2026-10-21-shell-redesign). */
  --hover: rgb(255 255 255 / 0.07);
  --shadow: 0 10px 30px rgb(0 0 0 / 0.45), 0 1px 3px rgb(0 0 0 / 0.35);
  --shadow-up: 0 -8px 24px rgb(0 0 0 / 0.35);
  --hero-glow: rgb(79 0 0 / 0.45);   /* behind the home page's opening lines */
  --ink: rgb(255 255 255 / 0.09);      /* the home page's ink-wash landscape (partials/landscape.php) */
  --ink-sun: rgb(217 2 2 / 0.4);
  /* Glass: the player, menus, the word card, dialogs and the chapter sheet float over the page,
     which shows through blurred. Opaque enough that text on it passes contrast over the page
     (tests/contrast-test.php: "glass over ..."). */
  --glass: rgb(38 38 38 / 0.78);
  --glass-edge: rgb(255 255 255 / 0.1);
  --glass-shine: inset 0 1px 0 rgb(255 255 255 / 0.06);
  --glass-blur: blur(24px) saturate(1.6);
  --card-tint: rgb(255 255 255 / 0.045);   /* settings cards on the glass sheet */
  --seg-bg: rgb(0 0 0 / 0.28);             /* segmented choice rows */
  --seg-hover: rgb(255 255 255 / 0.07);
  /* Controls are neutral: red is kept for the play button, the seek bar and the current line.
     A chosen segment, a pressed toggle and the main settings button are a raised chip; a switch
     that is on is a light track; the current chapter in the list is a tint with a red edge. */
  --seg-on: rgb(255 255 255 / 0.17);
  --seg-on-shadow: 0 1px 2px rgb(0 0 0 / 0.45), inset 0 1px 0 rgb(255 255 255 / 0.06);
  --switch-on: #c8c8c8;
  --switch-knob: #1a1a1a;
  --current-bg: rgb(255 255 255 / 0.09);   /* the current chapter in the list */
  /* Red as text and as the focus ring: --red itself is under 4.5:1 for small text on the dark
     current-line tint and under 3:1 on --surface-2 (tests/contrast-test.php). */
  --red-text: #ff4d4d;
  --focus: #ff4d4d;   /* focus rings */
  /* The share-a-line picture (js/share.js) is always dark, like the Open Graph pictures:
     these are not redefined for the light theme. */
  --share-bg: #121212;
  --share-text: #f2f2f2;
  --share-muted: #9a9a9a;

  /* Chinese interface text (zh pages, the word card) in the system Chinese sans, not whatever
     sans-serif falls back to. */
  --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
  /* Chinese prose and interface text (summaries, commentary, glosses, notes, term intros, pages):
     the reader's own system fonts, so the Kai web font stays the size of the book's characters. */
  --font-zh-prose: "Songti SC", "STSong", "Noto Serif CJK SC", "Noto Serif SC", "Source Han Serif SC", "Source Han Serif CN", "Microsoft YaHei", serif;
  /* Latin in Source Serif; Chinese in running text falls through to the prose fonts. */
  --font-text: "DDJ Serif", Georgia, "Times New Roman", var(--font-zh-prose);
  /* The classical text only (chapter lines, word and term characters, chapter titles: ddj_kai_text()
     in src/assets.php lists it, and the subset holds just that). The DDJ Kai subset is simplified-only:
     traditional characters (phase 4 §3) fall through to the system Kai fonts, the traditional ones
     (Kaiti TC, BiauKai, DFKai-SB) included. */
  --font-zh: "DDJ Kai", "KaiTi", "STKaiti", "Kaiti SC", "Kaiti TC", "BiauKai", "DFKai-SB", serif;

  --step--1: 0.875rem;
  --step-0: 1rem;
  --step-1: 1.25rem;
  --step-2: 1.6rem;
  --step-3: 2rem;
  --zh-size: 1.6rem;
  /* Pinyin is a fixed share of the Chinese (0.36em) but never smaller than this, times the
     reader's text size: under it the tone marks are hard to read on a phone. Smaller Chinese
     (the full text, term pages) has a smaller floor, so the pinyin does not outgrow it. */
  --py-min: 0.6875rem;
  --py-min-small: 0.625rem;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2.5rem;
  --radius: 12px;
  --sheet-radius: 18px;
  --measure: 38rem;

  color-scheme: dark;
}

:root[data-theme="light"] {
  --bg: #faf8f5;
  --surface: #ffffff;
  --surface-2: #f0ece6;
  --line: #ddd6cc;
  --text: #1a1a1a;
  --text-muted: #5f5a54;
  --link: #8a0101;
  --text-dim: #6b655e;
  --glow: rgb(217 2 2 / 0.45);
  --line-active: rgb(217 2 2 / 0.08);
  --red-text: #b80000;
  --focus: var(--red);
  --hover: rgb(0 0 0 / 0.05);
  --shadow: 0 10px 30px rgb(60 40 20 / 0.14), 0 1px 3px rgb(60 40 20 / 0.12);
  --shadow-up: 0 -8px 24px rgb(60 40 20 / 0.10);
  --hero-glow: rgb(217 2 2 / 0.07);
  --ink: rgb(60 40 20 / 0.11);
  --ink-sun: rgb(217 2 2 / 0.3);
  --glass: rgb(255 255 255 / 0.72);
  --glass-edge: rgb(60 40 20 / 0.1);
  --glass-shine: inset 0 1px 0 rgb(255 255 255 / 0.8);
  --card-tint: rgb(255 255 255 / 0.55);
  --seg-bg: rgb(60 40 20 / 0.08);
  --seg-hover: rgb(60 40 20 / 0.07);
  --seg-on: #ffffff;
  --seg-on-shadow: 0 1px 3px rgb(60 40 20 / 0.2), 0 0 0 1px rgb(60 40 20 / 0.06);
  --switch-on: #5f5a54;
  --switch-knob: #ffffff;
  --current-bg: rgb(60 40 20 / 0.08);
  color-scheme: light;
}

/* /css/fonts.css */
/* Generated by local-scripts/fonts/subset.php. Do not edit by hand. */
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-a-d280ba8aee.woff2) format("woff2"); font-display: swap; unicode-range: U+3001-3002, U+4E00, U+4E03, U+4E07, U+4E09-4E0B, U+4E0D-4E0E, U+4E3A, U+4E45, U+4E4B, U+4E5D, U+4E5F, U+4E8B-4E8C, U+4E8E, U+4E94, U+4EBA, U+4EC1, U+4ECB, U+4EE4-4EE5, U+4F01, U+4F50, U+4F73, U+4F7F, U+4FE1, U+5165, U+5168, U+516B, U+516D-516E, U+5171, U+5175-5176, U+51B2, U+51FA, U+5219, U+52C7, U+5341, U+539A, U+53CD, U+53D6, U+53E4, U+53EF, U+540D, U+542B, U+543E, U+548C, U+5584, U+56DB, U+56FD, U+5723, U+5730, U+58EB, U+5927, U+5929-592B, U+59CB, U+5A01, U+5B54, U+5B66, U+5B70, U+5B88-5B89, U+5BA0, U+5BB9, U+5BE1, U+5C06, U+5C0F, U+5C1A, U+5E0C, U+5E38, U+5E9F, U+5EFA, U+5F03, U+5F97, U+5FB7, U+5FC5, U+5FE7, U+6028, U+60CA, U+6210-6211, U+6237, U+6240, U+6267, U+62B1, U+6301, U+653F, U+6562, U+65E0, U+65E5, U+6613-6614, U+667A, U+66F2, U+6709, U+6781, U+6789, U+67D4, U+6839, U+6B32, U+6B59, U+6B63, U+6B7B, U+6C11, U+6C34, U+6C5F, U+6CBB, U+6CDB, U+6D77, U+6DF7, U+7136, U+7269, U+751A, U+751F, U+7528, U+754F, U+7686, U+76C8, U+76CA, U+77E5, U+795E, U+7ACB, U+7AE0, U+7B2C, U+7ECF, U+7EDD, U+7F3A, U+7F8E, U+8005, U+800C, U+81EA, U+81F3-81F4, U+8272, U+82E5, U+83AB, U+8425, U+865A, U+884C, U+89C1, U+89C6, U+8A00, U+8C13, U+8C37, U+8C61, U+8D24, U+8EAB, U+8F7B, U+8F7D, U+8F90, U+8F99, U+8FB1, U+9053, U+91CD, U+957F, U+95F7, U+95FB, U+96C4, U+975E, U+9965, U+9B44, U+FF01, U+FF08-FF09, U+FF0C, U+FF1A-FF1B, U+FF1F; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-core-615c829e4e.woff2) format("woff2"); font-display: swap; unicode-range: U+4E14, U+4E24, U+4E27, U+4E2D, U+4E3B, U+4E43, U+4E49, U+4E4E, U+4E50, U+4E58, U+4E71, U+4E89, U+4E91, U+4EA1, U+4EA4, U+4EA6, U+4EB2, U+4ECA, U+4ECE, U+4F10, U+4F17, U+4F24, U+4F3C, U+4F55, U+4F59, U+4F5C, U+4FAF, U+4FD7, U+4FDD, U+513F, U+5146, U+5148-5149, U+5151, U+516C, U+519B, U+51E0, U+51F6, U+521A, U+5229, U+5236, U+524D, U+5272, U+529B, U+529F-52A0, U+52A8, U+52E4, U+5316, U+538C, U+53BB, U+53C8, U+53CA, U+53D1, U+53E3, U+53F0, U+53F3, U+53F8, U+5404, U+5408, U+540C, U+540E, U+541B, U+542C, U+5473, U+547D, U+548E, U+54C0, U+54C9, U+552F, U+5668, U+56FA, U+5728, U+575A, U+585E, U+58EE, U+58F0, U+5904, U+590D, U+591A, U+5931, U+5937, U+5947-5948, U+597D, U+5982, U+5999, U+59D3, U+5A74, U+5B50, U+5B58, U+5B5D, U+5B64, U+5B69, U+5B97, U+5B9D-5B9E, U+5BA2, U+5BB0, U+5BB3, U+5BB6, U+5BCC, U+5BDF, U+5C0A, U+5C11, U+5C18, U+5C45, U+5C48, U+5DDD, U+5DE6-5DE7, U+5DF2, U+5E08, U+5E7F, U+5E94, U+5F00, U+5F02, U+5F0F, U+5F20, U+5F25, U+5F31, U+5F3A, U+5F52, U+5F62, U+5F70, U+5F7C, U+5F80, U+5F92, U+5FAE, U+5FC3, U+5FD7, U+5FE0, U+6043, U+604D, U+6076, U+60DA, U+6108, U+611A, U+6148, U+6216, U+6218, U+6254, U+632B, U+635F, U+640F, U+6518, U+6545, U+654C, U+6551, U+6559, U+6563, U+6570, U+6587, U+65B0, U+65B9, U+65E2, U+65E9, U+660E-660F, U+6627, U+662F, U+66F0, U+670D, U+671D, U+6728, U+672A, U+672C, U+6734, U+6740, U+6765, U+6B62, U+6B64, U+6B86, U+6BCD, U+6C14, U+6C42, U+6CA1, U+6CD5, U+6DE1, U+6DF1, U+6E05, U+6E0A, U+7109, U+7231, U+7256, U+725D, U+7261, U+72B6, U+72B9, U+72EC, U+7384, U+7389, U+738B, U+7518, U+7532, U+755C, U+758F, U+75C5, U+767D-767E, U+76D6-76D7, U+76F4, U+76F8, U+771F, U+77DC, U+77E3, U+793C, U+7965, U+7978, U+798F, U+79BB, U+79C1, U+79EF, U+7A77, U+7CBE, U+7EC6, U+7EC8, U+7ED3, U+7EF3, U+8001, U+80DC, U+80FD, U+8106, U+8131, U+8179, U+81C2, U+81E3, U+8206, U+8363, U+867D, U+88AB, U+89C2, U+89E3, U+8BC6, U+8C0B, U+8D25, U+8D27, U+8D31, U+8D35, U+8D3C, U+8D77, U+8DB3, U+8E81, U+8F9E, U+8FA9, U+8FC7, U+8FDB-8FDC, U+8FF7, U+9000, U+9057, U+90AA, U+90BB, U+9510, U+95E8, U+95ED, U+95F4, U+9664, U+968F, U+96BE, U+96CC, U+9759, U+97F3, U+98DF, U+9996, U+9A6C, U+9A70, U+9A84, U+9A8B, U+9AA8, U+9AD8; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r01-212c6dfd61.woff2) format("woff2"); font-display: swap; unicode-range: U+503E, U+520D, U+5E1D, U+5F17, U+5FBC, U+65AF, U+6A50, U+6E5B, U+72D7, U+77ED, U+7C65, U+7EB7, U+8C01, U+8F83; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r02-7e98d70b69.woff2) format("woff2"); font-display: swap; unicode-range: U+4E13, U+5802, U+5916, U+5C24, U+63E3, U+65F6, U+6DA4, U+6EE1, U+75B5, U+7EF5, U+89C8, U+8FBE, U+91D1, U+9616; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r03-5ce5ab5e5d.woff2) format("woff2"); font-display: swap; unicode-range: U+4FE8, U+51AC, U+51B0, U+51FF, U+57CF, U+57F4, U+59A8, U+5BA4, U+5BC4, U+5F53, U+5F90, U+5FA1, U+60A3, U+6258, U+6566, U+65F7, U+6BC2, U+6D4A, U+6D89, U+6DA3, U+723D, U+72C2, U+730E, U+754B, U+76A6, U+76EE, U+76F2, U+7EAA, U+8033, U+804B, U+853D, U+8BD8, U+8C6B, U+8F66, U+8FCE, U+901A, U+91CA; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r04-1d2e6e1eef.woff2) format("woff2"); font-display: swap; unicode-range: U+4EAB, U+4F2A, U+4FAE, U+500D, U+513D, U+592E, U+5984, U+5C5E, U+5E76, U+60A0, U+6167, U+6625, U+662D, U+6B21, U+6C8C, U+6CCA, U+6FB9, U+7199, U+7262, U+767B, U+7B03, U+7D20, U+82B8, U+8352, U+8A89, U+9042, U+9119, U+963F, U+987D, U+98C2; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r05-bc17a49bbc.woff2) format("woff2"); font-display: swap; unicode-range: U+51A5, U+51B5, U+5468, U+57DF, U+5B57, U+5BC2, U+5BE5, U+5C82, U+60D1, U+60DF, U+6539, U+655D, U+6D3C, U+752B, U+7A88, U+8BDA, U+8D58, U+8DE8, U+901D, U+9605, U+96E8, U+98CE, U+98D8, U+9AA4; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r06-dab2212626.woff2) format("woff2"); font-display: swap; unicode-range: U+5173, U+52FF, U+5439, U+5962, U+5B98, U+5E74, U+5FD2, U+679C, U+68D8, U+6957, U+6B54, U+6CF0, U+6EAA, U+71D5, U+7455, U+7B56, U+7B79, U+7EA6, U+7FB8, U+8346, U+88AD, U+8981, U+8C2A, U+8D44, U+8D85, U+8F8E, U+8FD8, U+8FF9, U+96B3, U+9ED1; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r07-34f6bc7e39.woff2) format("woff2"); font-display: swap; unicode-range: U+504F, U+517B, U+5409, U+5747, U+5BBE, U+5BFF, U+5E73, U+606C, U+60B2, U+6CE3, U+8863, U+8B6C, U+964D, U+9732, U+9975; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r08-405d186684.woff2) format("woff2"); font-display: swap; unicode-range: U+4E08, U+5174, U+534E, U+57FA, U+593A, U+5B81, U+5B9A, U+6050, U+6B47, U+706D, U+7075, U+73DE, U+742D, U+77F3, U+793A, U+7AED, U+8584, U+88C2, U+8D1E, U+8E76, U+9547, U+9C7C; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r09-d242ad2408.woff2) format("woff2"); font-display: swap; unicode-range: U+5077, U+5BD2, U+5F0A, U+62D9, U+665A, U+6881, U+6E1D, U+70ED, U+7236, U+79F0, U+7B11, U+85CF, U+8BB7, U+8D1F, U+8D28, U+8D37, U+8D39, U+9633-9634, U+9685, U+9690, U+98A3; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r10-d4c2b217c2.woff2) format("woff2"); font-display: swap; unicode-range: U+5155, U+5203, U+5374, U+620E, U+6295, U+63AA, U+6444, U+6D51, U+722A, U+7AA5, U+7CAA, U+864E, U+89D2, U+8D70, U+9047, U+90CA, U+9646; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r11-03eea1423a.woff2) format("woff2"); font-display: swap; unicode-range: U+4E30, U+4E60-4E61, U+4EAD, U+4ED3, U+4FEE, U+517D, U+5251, U+52BF, U+53F7, U+55C4, U+5938, U+5B59, U+5E26, U+5F69, U+5F84, U+62D4, U+636E, U+63E1, U+652B, U+65BD, U+666E, U+6B83, U+6BD2, U+6BD4, U+6D4E, U+731B, U+7530, U+7940, U+796D, U+7B4B, U+80B2, U+829C, U+867A, U+867F, U+86C7, U+8702, U+87AB, U+8986, U+8D22, U+8D64, U+8F8D, U+996E, U+9E1F; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r12-b2b994fd40.woff2) format("woff2"); font-display: swap; unicode-range: U+4F0E-4F0F, U+501A, U+514B, U+5206, U+523F, U+556C, U+5996, U+5EC9, U+5FCC, U+67E2, U+6DF3, U+6ECB, U+70F9, U+71FF, U+8086, U+8385, U+8BB3, U+8D2B, U+9B3C, U+9C9C; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r13-225e61839e.woff2) format("woff2"); font-display: swap; unicode-range: U+514D, U+517C, U+5343, U+56FE, U+571F, U+5750, U+5965, U+5B9C, U+5C42, U+5E02, U+614E, U+62A5, U+62F1, U+672B, U+6BEB, U+6CEE, U+6D41, U+74A7, U+7A3D, U+7D2F, U+7F6A, U+7F6E, U+8BFA, U+8F85, U+91CC, U+987A, U+9A77; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r14-f2721aba0d.woff2) format("woff2"); font-display: swap; unicode-range: U+4FED, U+536B, U+5BF8, U+5C3A, U+6000, U+6012, U+6297, U+63A8, U+6B66, U+8096, U+820D, U+8910, U+914D; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r15-690551acc4.woff2) format("woff2"); font-display: swap; unicode-range: U+4EE3, U+5320, U+53EC, U+6062, U+60E7, U+624B, U+65AB, U+6D3B, U+72CE, U+7A0E, U+7E5F, U+7F51; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r16-9c5333470c.woff2) format("woff2"); font-display: swap; unicode-range: U+4E3E, U+4EC0, U+4F2F, U+53D7, U+57A2, U+5949, U+5951, U+5F13, U+5F7B, U+5F99, U+6291, U+653B, U+671B, U+67AF, U+69C1, U+72AC, U+793E, U+7A37, U+821F, U+8349, U+8865, U+8D23, U+9648, U+9E21; }
@font-face { font-family: "DDJ Kai"; src: url(/public/fonts/kai-r17-20f4b00577.woff2) format("woff2"); font-display: swap; unicode-range: U+535A, U+5DF1; }
@font-face { font-family: "DDJ Serif"; src: url(/public/fonts/serif-400-f0c5a36ad0.woff2) format("woff2"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "DDJ Serif"; src: url(/public/fonts/serif-600-3682c0bf2b.woff2) format("woff2"); font-weight: 600; font-style: normal; font-display: swap; }
@font-face { font-family: "DDJ Serif"; src: url(/public/fonts/serif-400i-7ec4be5490.woff2) format("woff2"); font-weight: 400; font-style: italic; font-display: swap; }

/* /css/base.css */
/* Base layout and typography, mobile first. Components get their own files
   under css/components/ as they arrive (plan 1b: player, settings). */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; background: var(--bg); color: var(--text); font: var(--step-0)/1.6 var(--font-text); }
a { color: var(--link); }
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
img { max-width: 100%; height: auto; }

.skip { position: absolute; left: -9999px; }
.skip:focus { left: var(--space-3); top: var(--space-3); z-index: 10; padding: var(--space-2) var(--space-3); background: var(--surface-2); }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

.site-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: var(--space-3); background: var(--surface); }
.brand { display: flex; align-items: center; gap: var(--space-2); color: var(--text); font-weight: 600; text-decoration: none; }
.brand img { flex: none; border-radius: 8px; }
/* The same in every language, so switching language never changes the header (spec 2026-10-21-shell-redesign §5). */
.brand-text { display: grid; line-height: 1.15; }
.brand-zh { font: 600 var(--step-0) var(--font-zh); letter-spacing: 0.08em; }
.brand-py { color: var(--text-muted); font: 400 0.8rem var(--font-text); letter-spacing: 0.02em; }

main { max-width: var(--measure); margin: 0 auto; padding: var(--space-4) var(--space-3) var(--space-5); }
h1 { margin: 0 0 var(--space-3); font-size: var(--step-2); line-height: 1.25; }
/* Chapter heading: the chapter in Chinese, then the book and number in the page language (no brackets). */
.h1-zh { display: block; font-family: var(--font-zh); font-size: var(--step-3); font-weight: 600; letter-spacing: 0.06em; }
.h1-sub { display: block; margin-top: var(--space-1); color: var(--text-muted); font: 600 var(--step-0) var(--font-text); letter-spacing: 0.01em; }
h2 { margin: var(--space-5) 0 var(--space-3); font-size: var(--step-1); }
.lead { color: var(--text-muted); font-size: var(--step-1); line-height: 1.5; }

.zh { margin: 0; font-family: var(--font-zh); font-size: calc(var(--zh-size) * var(--user-scale, 1)); line-height: 1.5; letter-spacing: 0.04em; }
.tr { margin: var(--space-2) 0 0; font-family: var(--font-text); font-size: calc(var(--step-1) * var(--user-scale, 1)); line-height: 1.5; }

.lines { margin: 0; padding: 0; list-style: none; }
.line { padding: var(--space-3) 0; border-bottom: 1px solid var(--line); }
.chapter-audio { width: 100%; margin: var(--space-3) 0; }
.commentary p { font-family: var(--font-text); }
/* Previous and next chapter at the end of a chapter: the way on for a reader who has finished it. */
.pager { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-2) var(--space-3); margin-top: var(--space-5); }
.pager a { display: grid; gap: 0.15rem; min-width: min(100%, 14rem); padding: var(--space-3) var(--space-4); border-radius: 12px; background: var(--card-tint); color: var(--text); text-decoration: none; }
.pager a:hover { background: var(--seg-hover); }
.pager a[rel="next"] { margin-left: auto; text-align: right; }
.pager-dir { color: var(--text-muted); font: 600 var(--step--1) var(--font-ui); }
.pager-zh { font-family: var(--font-zh); font-size: 1.3rem; letter-spacing: 0.04em; }
.pager-title { color: var(--text-muted); font: var(--step--1) var(--font-ui); }

.ft-chapter .zh { --py-min: var(--py-min-small); font-size: var(--step-1); }
.ft-chapter .tr { margin-bottom: var(--space-3); font-size: var(--step-0); }


.site-footer { padding: var(--space-4) var(--space-3); background: var(--surface); color: var(--text-muted); font: var(--step--1)/1.6 var(--font-ui); text-align: center; }
.site-footer nav { display: flex; flex-wrap: wrap; justify-content: center; gap: 0 var(--space-3); margin-bottom: var(--space-2); }
.site-footer nav a { padding: var(--space-1) 0; }   /* taller than 24px to tap */

@media (min-width: 48rem) {
  :root { --zh-size: 1.9rem; }
  main { padding-top: var(--space-5); }
  h1 { font-size: var(--step-3); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* /css/app.css */
/* App layout and player (spec 2026-10-20-player-ux §3, §4, §6). The shell rules apply
   only when JavaScript runs (html.js, set by js/settings.js); without it chapter pages
   stay normal documents styled by base.css. */

/* Hidden without JavaScript */
html:not(.js) .player, html:not(.js) .strip, html:not(.js) .tog,
html:not(.js) .app-title, html:not(.js) .sheet-backdrop { display: none; }
html:not(.js) .chapter-list { max-width: var(--measure); margin: var(--space-5) auto; padding: 0 var(--space-3); }

/* Pinyin and translation toggles */
/* Hidden pinyin keeps its space, so turning it on or off never moves the text (spec 2026-10-21-desktop-polish §2). */
.no-pinyin rt { visibility: hidden; }
.no-tr .tr { display: none; }

/* Chinese text: pinyin as ruby, highlight states */
/* Pinyin wider than its character spreads the characters; the padding keeps neighbouring syllables apart. */
.zh rt::before { content: attr(data-py); }
.zh rt { font-family: var(--font-text); font-size: max(0.36em, calc(var(--py-min) * var(--user-scale, 1))); padding-inline: 0.15em; color: var(--text-muted); letter-spacing: 0; }
.zh .pu { color: var(--text-muted); }
.lines.live [data-i] { color: var(--text-dim); transition: color 0.12s, text-shadow 0.12s; }
.lines.live .line.done [data-i], .lines.live [data-i].p { color: var(--text); }
.lines.live [data-i].n { color: var(--text); text-shadow: 0 0 0.45em var(--glow); }
.lines.live [data-i].n rt { color: var(--red-text); }
.line.active { background: linear-gradient(90deg, var(--line-active), transparent 70%); box-shadow: inset 2px 0 0 var(--red); }
html.js .line[data-start] { cursor: pointer; }
/* A saved line (js/saved.js): a small ribbon after its Chinese. */
.line[data-saved] > .zh::after { content: ""; display: inline-block; width: 0.32em; height: 0.48em; margin-left: 0.4em; background: var(--red-text); clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 72%, 0 100%); vertical-align: 0.2em; }
/* A line opened from a shared link (#lK): marked for a moment (js/router.js) */
.line.linked { animation: line-linked 3s ease-out; }
@keyframes line-linked { 0%, 50% { background-color: var(--line-active); box-shadow: inset 3px 0 0 var(--red); } }
@media (prefers-reduced-motion: reduce) {
  .line.linked { animation: none; background-color: var(--line-active); box-shadow: inset 3px 0 0 var(--red); }
}

/* Header tools */
.site-header .tools { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; }
.tog, .icon-btn { appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 0.3rem; min-width: 2.25rem; height: 2.25rem; padding: 0 0.6rem; border: 1px solid transparent; border-radius: 999px; background: transparent; color: var(--text-muted); font: 600 var(--step--1) var(--font-ui); text-decoration: none; cursor: pointer; }
.tog:hover, .icon-btn:hover { background: var(--hover); color: var(--text); }
.tog[lang="zh-Hans"] { font-size: var(--step-0); }
/* The toggles are circles (a pressed one is a raised chip), never squeezed, and never touching. */
.tog { flex: none; width: 2.25rem; padding: 0; }
.tog + .tog { margin-left: var(--space-1); }
.tog[aria-pressed="true"] { background: var(--seg-on); box-shadow: var(--seg-on-shadow); color: var(--text); }
.icon-btn { padding: 0; }
.icon { width: 1.25rem; height: 1.25rem; }
.app-title { appearance: none; display: inline-flex; align-items: center; gap: 0.3rem; border: 0; background: transparent; color: var(--text); font: 600 var(--step-0) var(--font-ui); white-space: nowrap; cursor: pointer; padding: 0 var(--space-1); }
.app-title .icon { width: 1.1rem; height: 1.1rem; color: var(--text-muted); transition: transform 0.2s; }
.app-title[aria-expanded="true"] .icon { transform: rotate(180deg); }
/* One language control (spec 2026-10-21-shell-redesign §6): a details menu, so it works without JS. */
.menu { position: relative; }
.menu > summary { list-style: none; }
.menu > summary::-webkit-details-marker { display: none; }
.lang-btn { padding: 0 0.6rem 0 0.5rem; }
.lang-btn .icon { width: 1.1rem; height: 1.1rem; }
.menu[open] > summary { background: var(--hover); color: var(--text); }
.langs, .site-links { position: absolute; top: calc(100% + 0.4rem); right: 0; z-index: 30; display: grid; min-width: 9.5rem; padding: var(--space-1); border: 1px solid var(--glass-edge); border-radius: var(--radius); background: var(--glass); box-shadow: var(--shadow), var(--glass-shine); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); }
.site-links { min-width: 12rem; }
.langs a, .site-links a { padding: 0.55rem 0.8rem; border-radius: 8px; color: var(--text); font: var(--step-0) var(--font-ui); text-decoration: none; }
.langs a:hover, .site-links a:hover { background: var(--hover); }
.langs a[aria-current] { color: var(--red-text); font-weight: 600; }

/* Player */
.player { position: relative; z-index: 1; box-shadow: var(--shadow-up); padding: var(--space-2) var(--space-3) calc(var(--space-2) + env(safe-area-inset-bottom)); }
/* The glass is a layer behind the player's content rather than the player's own background: an
   element with a backdrop filter is a backdrop root, and the speed menu inside it would then blur
   only the player instead of the page. */
.player::before { background: var(--glass); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); }
.player::before { content: ""; position: absolute; inset: 0; z-index: -1; border-top: 1px solid var(--glass-edge); border-radius: inherit; }
.seekrow { display: flex; align-items: center; gap: var(--space-2); }
.time { min-width: 2.75rem; color: var(--text-muted); font: var(--step--1) var(--font-ui); font-variant-numeric: tabular-nums; }
.time.dur { text-align: right; }
.seek { flex: 1; min-width: 0; --p: 0%; height: 1.5rem; margin: 0; background: transparent; cursor: pointer; appearance: none; -webkit-appearance: none; }
/* A thin track filled in red up to the playhead (--p, set by js/player.js), and a small round thumb. */
.seek::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: linear-gradient(var(--red), var(--red)) 0 / var(--p) 100% no-repeat, var(--line); }
.seek::-moz-range-track { height: 4px; border-radius: 2px; background: linear-gradient(var(--red), var(--red)) 0 / var(--p) 100% no-repeat, var(--line); }
.seek::-webkit-slider-thumb { width: 0.8rem; height: 0.8rem; margin-top: calc(2px - 0.4rem); border: 0; border-radius: 50%; background: var(--text); -webkit-appearance: none; }
.seek::-moz-range-thumb { width: 0.8rem; height: 0.8rem; border: 0; border-radius: 50%; background: var(--text); }
.seek:hover::-webkit-slider-thumb { background: var(--red-text); }
.seek:hover::-moz-range-thumb { background: var(--red-text); }
.controls { display: flex; align-items: center; justify-content: space-between; max-width: 26rem; margin: var(--space-1) auto 0; }
.ctl { appearance: none; display: grid; place-items: center; width: 2.75rem; height: 2.75rem; border: 0; border-radius: 50%; background: var(--hover); color: var(--text); font: 600 var(--step--1) var(--font-ui); cursor: pointer; }
/* Buffering (js/player.js): a ring turns round the play button while the recitation loads. */
.player.buffering .ctl.play { position: relative; }
.player.buffering .ctl.play::after { content: ""; position: absolute; inset: -4px; border: 2px solid transparent; border-top-color: var(--red); border-radius: 50%; animation: ddj-spin 0.9s linear infinite; }
@keyframes ddj-spin { to { transform: rotate(360deg); } }
/* A page swap that takes a moment (js/router.js): a thin line runs along the top of the page. */
body.swapping::before { content: ""; position: fixed; top: 0; left: 0; right: 0; z-index: 50; height: 3px; background: linear-gradient(90deg, transparent, var(--red), transparent) 0 0 / 40% 100% no-repeat; animation: ddj-swap 1.1s ease-in-out infinite; }
@keyframes ddj-swap { from { background-position: -40% 0; } to { background-position: 140% 0; } }
@media (prefers-reduced-motion: reduce) {
  .player.buffering .ctl.play::after { animation: none; border-color: var(--red); opacity: 0.5; }
  body.swapping::before { animation: none; background: var(--red); opacity: 0.5; }
}
.ctl.play { width: 3.5rem; height: 3.5rem; border-color: var(--red); background: var(--red); color: var(--on-red); }
.ctl .icon { width: 1.4rem; height: 1.4rem; }
/* Speed: a menu of steps; the button has one width whatever the speed, so the bar never moves. */
.ctl.speed { width: 3.4rem; border-radius: 999px; font-variant-numeric: tabular-nums; }
.speeds { position: absolute; bottom: calc(100% + 0.6rem); left: 0; z-index: 30; display: grid; min-width: 5.5rem; padding: var(--space-1); border: 1px solid var(--glass-edge); border-radius: var(--radius); background: var(--glass); box-shadow: var(--shadow), var(--glass-shine); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); }
.speeds button { appearance: none; padding: 0.5rem 0.8rem; border: 0; border-radius: 8px; background: none; color: var(--text); font: var(--step-0) var(--font-ui); font-variant-numeric: tabular-nums; cursor: pointer; }
.speeds button:hover { background: var(--hover); }
.speeds button[aria-pressed="true"] { color: var(--red-text); font-weight: 600; }
.player .i-pause, .player.playing .i-play, .mode .icon { display: none; }
.player.playing .i-pause { display: block; }
.player[data-mode="next"] .i-mode-next, .player[data-mode="repeat"] .i-mode-repeat, .player[data-mode="stop"] .i-mode-stop { display: block; }

/* Chapter strip (mobile) */
.strip { position: relative; display: flex; gap: var(--space-1); overflow-x: auto; padding: var(--space-2) var(--space-3) calc(var(--space-2) + env(safe-area-inset-bottom)); background: var(--surface); scrollbar-width: none; scroll-snap-type: x proximity; }
.strip::-webkit-scrollbar { display: none; }
.strip a, .strip span { flex: 0 0 auto; display: grid; place-items: center; min-width: 2.5rem; height: 2.5rem; border-radius: 10px; background: var(--hover); color: var(--text-muted); font: 600 var(--step--1) var(--font-ui); text-decoration: none; scroll-snap-align: center; }
.strip a[aria-current] { background: var(--red); color: var(--on-red); }
.strip .soon { background: transparent; border: 1px dashed var(--line); }

/* Chapter list: desktop sidebar, mobile sheet */
.chapter-list .cl-title { margin: 0; padding: var(--space-3) var(--space-3) var(--space-2); font: 600 var(--step-1) var(--font-zh); letter-spacing: 0.1em; }
.cl-head { display: flex; align-items: center; justify-content: space-between; }
html:not(.js) .cl-close { display: none; }
/* The phone sheet opens at the current chapter: its title and close button stay in view above
   the list as it scrolls. Solid, not glass: the sheet is already a backdrop root, so glass here
   would let the rows passing under show through. */
html.js body.app .cl-head { position: sticky; top: 0; z-index: 1; padding-right: var(--space-3); border-radius: var(--sheet-radius) var(--sheet-radius) 0 0; background: var(--surface-2); }
html.js body.app .cl-close { color: var(--text); }
.chapter-list ol { margin: 0; padding: 0 var(--space-2) var(--space-3); list-style: none; }
.chapter-list ol a, .chapter-list .soon { display: flex; align-items: baseline; gap: var(--space-3); padding: 0.5rem var(--space-3) 0.5rem var(--space-2); border-radius: 8px; color: var(--text); font-family: var(--font-zh); font-size: 1.3rem; letter-spacing: 0.05em; text-decoration: none; }
.chapter-list b { min-width: 1.8rem; color: var(--text-muted); font: 500 var(--step-0) var(--font-ui); font-variant-numeric: tabular-nums; text-align: right; }
.chapter-list a:hover { background: var(--hover); color: var(--text); }
/* The opening, and under it the chapter's short title in English or French. */
.chapter-list .cl-text { display: grid; min-width: 0; }
.chapter-list .cl-tr { overflow: hidden; color: var(--text-muted); font: var(--step--1)/1.3 var(--font-ui); letter-spacing: 0; text-overflow: ellipsis; white-space: nowrap; }
/* data-was: the chapter last opened, still marked while another page is shown (js/ui.js). */
/* A short red mark at the left edge, inside the rounded tint. */
.chapter-list a[aria-current], .chapter-list a[data-was] { background: linear-gradient(var(--red), var(--red)) no-repeat 0.2rem 50% / 2px 1.1rem, var(--current-bg); }
.chapter-list a[aria-current] b, .chapter-list a[data-was] b { color: var(--text); }
.sheet-backdrop { display: none; position: fixed; inset: 0; z-index: 19; background: var(--scrim); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }

/* Settings dialog and page. The sheet is wide on desktop (three cards side by side, nothing to
   scroll at common sizes), a centred column on tablets and a bottom sheet on phones. Short
   choices are radio groups drawn as segmented rows; on/off settings are switches. */
.sheet-dialog, .settings-page { font-family: var(--font-ui); }
.sheet-dialog { width: min(64rem, 94vw); max-width: none; max-height: 92dvh; padding: 0 var(--space-4) var(--space-4); border: 1px solid var(--glass-edge); box-shadow: var(--shadow), var(--glass-shine); border-radius: var(--sheet-radius); background: var(--glass); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); color: var(--text); }
.sheet-dialog::backdrop { background: var(--scrim); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
/* The head stays in view while a phone scrolls the sheet. */
.dialog-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin: 0 calc(-1 * var(--space-4)) var(--space-2); padding: var(--space-3) var(--space-4) var(--space-2); }
.dialog-head h2 { margin: 0; font: 600 var(--step-1, 1.25rem)/1.2 var(--font-ui); }
.dialog-head h2:focus { outline: none; }
.dialog-head .icon-btn { flex: none; margin-right: calc(-1 * var(--space-2)); color: var(--text); }

.settings-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: var(--space-3); }
.settings-stack { display: grid; grid-template-rows: auto 1fr; gap: var(--space-3); }
.settings-card { align-content: start; }
.settings-card { display: grid; gap: var(--space-3); margin: 0; padding: var(--space-3); border-radius: 14px; background: var(--card-tint); }
.settings-card > h2, .settings-card > h3 { margin: 0; color: var(--text-muted); font: 600 0.75rem/1 var(--font-ui); letter-spacing: 0.08em; text-transform: uppercase; }
.setting { display: grid; gap: 0.4rem; }
.setting-label { font-size: var(--step--1); }
.setting.switch { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); cursor: pointer; }
.setting.switch input { flex: none; width: 2.6rem; height: 1.5rem; margin: 0; border-radius: 999px; -webkit-appearance: none; appearance: none; cursor: pointer;
  background: radial-gradient(circle, #fff 0 0.52rem, transparent 0.56rem) no-repeat 0.05rem 50% / 1.4rem 1.4rem, var(--line); transition: background-position 0.15s, background-color 0.15s; }
.setting.switch input:checked { background-position: calc(100% - 0.05rem) 50%, 0 0; background-color: var(--switch-on); background-image: radial-gradient(circle, var(--switch-knob) 0 0.52rem, transparent 0.56rem), linear-gradient(var(--switch-on), var(--switch-on)); }
.setting.switch input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.seg { display: flex; gap: 2px; padding: 3px; border-radius: 11px; background: var(--seg-bg); }
.seg label { position: relative; flex: 1 1 auto; min-width: 0; }
/* The radio stays in the page (focusable, arrow keys) but invisible over its segment. */
.seg input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.seg span { display: flex; align-items: center; justify-content: center; min-height: 2.25rem; padding: 0 0.45rem; border-radius: 8px; font: 500 var(--step--1)/1 var(--font-ui); white-space: nowrap; color: var(--text); }
.seg label:hover input:not(:checked) + span { background: var(--seg-hover); }
.seg input:checked + span { background: var(--seg-on); box-shadow: var(--seg-on-shadow); font-weight: 600; }
.seg input:focus-visible + span { outline: 2px solid var(--focus); outline-offset: 1px; }
.size-a { line-height: 1; font-family: var(--font-text); }
.size-a.size-85 { font-size: 0.8rem; } .size-a.size-100 { font-size: 0.95rem; } .size-a.size-115 { font-size: 1.1rem; } .size-a.size-130 { font-size: 1.25rem; }
/* Buttons in the word card's pill style: the main action a raised chip like a chosen segment, quiet secondary ones. */
.btn-primary, .btn-quiet, .btn-line { min-height: 2.5rem; padding: 0 var(--space-3); border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--text); font: 600 var(--step--1) var(--font-ui); cursor: pointer; }
.btn-primary { border-color: transparent; background: var(--seg-on); box-shadow: var(--seg-on-shadow); }
.btn-primary:hover { background: var(--seg-hover); }
.btn-quiet { justify-self: start; min-height: 2rem; border-color: transparent; padding: 0; color: var(--text-muted); font-weight: 500; text-decoration: underline; text-underline-offset: 0.2em; }
.btn-line:hover { background: var(--seg-hover); }
.settings-card .status:empty { display: none; }
.settings-card .status { margin: 0; font-size: var(--step--1); }
.settings-card .hint { margin: 0; color: var(--text-muted); font-size: var(--step--1); line-height: 1.45; }
/* Offline: what it does, where it stands, one main action and a quiet remove link. */
.offline-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); }
.offline-install { display: grid; justify-items: start; gap: var(--space-1); }
.offline-install[hidden] { display: none; }
.offline-bar { width: 100%; height: 4px; border: 0; border-radius: 2px; background: var(--seg-bg); -webkit-appearance: none; appearance: none; }
.offline-bar::-webkit-progress-bar { border-radius: 2px; background: var(--seg-bg); }
.offline-bar::-webkit-progress-value { border-radius: 2px; background: var(--switch-on); }
.offline-bar::-moz-progress-bar { border-radius: 2px; background: var(--switch-on); }
/* Tablets: one centred column. Phones: a bottom sheet the width of the screen. */
@media (max-width: 59.99rem) { .sheet-dialog { width: min(36rem, 94vw); } }
@media (max-width: 39.99rem) {
  .sheet-dialog { width: 100%; max-height: 90dvh; margin: auto 0 0; padding: 0 var(--space-3) var(--space-4); border-width: 1px 0 0; border-radius: var(--sheet-radius) var(--sheet-radius) 0 0; }
  /* The head scrolls with nothing behind it here: give it the sheet's glass. */
  .dialog-head { margin: 0 calc(-1 * var(--space-3)) var(--space-2); padding: var(--space-3) var(--space-3) var(--space-2); background: var(--glass); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); }
}

/* The shell: mobile first */
html.js body.app { display: grid; grid-template-rows: auto minmax(0, 1fr); height: 100dvh; overflow: hidden; }
html.js body.app .site-header { padding: var(--space-2) var(--space-3); }
html.js body.app .chapter-audio { display: none; }
/* The strip is the chapter navigation of chapter pages only. */
html.js body.app:not(.page-chapter) .strip { display: none; }
html.js body.app .shell { display: grid; min-height: 0; grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto auto; grid-template-areas: "main" "player" "strip"; }
html.js body.app main { grid-area: main; max-width: none; margin: 0; padding: var(--space-3) var(--space-3) var(--space-4); overflow-y: auto; overscroll-behavior: contain; }
/* main (and the desktop sidebar, below) are the scroll areas, so they are also the containing
   blocks of what is positioned inside them: a .visually-hidden live region or caption (position:
   absolute) would otherwise hang off the page at its place in the text, make the document taller
   than the window, and any scrollIntoView would then shift the whole shell. */
html.js body.app main { position: relative; }
/* The page scrolls under the glass player (main spans its row and leaves room at the end). */
html.js body.app main { grid-row: 1 / 3; padding-bottom: 8rem; }
html.js body.app .player { grid-area: player; align-self: end; }
html.js body.app .strip { grid-area: strip; }
html.js body.app .chapter-list { position: fixed; right: 0; bottom: 0; left: 0; z-index: 20; height: 70dvh; overflow-y: auto; border-top: 1px solid var(--glass-edge); border-radius: var(--sheet-radius) var(--sheet-radius) 0 0; background: var(--glass); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); transform: translateY(100%); visibility: hidden; transition: transform 0.25s, visibility 0.25s; }
html.js body.app.sheet-open .chapter-list { transform: none; visibility: visible; transition: transform 0.25s, visibility 0s; }   /* visible at once on open, so focus can move in */
html.js body.app.sheet-open .sheet-backdrop { display: block; }
html.js body.app .chapter-head h1 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
html.js body.app .chapter-head .lead { margin: 0 0 var(--space-3); font-size: var(--step-0); }
/* One text size per breakpoint, never per chapter or language (spec 2026-10-21-shell-redesign §4);
   only the reader's text-size setting scales it. */
html.js body.app { --zh-size: 1.6rem; --tr-size: 1.125rem; }
html.js body.app .zh { font-size: calc(var(--zh-size) * var(--user-scale, 1)); line-height: 1.9; }
html.js body.app .tr { font-size: calc(var(--tr-size) * var(--user-scale, 1)); }
html.js body.app .line { padding: var(--space-2); border-bottom: 0; border-radius: 6px; }
@media (max-width: 63.99rem) {
  html.js body.app .brand span { display: none; }
  html.js body.app .site-header { gap: var(--space-2); }
  html.js body.app .site-header .tools { gap: var(--space-1); }
}
/* Small phones: the tools close up and the language button shows its code only, so the header fits 360px. */
@media (max-width: 26rem) {
  html.js body.app .site-header .tools { gap: 0; }
  html.js body.app .site-header .icon-btn { min-width: 2.1rem; padding: 0; }
  html.js body.app .lang-btn .icon { display: none; }
}
/* The smallest phones (320px): the logo gives way; Home is in the ☰ menu. */
@media (max-width: 21rem) {
  html.js body.app .brand { display: none; }
}

/* Desktop: no top bar. The header and the shell step aside (display: contents) so the brand, the
   chapter label, the tools, main, the player and the list sit on one body grid; the player row is
   the toolbar (spec 2026-10-21-shell-redesign §1). Nothing is duplicated in the DOM. */
@media (min-width: 64rem) {
  html.js body.app { --zh-size: 2rem; --tr-size: 1.2rem; --bar-h: 4.25rem; --tools-w: 12rem; grid-template-columns: 18rem minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto; grid-template-areas: "brand main" "list main" "list main"; }
  html.js body.app .site-header, html.js body.app .shell { display: contents; }
  html.js body.app .brand { grid-area: brand; gap: var(--space-3); padding: var(--space-4) var(--space-4) var(--space-3); background: var(--surface); }
  html.js body.app .brand img { width: 48px; height: 48px; }
  html.js body.app .brand-zh { font-size: 1.6rem; }
  html.js body.app .brand-py { font-size: 0.9rem; }
  html.js body.app .chapter-list { position: relative; grid-area: list; min-height: 0; height: auto; overflow-y: auto; border: 0; border-radius: 0; background: var(--surface); -webkit-backdrop-filter: none; backdrop-filter: none; transform: none; visibility: visible; transition: none; }
  html.js body.app .chapter-list .cl-head { display: none; }
  html.js body.app .strip, html.js body.app .sheet-backdrop, html.js body.app.sheet-open .sheet-backdrop { display: none; }
  html.js body.app main { grid-row: 1 / -1; min-height: 0; padding: var(--space-5) var(--space-5) calc(var(--bar-h) + var(--space-3) + var(--space-5)); }
  /* One row (spec 2026-10-21-desktop-polish §1): the controls, the seek bar, the tools. The tools
     are header elements laid over the bar's end (same grid area), so the bar leaves room for them.
     No chapter label: the sidebar marks the chapter. */
  html.js body.app:has(.tools .tog) { --tools-w: 18rem; }
  html.js body.app .player { display: flex; align-items: center; gap: var(--space-4); height: var(--bar-h); margin: 0 var(--space-4) var(--space-3); padding: 0 var(--tools-w) 0 var(--space-4); border-radius: var(--radius); box-shadow: var(--shadow), var(--glass-shine); }
  html.js body.app .player::before { border: 1px solid var(--glass-edge); }
  html.js body.app .controls { order: -1; flex: none; gap: var(--space-1); max-width: none; margin: 0; }
  html.js body.app .seekrow { flex: 1; min-width: 0; }
  html.js body.app .ctl { width: 2.75rem; height: 2.75rem; background: transparent; color: var(--text); }
  html.js body.app .ctl:hover { background: var(--hover); color: var(--text); }
  html.js body.app .ctl.play { width: 3rem; height: 3rem; margin: 0 var(--space-1); background: var(--red); color: var(--on-red); }
  html.js body.app .ctl.play:hover { filter: brightness(1.1); }
  html.js body.app .ctl .icon { width: 1.45rem; height: 1.45rem; }
  html.js body.app .ctl.speed { width: 3.6rem; font-size: var(--step-0); }
  html.js body.app .time { min-width: 3rem; color: var(--text); font-size: var(--step-0); }
  html.js body.app .tools .tog, html.js body.app .tools .icon-btn { min-width: 2.75rem; height: 2.75rem; color: var(--text); font-size: var(--step-0); }
  html.js body.app .tools .tog { width: 2.75rem; }
  html.js body.app .tools .tog[lang="zh-Hans"] { font-size: var(--step-1); }
  html.js body.app .tools .icon-btn .icon { width: 1.35rem; height: 1.35rem; }
  html.js body.app .app-title { display: none; }
  html.js body.app .player, html.js body.app .tools { grid-area: 3 / 2; }
  html.js body.app .tools { z-index: 2; align-self: end; height: var(--bar-h); margin-bottom: var(--space-3); }
  html.js body.app .tools { justify-self: end; gap: var(--space-1); margin-right: calc(var(--space-4) + var(--space-3)); }
  html.js body.app .chapter-head .h1-zh { font-size: 2.6rem; }
  html.js body.app .tools .langs, html.js body.app .site-links { top: auto; bottom: calc(100% + 0.6rem); }
}
/* Narrow desktops (1024 to 1200 px): the one-row player has no room for both times beside the
   seek bar and the tools, so the sidebar narrows and the times give way (the seek preview and
   the lock screen still show the position). */
@media (min-width: 64rem) and (max-width: 74.99rem) {
  html.js body.app { grid-template-columns: 15rem minmax(0, 1fr); }
  html.js body.app .player .time { display: none; }
}
@media (min-width: 64rem) {
  html.js body.app .chapter-head { margin-bottom: var(--space-4); }
  html.js body.app .chapter-head h1 { position: static; width: auto; height: auto; overflow: visible; clip-path: none; white-space: normal; margin: 0 0 var(--space-3); }
  /* Both columns in every language and with the translation hidden: the Chinese never moves.
     The Chinese column is as wide as the chapter's longest line (the lines share the list's
     columns through subgrid), so the translation sits right beside the text. */
  html.js body.app article.chapter:not([data-preview]) .lines { display: grid; grid-template-columns: fit-content(58%) minmax(0, 1fr); column-gap: var(--space-5); }
  html.js body.app article.chapter:not([data-preview]) .line { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; }
  html.js body.app .tr { margin: 0; }
  html.js body.app .lines .zh rt { font-size: max(0.4em, calc(var(--py-min) * var(--user-scale, 1))); }
}

/* /css/components/word-card.css */
/* The word card (spec 2026-10-21-tap-a-word §6, §7), opened by js/words.js: a popover by
   the word on wide screens, a bottom sheet like the chapter list under 700px. Tokens only. */

/* Words in the Chinese: tappable, and the open one outlined */
html.js .zh .w { border-radius: 4px; cursor: pointer; }
@media (hover: hover) {
  html.js .zh .w:hover { background: var(--surface-2); }
}
.zh .w.on { outline: 2px solid var(--focus); outline-offset: 2px; }

/* The card: interface text in the UI font (spec 2026-10-21-desktop-polish §4); only the characters are Kai.
   Wide and short: the word, its pinyin and three icon buttons on one row, the meaning in this
   line under it, then one line of other meanings and one of counts. */
.word-card { position: fixed; top: 0; left: 0; z-index: 30; width: min(30rem, calc(100vw - 24px)); max-height: calc(100dvh - 24px); overflow-y: auto; padding: var(--space-2) var(--space-3) var(--space-3) var(--space-4); border: 1px solid var(--glass-edge); border-radius: var(--radius); background: var(--glass); color: var(--text); box-shadow: 0 0.75rem 2rem var(--scrim), var(--glass-shine); -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur); font: var(--step-0)/1.5 var(--font-ui); animation: word-in 0.14s ease-out; }
.word-card[hidden], .word-card [hidden] { display: none; }
.word-head { display: flex; align-items: center; gap: var(--space-3); }
.word-zh { margin: 0; font: 400 2.25rem/1.2 var(--font-zh); letter-spacing: 0.06em; }
.word-zh:focus { outline: none; }   /* focused on open for screen readers; it is not a control */
.word-py { flex: 1; margin: 0; color: var(--text-muted); font-size: var(--step-1); }
.word-actions { display: flex; gap: 0.125rem; margin: 0 calc(-1 * var(--space-1)) 0 0; }
.word-actions .icon { width: 1.15rem; height: 1.15rem; }
.word-gloss { margin: var(--space-1) 0 0; font: 600 var(--step-1)/1.35 var(--font-ui); }
.word-note { margin: var(--space-1) 0 0; color: var(--text-muted); font: var(--step--1)/1.45 var(--font-ui); }
.word-other { margin: var(--space-2) 0 0; color: var(--text-muted); font: var(--step--1)/1.45 var(--font-ui); }
.word-label { margin-right: 0.5em; color: var(--text); font-weight: 600; }
.word-foot { display: flex; flex-wrap: nowrap; align-items: baseline; justify-content: space-between; gap: var(--space-1) var(--space-3); margin-top: var(--space-3); padding-top: var(--space-2); border-top: 1px solid var(--line); color: var(--text-muted); font: 0.75rem/1.45 var(--font-ui); }
.word-foot p { margin: 0; }
/* One line: the counts give way (ellipsis) before the links do. */
.word-count { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.word-term, .word-report { flex: none; white-space: nowrap; }
/* "Key term: 道" link to the term page */
.word-term { font-weight: 600; }
.word-report { margin-left: auto; }
/* A tapped word while the word data loads (the full text fetches it on the first tap). */
.zh .w.loading { outline: 1px dashed var(--line); outline-offset: 2px; border-radius: 4px; cursor: progress; }   /* the counts on the left, the report link at the end of the same line */
.word-save[aria-pressed="true"] { color: var(--red-text); }
.word-save[aria-pressed="true"] .icon { fill: currentColor; }
.word-report a { color: var(--text-muted); }
/* Traditional characters on: the simplified form in brackets after the word */
.word-zh .word-alt { margin-left: var(--space-1); color: var(--text-muted); font-size: 0.5em; letter-spacing: 0; }
/* Share this line: the status ("Link copied") and the picture to download, when there is no share sheet */
.word-share-out { margin: 0; text-align: right; color: var(--text-muted); font: var(--step--1)/1.5 var(--font-ui); overflow-wrap: anywhere; }
.word-share-status:not(:empty), .word-share-dl:not([hidden]) { display: inline-block; margin-top: var(--space-2); }
.word-share-dl { font-weight: 600; }

/* Narrow screens: a bottom sheet, as the chapter list, docked on top of the player bar
   (js/words.js sets bottom and max-height from the bar's position) */
@media (max-width: 699.98px) {
  .word-card { top: auto; right: 0; bottom: 0; left: 0; width: auto; max-height: 70dvh; border-width: 1px 0 0; border-radius: var(--sheet-radius) var(--sheet-radius) 0 0; animation-name: word-up; animation-duration: 0.2s; }
  .word-zh { font-size: 2rem; }
}

@keyframes word-in { from { opacity: 0; transform: translateY(0.25rem); } }
@keyframes word-up { from { transform: translateY(100%); } }
@media (prefers-reduced-motion: reduce) {
  .word-card { animation: none; }
}

/* /css/components/memo.css */
/* Memorisation mode (spec 2026-10-21-phase-4 §2). With html[data-hide] set by
   js/settings.js, each line not yet revealed blurs its Chinese (with the pinyin) or its
   translation: the shape of the text shows through, the words cannot be read. js/memo.js
   adds a transparent button over the blurred part only ("Hidden. Tap to reveal"), so a tap
   elsewhere on the line seeks as usual, and marks the blurred part aria-hidden, so screen readers skip it too. Nothing moves when a line is
   revealed. Tokens only. */

.memo-ph { display: none; }
html.js[data-hide="zh"] article.chapter .line:not(.revealed) .zh,
html.js[data-hide="tr"] article.chapter .line:not(.revealed) .tr { filter: blur(0.24em); opacity: 0.8; user-select: none; -webkit-user-select: none; }
html.js[data-hide="zh"] article.chapter .line:not(.revealed) .zh { filter: blur(0.18em); }
/* The blurred part shrinks to its text, so only a tap on the text reveals it. */
html.js[data-hide="zh"] article.chapter .line:not(.revealed) > .zh,
html.js[data-hide="tr"] article.chapter .line:not(.revealed) > .tr { position: relative; width: fit-content; }
/* The home page's preview is centred: the shrunk part stays centred under its line. */
html.js[data-hide="zh"] article.chapter[data-preview] .line:not(.revealed) > .zh,
html.js[data-hide="tr"] article.chapter[data-preview] .line:not(.revealed) > .tr { margin-inline: auto; }
html.js[data-hide="zh"] article.chapter .line:not(.revealed) > .zh > .memo-ph,
html.js[data-hide="tr"] article.chapter .line:not(.revealed) > .tr > .memo-ph {
  display: block; position: absolute; inset: 0; width: 100%; padding: 0; border: 0; border-radius: 4px; background: transparent; cursor: pointer;
}
html.js[data-hide] article.chapter .line .memo-ph:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
/* A revealed line keeps focus after its placeholder goes (js/memo.js). */
article.chapter .line:focus { outline: none; }
article.chapter .line:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* /css/components/legge.css */
/* After the commentary on chapter pages: "Words in this chapter", the pinyin text and the Legge
   comparison (phase 4 spec §5). Both span the text column like the commentary; areas are told apart by
   tone, not rules. Tokens only. */

/* Words in this chapter: an HTML table for readers without JS and for crawlers
   (context/seo-geo.md), drawn as a grid of tiles. A tap on a tile opens the word card
   (js/words.js turns each character into a button). */
.vocab-table { display: block; width: 100%; margin: 0; border-collapse: collapse; }
.vocab-table tbody { display: grid; grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr)); gap: var(--space-2); }
.vocab-table tr { display: grid; grid-template-columns: auto 1fr; align-items: baseline; align-content: start; gap: 0.15rem var(--space-2); padding: var(--space-2) var(--space-3) var(--space-3); border-radius: 12px; background: var(--card-tint); }
.vocab-table th, .vocab-table td { padding: 0; text-align: left; }
.vocab-table tbody th { color: var(--text); font: 400 1.75rem/1.3 var(--font-zh); white-space: nowrap; }
.vocab-table td[lang] { color: var(--text-muted); font: var(--step--1)/1.3 var(--font-text); }
.vocab-table td:last-child { grid-column: 1 / -1; font: var(--step--1)/1.35 var(--font-ui); }
html.js .vocab-table tr[data-w] { cursor: pointer; }
html.js .vocab-table tr[data-w]:hover, .vocab-table tr.on { background: var(--seg-hover); }
.vocab-table tr.on { box-shadow: inset 0 0 0 1px var(--line); }
.vocab-term { display: block; margin-top: 0.3rem; color: var(--link); font-size: var(--step--1); }
.vocab-open { appearance: none; padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; }
.vocab-open:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: 4px; }

/* The pinyin text and the Legge comparison: quiet lines that fold out, muted so they never
   compete with the lines above. The pinyin is here as text because the lines draw theirs from an
   attribute (src/chinese.php). */
.fold { margin: var(--space-5) 0 0; color: var(--text-muted); }
.fold + .fold { margin-top: var(--space-2); }
.fold summary { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-3); border-radius: 12px; background: var(--card-tint); color: var(--text); font: 600 var(--step--1) var(--font-ui); cursor: pointer; list-style: none; }
.fold summary::-webkit-details-marker { display: none; }
.fold summary::after { content: ""; width: 0.5rem; height: 0.5rem; margin-left: auto; border: solid var(--text-muted); border-width: 0 2px 2px 0; transform: translateY(-25%) rotate(45deg); transition: transform 0.15s; }
.fold[open] summary::after { transform: translateY(25%) rotate(-135deg); }
.fold summary:hover { background: var(--seg-hover); }
.fold summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.fold-text { margin-top: var(--space-3); padding: 0 var(--space-3); font: var(--step-0)/1.6 var(--font-text); }
.fold-text p { margin: 0 0 var(--space-3); }
.fold-credit { margin: var(--space-2) 0 0; padding: 0 var(--space-3); font: var(--step--1) var(--font-ui); }
@media (prefers-reduced-motion: reduce) { .fold summary::after { transition: none; } }
@media (max-width: 699.98px) {
  .vocab-table tbody { grid-template-columns: 1fr 1fr; }
  .vocab-table tbody th { font-size: 1.5rem; }
}

/* /css/components/onboarding.css */
/* First-use hints and "Install the app" (plan 2026-10-23-install-hints-art; js/hints.js,
   js/pwa.js). The bubble is a solid raised card, not glass: its arrow has to match its fill. */
.coach { position: fixed; z-index: 45; width: max-content; max-width: min(19rem, calc(100vw - 24px)); padding: var(--space-3); border: 1px solid var(--glass-edge); border-radius: var(--radius); background: var(--surface-2); box-shadow: var(--shadow); color: var(--text); font: var(--step--1)/1.45 var(--font-ui); pointer-events: none; animation: coach-in 0.25s ease-out; }
.coach[hidden] { display: none; }
/* Taps on the bubble's text reach the page (and close it): only its buttons take them. */
.coach button { pointer-events: auto; }
.coach::before { content: ""; position: absolute; left: calc(var(--arrow-x, 50%) - 7px); width: 12px; height: 12px; border: inherit; border-width: 1px 0 0 1px; background: inherit; transform: rotate(45deg); }
.coach[data-side="below"]::before { top: -7px; }
.coach[data-side="above"]::before { bottom: -7px; transform: rotate(225deg); }
.coach-text { margin: 0 0 var(--space-2); }
.coach-actions { display: flex; justify-content: flex-end; align-items: center; gap: var(--space-3); }
.coach-actions .btn-primary { min-height: 2.25rem; }
.coach-actions .btn-quiet { min-height: 2.25rem; }
.coach:not([data-hint="install"]) [data-coach="later"], .coach:not([data-hint="install"]) [data-coach="install"], .coach[data-hint="install"] [data-coach="ok"] { display: none; }
@keyframes coach-in { from { opacity: 0; translate: 0 4px; } }
@media (prefers-reduced-motion: reduce) { .coach { animation: none; } }

/* The ☰ menu's install entry: a button that looks like the links above it. */
.site-links .install-link { appearance: none; display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-1); padding: 0.55rem 0.8rem; border: 0; border-top: 1px solid var(--glass-edge); border-radius: 0 0 8px 8px; background: transparent; color: var(--text); font: 600 var(--step-0) var(--font-ui); text-align: start; cursor: pointer; }
.site-links .install-link:hover { background: var(--hover); }
.site-links .install-link[hidden] { display: none; }
.install-link .icon { width: 1.1rem; height: 1.1rem; color: var(--text-muted); }

/* The install dialog: one short column, not the settings' three. */
.install-dialog { width: min(26rem, 94vw); }
.install-lead { margin: 0 0 var(--space-3); color: var(--text-muted); }
.install-steps { display: grid; gap: var(--space-2); margin: 0 0 var(--space-3); padding-left: 1.4rem; font-size: var(--step-0); }
.install-steps .icon { width: 1.2rem; height: 1.2rem; margin-left: 0.2rem; vertical-align: -0.2rem; color: var(--link); }
@media (max-width: 40rem) { .install-dialog { width: 100%; } }

/* /css/pages/home.css */
/* The home page (spec 2026-10-21-home-redesign): the opening of chapter 1 as the hero, the
   answer under the H1, the chapter of the day, the chapters in the book's two parts, the key
   terms, how the site works, and the book in figures. Loaded on every app page, because the
   router swaps the home page in and out without reloading the styles. */
.page-home { --hero-size: 2.2rem; }
html.js body.app.page-home main > * { max-width: 76rem; margin-inline: auto; }

/* Hero: the opening lines, large and centred, painted by the player like a chapter */
.hero { margin: 0 auto var(--space-4); padding: var(--space-4) 0 var(--space-3); border-radius: var(--radius); background: radial-gradient(ellipse at 50% 0%, var(--hero-glow), transparent 70%); text-align: center; }
/* The glow starts at the very top of the page: main gives up its top padding to the hero. */
html.js body.app.page-home main { padding-top: 0; }
html.js body.app .hero { padding-top: calc(var(--space-4) + var(--space-3)); border-radius: 0 0 var(--radius) var(--radius); }
.hero .line, html.js body.app .hero .line { display: block; padding: var(--space-2) var(--space-3); border: 0; }
.hero .line.active { background: none; box-shadow: none; }
.hero .zh, html.js body.app .hero .zh { font-size: calc(var(--hero-size) * var(--user-scale, 1)); line-height: 1.8; letter-spacing: 0.08em; }
.hero .tr, html.js body.app .hero .tr { margin: 0; color: var(--text-muted); font-size: calc(var(--step-0) * var(--user-scale, 1)); font-style: italic; }

/* The ink-wash landscape: behind the H1 and the answer, rising from the bottom of the opening.
   Decoration stays faint (brand guide: nothing competes with the characters). */
.home-top { position: relative; isolation: isolate; padding-bottom: var(--space-4); }
.landscape { position: absolute; inset: auto 0 0; z-index: -1; width: 100%; height: auto; aspect-ratio: 1200 / 320; color: var(--ink); pointer-events: none; }
.landscape path { fill: url(#ink-fade); }
.landscape .ink-far { fill-opacity: 0.55; }
.landscape .ink-mid { fill-opacity: 0.8; }
.landscape .ink-sun { fill: var(--ink-sun); }
/* Phones and tablets: no landscape (fox: on a narrow screen it looks off). */
@media (max-width: 63.99rem) { .landscape { display: none; } }

/* H1, the answer and the two buttons */
.intro { text-align: center; }
html.js body.app .intro h1 { margin: 0 0 var(--space-3); }
.intro .h1-zh { font-size: var(--step-3); letter-spacing: 0.12em; }
.intro .lead { max-width: 40rem; margin: 0 auto var(--space-4); font-size: var(--step-0); }
.actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); margin: 0; }
.button { appearance: none; display: inline-flex; align-items: center; gap: var(--space-2); min-height: 2.75rem; padding: 0 var(--space-4); border: 0; border-radius: 999px; background: var(--red); color: var(--on-red); font: 600 var(--step-0) var(--font-ui); text-decoration: none; cursor: pointer; }
.button .icon { width: 1.1rem; height: 1.1rem; }
.button.quiet { background: var(--surface-2); color: var(--text); }
.button:hover { filter: brightness(1.1); }
html:not(.js) [data-play-preview] { display: none; }

.page-home h2 { margin: var(--space-5) 0 var(--space-3); }

/* Chapter of the day */
.today-card { display: grid; gap: var(--space-1); padding: var(--space-4); border-radius: var(--radius); background: var(--surface-2); box-shadow: var(--shadow); color: var(--text); text-decoration: none; }
.today-card:hover { background: var(--surface-2); filter: brightness(1.08); }
/* Saved lines (js/saved.js): each a card linking to its line, with a quiet remove button. */
.saved-lines { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
.saved-lines li { display: flex; align-items: flex-start; gap: var(--space-2); padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4); border-radius: 12px; background: var(--surface); }
.saved-link { display: grid; flex: 1; gap: 0.2rem; min-width: 0; color: var(--text); text-decoration: none; }
.saved-link:hover .saved-zh { color: var(--red-text); }
.saved-where { color: var(--text-muted); font: 600 var(--step--1) var(--font-ui); }
.saved-zh { font: var(--step-1) var(--font-zh); letter-spacing: 0.04em; }
.saved-tr { color: var(--text-muted); font-style: italic; }
/* Chinese has no italic: a 白话 line (Chinese pages) stays upright where Latin translations slant. */
.tr[lang|="zh"], .saved-tr:lang(zh), .today-tr:lang(zh) { font-style: normal; }
.saved-remove { flex: none; color: var(--text-muted); font: var(--step-1)/1 var(--font-ui); }
.today-zh { color: var(--red-text); font: 600 var(--step-0) var(--font-zh); letter-spacing: 0.1em; }
.today-first { font: var(--step-2) var(--font-zh); letter-spacing: 0.06em; }
.today-tr { color: var(--text-muted); font-style: italic; }
.today-go { margin-top: var(--space-2); color: var(--red-text); font: 600 var(--step--1) var(--font-ui); }
.today-go::after { content: " →"; }

/* The two parts and their tiles */
.part-h { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--space-3); margin: var(--space-4) 0 var(--space-3); font-size: var(--step-0); }
.part-zh { font: 600 var(--step-1) var(--font-zh); letter-spacing: 0.1em; }
.part-range { color: var(--text-muted); font-size: var(--step--1); font-weight: 400; }
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(6.75rem, 1fr)); gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
/* Every tile in a row as tall as the tallest (titles wrap to one, two or three lines). */
.tiles > li { display: grid; }
.tile { position: relative; display: grid; align-content: start; gap: 0.1rem; padding: var(--space-2) var(--space-3); border-radius: 10px; background: var(--surface); color: var(--text); text-decoration: none; }
a.tile:hover { background: var(--surface-2); }
.tile b { color: var(--text-muted); font: 600 var(--step--1) var(--font-ui); font-variant-numeric: tabular-nums; }
.tile-zh { font-family: var(--font-zh); font-size: 1.15rem; letter-spacing: 0.04em; white-space: nowrap; }
.tile-title { color: var(--text-muted); font: var(--step--1)/1.3 var(--font-ui); }
.tile.soon { background: transparent; border: 1px dashed var(--line); color: var(--text-muted); font-size: var(--step--1); }

/* Heard to the end: a dot on the tile, the sidebar row and the strip number (§6) */
.tile[data-heard]::after, .chapter-list a[data-heard]::after, .strip a[data-heard]::after { content: ""; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: var(--red-text); }
.tile[data-heard]::after { position: absolute; top: 0.6rem; right: 0.6rem; }
.chapter-list a[data-heard]::after { flex: none; align-self: center; margin-left: auto; }
.strip a[data-heard] { position: relative; }
.strip a[data-heard]::after { position: absolute; bottom: 0.25rem; left: calc(50% - 0.2rem); }
.strip a[aria-current][data-heard]::after { background: var(--on-red); }

/* Key terms */
.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
.chip { display: inline-flex; align-items: baseline; gap: var(--space-2); padding: 0.4rem 0.9rem; border-radius: 999px; background: var(--surface); color: var(--text); text-decoration: none; }
.chip:hover { background: var(--surface-2); }
.chip-zh { font: 600 var(--step-1) var(--font-zh); }
.chip-py { color: var(--text-muted); font-size: var(--step--1); font-style: italic; }
.chip-gloss { font-size: var(--step--1); }
.more-link { margin: var(--space-3) 0 0; }

/* How it works: four live samples drawn with the site's own styles */
.how-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: var(--space-3); margin: 0; padding: 0; list-style: none; }
.how-tiles li { padding: var(--space-3) var(--space-4) var(--space-4); border-radius: var(--radius); background: var(--surface); }
.how-tiles h3 { margin: var(--space-2) 0 var(--space-1); font-size: var(--step-0); }
.how-tiles p { margin: 0; color: var(--text-muted); font-size: var(--step--1); }
.how-tiles .demo { display: grid; place-items: center start; min-height: 3.5rem; color: var(--text); }
.demo-zh { font-family: var(--font-zh); font-size: 1.6rem; letter-spacing: 0.06em; }
.demo-zh > span { color: var(--text-dim); }
.demo-zh > .p { color: var(--text); }
.demo-zh > .n { color: var(--text); text-shadow: 0 0 0.45em var(--glow); }
.demo-zh > .hid { color: transparent; background: linear-gradient(var(--text-dim), var(--text-dim)) center / 82% 0.35em no-repeat; }
.demo-card { display: inline-flex; align-items: baseline; gap: var(--space-2); padding: 0.3rem 0.8rem; border-radius: 10px; background: var(--surface-2); box-shadow: var(--shadow); }
.demo-card .demo-zh { color: var(--text); }
.demo-py { color: var(--text-muted); font-style: italic; }
.demo-gloss { font-size: var(--step--1); }
.demo-share { display: inline-grid; gap: 0.1rem; padding: 0.35rem 0.9rem; border-radius: 8px; background: var(--share-bg); box-shadow: var(--shadow); }
.demo-share .demo-zh { color: var(--share-text); font-size: 1.3rem; }
.demo-site { color: var(--share-muted); font: 0.7rem var(--font-ui); }

/* The book in figures */
.facts { width: 100%; border-collapse: collapse; }
.facts th, .facts td { padding: var(--space-2) var(--space-3); text-align: left; vertical-align: top; }
.facts th { width: 34%; color: var(--text-muted); font-weight: 400; }
.facts tr:nth-child(odd) { background: var(--surface); }

@media (min-width: 48rem) {
  .page-home { --hero-size: 3rem; }
  .hero { padding-top: var(--space-5); }
  html.js body.app .hero { padding-top: calc(var(--space-5) + var(--space-3)); }
  .intro .h1-zh { font-size: 2.6rem; }
}
@media (min-width: 64rem) {
  .page-home { --hero-size: 3.4rem; }
  /* The opening lines, the H1 and the buttons fill the screen above the player bar; the
     chapter of the day starts below it and comes into view on scrolling. The glow moves to
     the whole block, so it still starts at the top. */
  html.js body.app .home-top { display: flex; flex-direction: column; justify-content: center; min-height: 100dvh; padding-bottom: calc(var(--bar-h) + var(--space-3) * 2); border-radius: 0 0 var(--radius) var(--radius); background: radial-gradient(ellipse 50% 55% at 50% 0%, var(--hero-glow), transparent); }
  html.js body.app .landscape { width: min(100%, 64rem); left: 50%; translate: -50% 0; }   /* the peaks stay below the answer; the glass toolbar blurs their base */
  html.js body.app .home-top > .hero { margin-bottom: var(--space-5); padding-top: 0; background: none; }
  /* Desktop: larger tiles, chips and samples across the wider page (spec 2026-10-21-desktop-polish §5) */
  .page-home h2 { font-size: 1.6rem; }
  .intro .lead { max-width: 48rem; font-size: var(--step-1); }
  .tiles { grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); gap: var(--space-3); }
  .tile { padding: var(--space-3) var(--space-4); }
  .tile b { font-size: var(--step-0); }
  .tile-zh { font-size: 1.45rem; }
  .part-h { font-size: var(--step-1); }
  .part-zh { font-size: var(--step-2); }
  .chip { padding: 0.55rem 1.1rem; }
  .chip-zh { font-size: var(--step-2); }
  .chip-py, .chip-gloss { font-size: var(--step-0); }
  .how-tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .how-tiles h3 { font-size: var(--step-1); }
  .how-tiles p { font-size: var(--step-0); }
  .today-card { grid-template-columns: auto 1fr; align-items: baseline; column-gap: var(--space-4); padding: var(--space-4) var(--space-5); }
  .today-zh { grid-row: 1 / span 3; font-size: var(--step-1); }
  .today-first { font-size: var(--step-3); }
  .today-tr { font-size: var(--step-1); }
  .facts { font-size: 1.0625rem; }
}

/* /css/pages/terms.css */
/* Key-term pages (phase 4 spec §1): the index table and one term's page (facts table,
   occurrences by chapter with the term highlighted). Tokens only; base.css does the rest. */

.t-zh { font-family: var(--font-zh); font-size: 1.25em; letter-spacing: 0.04em; }
.t-py { color: var(--text-muted); }

/* Index */
.terms-table { width: 100%; margin-top: var(--space-4); border-collapse: collapse; }
.terms-table th, .terms-table td { padding: var(--space-2) var(--space-2) var(--space-2) 0; border-bottom: 1px solid var(--line); text-align: left; vertical-align: baseline; }
.terms-table thead th { color: var(--text-muted); font: 600 var(--step--1) var(--font-ui); }
.terms-table tbody th { font-weight: 400; white-space: nowrap; }
.terms-table tbody th a { text-decoration: none; }
.terms-table tbody th a .t-zh { color: var(--text); }
.terms-table .t-count { color: var(--text-muted); font-size: var(--step--1); }

/* Term page */
.crumbs { margin: 0 0 var(--space-2); font-size: var(--step--1); }
.term-intro p { margin: var(--space-3) 0; }
.term-facts { width: 100%; margin: var(--space-4) 0; border-collapse: collapse; font-size: var(--step--1); }
.term-facts caption { padding-bottom: var(--space-2); color: var(--text-muted); font: 600 var(--step--1) var(--font-ui); text-align: left; }
.term-facts th, .term-facts td { padding: var(--space-2) var(--space-3) var(--space-2) 0; border-top: 1px solid var(--line); text-align: left; vertical-align: baseline; }
.term-facts th { width: 34%; color: var(--text-muted); font-weight: 400; }
.term-facts td.t-zh { font-size: var(--step-1); }
.t-chapters { line-height: 1.8; }
.t-ch { white-space: nowrap; }   /* a number and its comma never part at a line end */

.occ-chapter h3 { margin: var(--space-4) 0 0; font-size: var(--step-0); }
.occ-lines { margin: 0; padding: 0; list-style: none; }
.occ-lines li { padding: var(--space-3) 0; border-bottom: 1px solid var(--line); }
.occ-k { font: var(--step--1) var(--font-ui); color: var(--text-muted); }
.occ-lines .zh { --py-min: var(--py-min-small); margin-top: var(--space-1); font-size: calc(var(--step-2) * var(--user-scale, 1)); }
.occ-lines .tr { font-size: calc(var(--step-0) * var(--user-scale, 1)); }
/* The term itself: an underline in the accent colour, so the text colour stays readable. */
.zh .w.hl { color: var(--text); box-shadow: inset 0 -0.12em 0 var(--red); }
.zh .w.hl rt { color: var(--text); }

.term-related ul { margin: 0; padding: 0; list-style: none; }
.term-related li { padding: var(--space-1) 0; }
.term-related .t-gloss { color: var(--text-muted); }
.term-all { margin-top: var(--space-5); }
/* One- and two-digit links: 24px wide to tap. The number sits at the end of its box, against its
   comma, and the spare width falls in the space before it ("1, 4", not "1 , 4"). */
.term-facts .t-chapters a { display: inline-block; min-width: 1.5rem; padding: 0.1rem 0; text-align: end; }

/* /css/pages/pages.css */
/* The pages around the chapters inside the app shell (spec 2026-10-21-desktop-polish §5): content
   pages (How to read, Laozi, Privacy, About), the full text, the key terms index, a term page and
   settings. Loaded on every app page, because the router swaps pages without reloading the styles.
   The shell's own text sizes (app.css: html.js body.app .zh, .tr) are for chapter lines; these
   pages set theirs here. */

/* Phones and tablets: one readable column */
html.js body.app main > .page, html.js body.app main > .terms-index, html.js body.app main > .term,
html.js body.app .ft-head, html.js body.app .ft { max-width: 44rem; margin-inline: auto; }
.pg-sec h2 { margin: var(--space-5) 0 var(--space-2); }
.pg-sec > :last-child { margin-bottom: 0; }

body.app.page-full-text .ft-chapter h2 { margin: var(--space-5) 0 var(--space-2); }
/* 81 chapters and some 40,000 elements: the browser lays out only the chapters near the screen,
   which matters on low-end phones. The size is a guess the browser replaces once it has seen one. */
body.app.page-full-text .ft-chapter { content-visibility: auto; contain-intrinsic-size: auto 1200px; }
body.app.page-full-text .ft-line .zh { font-size: calc(1.35rem * var(--user-scale, 1)); line-height: 1.9; }
body.app.page-full-text .ft-line .tr { margin: 0 0 var(--space-2); font-size: calc(1rem * var(--user-scale, 1)); }

body.app.page-term .occ-lines .zh { font-size: calc(1.5rem * var(--user-scale, 1)); line-height: 1.9; }
body.app.page-term .occ-lines .tr { margin: 0; font-size: calc(1rem * var(--user-scale, 1)); }
body.app.page-term .term-side { margin: var(--space-4) 0; }
body.app.page-term .term-facts { margin-top: 0; }

/* Desktop: the width of the reading area, larger type, sections side by side */
@media (min-width: 64rem) {
  html.js body.app main > .page, html.js body.app main > .terms-index, html.js body.app main > .term,
  html.js body.app .ft-head, html.js body.app .ft { max-width: 78rem; margin-inline: 0; }
  html.js body.app main > .settings-page { max-width: 64rem; }
  html.js body.app:not(.page-chapter, .page-home) main h1 { margin-bottom: var(--space-3); font-size: 2.5rem; line-height: 1.2; }
  html.js body.app:not(.page-chapter, .page-home) main .lead { max-width: 52rem; font-size: 1.3rem; }

  /* Content pages: the answer full width, then each section as a card, in columns */
  body.app.page-content .page { font-size: 1.0625rem; line-height: 1.7; }
  body.app.page-content .page > p { max-width: 52rem; color: var(--text-muted); font-size: 1.3rem; line-height: 1.55; }
  .pg-secs { columns: 2 26rem; column-gap: var(--space-4); margin-top: var(--space-4); }
  .pg-sec { break-inside: avoid; margin: 0 0 var(--space-4); padding: var(--space-4) var(--space-5) var(--space-4) var(--space-4); border-radius: var(--radius); background: var(--surface); }
  .pg-sec h2 { margin: 0 0 var(--space-3); font-size: 1.35rem; }
  .pg-sec ul { padding-left: 1.2rem; }
  .pg-sec li + li { margin-top: var(--space-2); }

  /* Full text: the chapter heading in its own column (it stays in view), the translation beside the Chinese */
  body.app.page-full-text .ft-chapter { display: grid; grid-template-columns: 9rem minmax(0, 1fr); column-gap: var(--space-5); padding: var(--space-3) 0; }
  body.app.page-full-text .ft-chapter h2 { position: sticky; top: 0; align-self: start; margin: 0; padding-top: 0.6rem; font-size: var(--step-1); }
  body.app.page-full-text .ft-chapter h2 a { color: var(--text); text-decoration: none; }
  body.app.page-full-text .ft-chapter h2 a:hover { color: var(--red-text); }
  /* Each line beside its translation, with a little air between pairs. One Chinese column width
     for the whole page (about 14 characters), so the columns do not jump from chapter to chapter. */
  body.app.page-full-text .ft-lines { display: grid; grid-template-columns: minmax(0, calc(24rem * var(--user-scale, 1))) minmax(0, 1fr); column-gap: var(--space-5); row-gap: var(--space-2); }
  body.app.page-full-text .ft-line { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; }
  body.app.page-full-text .ft-line .zh { font-size: calc(1.6rem * var(--user-scale, 1)); }
  body.app.page-full-text .ft-line .tr { margin: 0; font-size: calc(1.1rem * var(--user-scale, 1)); }
  /* Chinese pages have no translation: the chapters flow in two columns instead. */
  body.app.page-full-text.lang-zh .ft { columns: 2; column-gap: var(--space-5); }
  body.app.page-full-text.lang-zh .ft-chapter { display: block; break-inside: avoid; }
  body.app.page-full-text.lang-zh .ft-chapter h2 { position: static; }
  body.app.page-full-text.lang-zh .ft-lines, body.app.page-full-text.lang-zh .ft-line { display: block; }

  /* Key terms: the table as a grid of cards (the template's roles keep it a table for screen readers) */
  body.app.page-terms .terms-table { display: block; margin-top: var(--space-5); }
  body.app.page-terms .terms-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  body.app.page-terms .terms-table tbody { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: var(--space-3); }
  body.app.page-terms .terms-table tr { position: relative; display: grid; align-content: start; gap: var(--space-1); padding: var(--space-4); border-radius: var(--radius); background: var(--surface); }
  body.app.page-terms .terms-table tr:hover { background: var(--surface-2); }
  body.app.page-terms .terms-table th, body.app.page-terms .terms-table td { padding: 0; border: 0; }
  body.app.page-terms .terms-table tbody th a { display: flex; align-items: baseline; gap: var(--space-3); color: var(--text); }
  body.app.page-terms .terms-table tbody th a::after { content: ""; position: absolute; inset: 0; border-radius: var(--radius); }   /* the whole card opens the term */
  body.app.page-terms .terms-table .t-zh { font-size: 2.4rem; line-height: 1.3; }
  body.app.page-terms .terms-table .t-py { font-size: var(--step-0); }
  body.app.page-terms .terms-table td { font-size: var(--step-1); }
  body.app.page-terms .terms-table .t-count { font: var(--step--1) var(--font-ui); }

  /* A term page: the reading on the left, the facts and related terms in a column that stays in view */
  body.app.page-term .term { display: grid; grid-template-columns: minmax(0, 1fr) 22rem; column-gap: var(--space-5); align-items: start; font-size: 1.0625rem; line-height: 1.7; }
  body.app.page-term .term > * { grid-column: 1; }
  body.app.page-term .term > .crumbs, body.app.page-term .term > h1, body.app.page-term .term > .lead { grid-column: 1 / -1; }
  body.app.page-term .term-side { position: sticky; top: 0; grid-column: 2; grid-row: 4 / span 3; margin: 0; padding: var(--space-4); border-radius: var(--radius); background: var(--surface); font-size: var(--step--1); }
  body.app.page-term .term-related h2 { margin: var(--space-4) 0 var(--space-2); font-size: var(--step-0); }
  body.app.page-term .term-related p { margin: 0 0 var(--space-2); color: var(--text-muted); }
  body.app.page-term .term-occ > h2 { margin-top: var(--space-4); }
  body.app.page-term .occ-lines li { display: grid; grid-template-columns: 4.5rem minmax(0, 1.15fr) minmax(0, 1fr); column-gap: var(--space-4); align-items: center; padding: var(--space-2) 0; }
  body.app.page-term .occ-lines .zh { margin: 0; font-size: calc(1.7rem * var(--user-scale, 1)); }
  body.app.page-term .occ-lines .tr { font-size: calc(1.05rem * var(--user-scale, 1)); }
  body.app.page-term.lang-zh .occ-lines li { grid-template-columns: 4.5rem minmax(0, 1fr); }
}
@media (min-width: 100rem) {
  .pg-secs { columns: 3 24rem; }
}

