/* syuan.space design system */
:root {
  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-alt: #f8fafc;
  --border: #e3e6ea;
  --border-strong: #cfd5dc;
  --text: #1a1d21;
  --text-body: #23272e;
  --text-soft: #3d444d;
  --muted: #6b7280;
  --accent: #2563eb;
  --ok: #16a34a;
  --warn: #d97706;
  --err: #dc2626;
  /* The chips carry a tinted background of their own, so a dark theme
     needs a dark tint rather than the same light one. */
  --ok-bg: #dcfce7;
  --ok-border: #bbf7d0;
  --warn-bg: #fef3c7;
  --warn-border: #fde68a;
  --err-bg: #fee2e2;
  --err-border: #fecaca;
  --accent-bg: #dbeafe;
  --on-accent: #ffffff;
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px; --sp-6: 32px;
  --radius: 8px;
  --radius-sm: 6px;
  --shadow: 0 1px 2px rgba(0,0,0,.04);
  --shadow-2: 0 4px 16px rgba(0,0,0,.10);
  --overlay: rgba(26, 29, 33, .35);

  /* The reading settings: the size of the text, the space between the
     paragraphs and the leading inside a line. A reader sets them from
     the panel in the top bar.
   *
   * The reader's choices are multipliers, and the base values are what
   * the page would use anyway: one is the page's own choice, so a reader
   * who has not touched the panel sees exactly what they saw before.
   * The narrow-screen block below has base values of its own. */
  --read-scale: 1;
  --read-line-scale: 1;
  --read-gap-scale: 1;
  --read-base-size: 16px;
  --read-base-line: 1.75;
  --read-base-gap: 16px;
  --read-base-news-title: 16px;
  --read-base-news-summary: 14px;
  --read-base-news-gap: 8px;
  /* One base value per kind of text the pages carry. The top bar and the
     controls have plain pixel sizes instead: they are operated, not read,
     so they keep their shape when the text grows. */
  --read-base-body: 14px;
  --read-base-chat-ref: 9px;     /* the citation tag in an answer */
  --read-base-search: 14px;      /* the excerpt of a search hit */
  --read-base-search-line: 1.7;
  --read-base-title: 22px;       /* a page title */
  --read-base-sub: 13px;         /* a subtitle under it */
  --read-base-section: 15px;     /* a section heading in a card */
  --read-base-card-title: 16px;
  --read-base-card-desc: 13px;
  --read-base-stat: 26px;        /* the big number */
  --read-base-stat-sm: 19px;     /* the compact one */
  --read-base-stat-label: 13px;
  --read-base-table: 13px;
  --read-base-wiki-h1: 26px;
  --read-base-wiki-h2: 21px;
  --read-base-wiki-h3: 18px;
  --read-base-wiki-h4: 16px;
  --read-base-wiki-h5: 15px;
  --read-base-wiki-h6: 14px;
  --read-base-wiki-table: 14px;
  --read-base-wiki-pre: 13px;
  --read-base-login-title: 20px;
  --read-base-login-label: 13px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
          "Microsoft YaHei", "Noto Sans CJK SC", "Hiragino Sans GB", sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* The colours of the chat page.
   *
   * The chat page is laid out like DeepSeek's, and DeepSeek's greys are
   * a little blue rather than neutral. Changing the whole site's greys
   * for that would touch the top bar, the cards and the badges of every
   * other page, so the chat page carries its own.
   *
   * These are measured off DeepSeek's own stylesheet:
   *   --dsw-static-neutral-bluish-1000  #0f1115  the text
   *   --dsw-static-neutral-bluish-700    #61666b  the second-level text
   *   --dsw-static-neutral-bluish-600    #81858c  the third-level text
   *   --dsw-static-neutral-bluish-75     #f1f3f5  the hover
   *   --dsw-static-neutral-bluish-100    #ebeef2  the inline code
   *   --dsw-static-deepseek-500         #3964fe  the accent
   *   --dsw-static-deepseek-100         #e4edfd  the accent, pale
   */
  --chat-text: #0f1115;
  --chat-text-soft: #61666b;
  --chat-text-faint: #81858c;
  /* The number inside a citation tag.
   *
   * It has a colour of its own because the tag is a link, and a link in
   * an answer is the answer's blue: the number would lose its grey to
   * the rule for links. DeepSeek keeps the number grey in both themes,
   * so the tag carries the colour rather than the link around it. */
  --chat-cite-text: #81858c;
  /* The shade of a mark that is not text: the line at the left of a
     quotation. */
  --chat-caption: #adb2b8;
  /* The line at the left of the model's thinking. It is barely there:
     the thinking is the machine talking, and the line says where its
     words begin. */
  --chat-dimmed: #e1e5ee;
  --chat-border: rgba(0, 0, 0, .1);
  --chat-border-faint: rgba(0, 0, 0, .04);
  --chat-border-strong: rgba(0, 0, 0, .12);
  --chat-hover: #f1f3f5;
  --chat-code-bg: #ebeef2;
  --chat-code-block-bg: #f9fafb;
  --chat-cite-bg: #f5f6f7;
  /* The ground of a citation tag under the mouse: a step darker than
     the tag itself, the way DeepSeek's is. */
  --chat-cite-hover-bg: #ebeef2;
  /* The layer behind the drawer on a phone. The site's own overlay is
     a shade deeper (35%); DeepSeek's is 24%. */
  --chat-overlay: rgba(0, 0, 0, .24);
  /* The paper of the box the question is written in.
   *
   * In the light theme it is a shade deeper than the page, so that the
   * box stands out from the paper it sits on. In the dark theme it is
   * a shade lighter, the way DeepSeek's is: a box that is darker than
   * the page is a hole in it, and a reader writes into a hole. */
  --chat-input-bg: #f6f7f9;
  --chat-accent: #3964fe;
  /* The blue of a word in the answer, as against the blue of a button.
   *
   * They are the same in the light theme. In the dark one DeepSeek's
   * text blue is a step lighter than its button blue, because text on
   * a dark paper has to be lighter than a block of colour. */
  --chat-accent-text: #3964fe;
  --chat-accent-bg: #edf3fe;
  /* The text on the accent: white, in both themes. */
  --chat-on-accent: #ffffff;
}

/* The dark theme.
 *
 * The attribute is set on the html element by an inline script in the head,
 * before the page paints, so a reader who chose the dark theme does not see
 * a flash of the light one while the page loads.
 *
 * The colours are the same relationships as the light ones: a near-black
 * background, a surface a little lighter than it, a text colour that is
 * bright but not glaring. The tints behind the chips are dark versions of
 * the light tints, so a chip still reads as "this is the state" rather than
 * as a block of colour.
 */
[data-theme="dark"] {
  --bg: #14161a;
  --surface: #1c1f24;
  --surface-alt: #21252b;
  --border: #2c3037;
  --border-strong: #3a4049;
  --text: #e6e8eb;
  --text-body: #dfe2e6;
  --text-soft: #c2c7ce;
  --muted: #9aa1ab;
  --accent: #4f8ef7;
  --ok: #4ade80;
  --warn: #fbbf24;
  --err: #f87171;
  --ok-bg: #14331f;
  --ok-border: #1f5133;
  --warn-bg: #3a2c0c;
  --warn-border: #5c4715;
  --err-bg: #3d1a1a;
  --err-border: #5f2828;
  --accent-bg: #16294a;
  --on-accent: #0b1220;
  --shadow: 0 1px 2px rgba(0,0,0,.5);
  --shadow-2: 0 4px 16px rgba(0,0,0,.55);
  --overlay: rgba(0, 0, 0, .6);

  /* The chat page's own colours, in the dark. DeepSeek's dark greys:
     the text is the white DeepSeek uses, and the pale tints become the
     dark ones they are in its stylesheet. */
  --chat-text: #f9fafb;
  --chat-text-soft: #b4b6ba;
  --chat-text-faint: #8a8d92;
  /* The number inside a citation tag: DeepSeek's own third-level grey
     in the dark is a step lighter than the light theme's, and the tag
     follows it. */
  --chat-cite-text: #adb2b8;
  --chat-caption: #81858c;
  --chat-dimmed: #43454a;
  --chat-border: rgba(255, 255, 255, .12);
  --chat-border-faint: rgba(255, 255, 255, .06);
  --chat-border-strong: rgba(255, 255, 255, .16);
  --chat-hover: #2c2c2e;
  --chat-code-bg: #2c2c2e;
  --chat-code-block-bg: #1b1b1c;
  --chat-cite-bg: #2c2c2e;
  /* The ground of a citation tag under the mouse: in the dark it is a
     step lighter than the tag, the way DeepSeek's is. */
  --chat-cite-hover-bg: #353638;
  --chat-overlay: rgba(0, 0, 0, .5);
  --chat-input-bg: #2c2c2e;
  --chat-accent: #5686fe;
  --chat-accent-text: #679efe;
  --chat-accent-bg: #2c2c2e;
  --chat-on-accent: #ffffff;
}
* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--text);
  font-family: var(--font);
  /* The text every page reads in, and the leading between its lines.
     The top bar and the controls take their own fixed sizes instead, so
     that they keep their shape when the reader enlarges the text. */
  font-size: calc(var(--read-base-body) * var(--read-scale));
  line-height: calc(1.6 * var(--read-line-scale));
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------- layout ---------- */
.container {
  max-width: 1100px;
  margin: 0 auto;
  padding: var(--sp-5) var(--sp-4);
}
/* The wiki pages carry three columns (the tree, the article, the
   backlinks), so they need more room than the portal and the status pages
   do. This used to be a dead rule: the wiki shell asked for 1400px but sat
   inside the 1100px .container, so the request was never met and the
   article column was squeezed to 536px -- narrower than the article got on
   a 1100px screen, where the backlinks move to a second row. */
.container-wide {
  max-width: 1280px;
}
.grid {
  display: grid;
  gap: var(--sp-4);
  align-items: stretch;
}
.grid.cols-4 {
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.spacer { flex: 1; }

/* spacing utilities (the stylesheet is the only place inline style is banned) */
.mt-2 { margin-top: var(--sp-2); }
.pt-2 { padding-top: var(--sp-2); }
.mb-3 { margin-bottom: var(--sp-3); }
.mb-4 { margin-bottom: var(--sp-4); }
.mb-5 { margin-bottom: var(--sp-5); }
.w-140 { min-width: 140px; }

/* ---------- top navigation ---------- */
.topnav {
  position: sticky;
  top: 0;
  z-index: 20;
  /* The bar is tall enough for the notch on a phone that has one: the
     row of links sits below it, not behind it. */
  height: 56px;
  height: calc(56px + env(safe-area-inset-top));
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  padding: env(safe-area-inset-top) var(--sp-4) 0;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  /* The bar is operated, not read. It takes the base size without the
     reader's multiplier, so the links and the buttons keep the row's
     shape however large the text on the page gets. */
  font-size: var(--read-base-body);
}
.brand {
  font-weight: 600;
  font-size: 15px;
  margin-right: var(--sp-3);
}
.brand a { color: var(--text); }
.nav-link {
  padding: 0 12px;
  border-radius: var(--radius-sm);
  color: var(--text);
  line-height: 32px;
}
.nav-link:hover {
  text-decoration: none;
  background: var(--bg);
}
.nav-link.active {
  color: var(--accent);
  background: var(--bg);
}
/* The brand is the only entry to the portal, so it says "you are here"
   when the current page is the portal. The rest of the brand's look
   stays as it is. */
.brand a.active { color: var(--accent); }

/* ---------- the market menu ---------- */

/* The container only positions the panel; it is the switch and the panel
   together, so that a click on either counts as a click on the menu. */
.nav-menu { position: relative; }

/* The switch is a button laid out like the neighbouring links. `.nav-link`
   was written for <a>, so the button takes the font and the rest of the
   link's shape from it rather than from the browser's own button style. */
.nav-menu-toggle {
  font: inherit;
  border: 0;
  background: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.nav-menu-caret { width: 14px; height: 14px; transition: transform .15s; }
.nav-menu.open .nav-menu-caret { transform: rotate(180deg); }

/* The panel hangs below the switch, left edges aligned, the way the
   reading settings panel hangs below its own button. */
.nav-menu-panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: 160px;
  padding: var(--sp-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  z-index: 40;
}
.nav-menu-panel[hidden] { display: none; }
.nav-menu-link {
  display: block;
  padding: 6px var(--sp-3);
  border-radius: var(--radius-sm);
  color: var(--text);
  white-space: nowrap;
}
.nav-menu-link:hover {
  text-decoration: none;
  background: var(--bg);
}
.nav-menu-link.active {
  color: var(--accent);
  background: var(--bg);
}
.user-chip {
  font-size: 13px;
  color: var(--muted);
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A reader's name is shown in the quiet colour: the site's owner has
   the whole machine, a reader has only the library. */
.user-chip.reader { background: var(--bg); color: var(--muted); }

/* The theme switch. It shows the theme it would switch to, not the one in
   use, so the reader knows what a click does without having to remember. */
.theme-toggle {
  width: 34px;
  height: 30px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
}
.theme-toggle:hover { background: var(--bg); }

/* The button shows the theme it would switch to. The icons are drawn
   rather than typed: a machine without an emoji font would show the
   character as a blank box. */
.theme-icon { width: 17px; height: 17px; display: none; }
[data-theme="light"] .theme-icon-moon { display: block; }
[data-theme="dark"] .theme-icon-sun { display: block; }

/* ---------- the reading settings ---------- */

/* The button that opens the panel. It carries the same shape as the
   theme switch, so the two sit together as one group. */
.settings-toggle { position: relative; }

/* The panel itself. It hangs below the button, on the right, so it does
   not cover the links in the top bar. */
.settings-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 260px;
  padding: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  z-index: 40;
}
.settings-panel[hidden] { display: none; }

.settings-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 var(--sp-3);
}

.settings-row { margin-bottom: var(--sp-3); }
.settings-row:last-child { margin-bottom: 0; }
.settings-label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 12px;
  color: var(--muted);
  margin-bottom: var(--sp-1);
}
.settings-value {
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* A range input, restyled: the default one is too small to hit with a
   thumb on a phone. */
.settings-range {
  width: 100%;
  height: 22px;
  margin: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
}
.settings-range::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: var(--border-strong);
}
.settings-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface);
  box-shadow: var(--shadow);
}
.settings-range::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--border-strong);
}
.settings-range::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface);
}
.settings-range:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The panel's own button keeps the size the panel was laid out at; it
   carries .btn, whose rule comes later and would otherwise win. */
.btn.settings-reset {
  width: 100%;
  margin-top: var(--sp-3);
  font-size: 12px;
}

/* ---------- typography ---------- */
.page-title {
  font-size: calc(var(--read-base-title) * var(--read-scale));
  line-height: calc(1.6 * var(--read-line-scale));
  font-weight: 600;
  margin: 0 0 var(--sp-1);
}
.page-sub {
  color: var(--muted);
  font-size: calc(var(--read-base-sub) * var(--read-scale));
  /* The subtitle is prose: the gap under it is the break between the
     page's own words and what comes next, so it follows the reader's
     paragraph gap. The gap above the title is layout and does not. */
  margin: 0 0 calc(var(--sp-5) * var(--read-gap-scale));
}
.muted { color: var(--muted); }
.mono { font-family: var(--mono); }
.error { color: var(--err); }
.section-title {
  font-size: calc(var(--read-base-section) * var(--read-scale));
  font-weight: 600;
  /* A heading is followed by the text it heads; that break is a paragraph
     break, so the reader's gap moves it. */
  margin: 0 0 calc(var(--sp-3) * var(--read-gap-scale));
}

/* ---------- card ---------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-4);
  box-shadow: var(--shadow);
}
.card-title {
  font-size: calc(var(--read-base-card-title) * var(--read-scale));
  font-weight: 600;
  /* The space under a card's heading is the break to the description
     under it, which the reader reads next, so it follows the gap. */
  margin: var(--sp-2) 0 calc(var(--sp-1) * var(--read-gap-scale));
}
.card-desc {
  font-size: calc(var(--read-base-card-desc) * var(--read-scale));
  color: var(--muted);
  /* No margin of its own. The space under a card's description is set by
     the card (the `.mt-2` on the block that follows it); giving the
     description a margin would push every card down at the reader's
     default size, and one is what the page would have used anyway. */
}

/* ---------- stat block ---------- */
.stat {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}
.stat-label {
  font-size: calc(var(--read-base-stat-label) * var(--read-scale));
  color: var(--muted);
}
.stat-value {
  font-size: calc(var(--read-base-stat) * var(--read-scale));
  font-weight: 600;
  font-family: var(--mono);
  /* A number is one line, so there is no leading between lines to set:
     the 1.3 only trims the box, and it trims it in proportion. */
  line-height: 1.3;
  white-space: nowrap;
}
/* Compact variant for values that carry several numbers (load 1/5/15, net rx/tx):
   at 26px they overflow a 4-column grid cell and break across lines.
 *
 * Breaking across lines is what these may do when they no longer fit: at
 * the reader's larger size three numbers do not fit one cell, and a
 * number that runs past the edge of its card is worse than one that
 * takes a second line. At the page's own size each still fits on one. */
.stat-value.sm {
  font-size: calc(var(--read-base-stat-sm) * var(--read-scale));
  letter-spacing: -.01em;
  white-space: normal;
}
.stat-bar {
  height: 4px;
  background: var(--border);
  border-radius: 999px;
  margin-top: var(--sp-2);
  overflow: hidden;
}
.stat-bar > i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
}
.stat-bar.hot > i { background: var(--err); }

/* ---------- button ---------- */
.btn {
  padding: 6px 14px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font: inherit;
  /* A button is a control: it takes the page's text size as it stood
     before the reader touched the panel, so that the row it sits in keeps
     its height however large the text around it gets. */
  font-size: var(--read-base-body);
  /* A squeezed button used to stack its label into one character per line. */
  white-space: nowrap;
}
.btn:hover { background: var(--bg); }
.btn.primary {
  background: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}
.btn.primary:hover { filter: brightness(1.06); }
.btn:disabled {
  opacity: .45;
  cursor: default;
}

/* ---------- table ---------- */
.table {
  width: 100%;
  border-collapse: collapse;
}
.table th {
  text-align: left;
  color: var(--muted);
  font-size: calc(var(--read-base-table) * var(--read-scale));
  font-weight: 500;
  padding: var(--sp-2) var(--sp-2) var(--sp-2) 0;
  border-bottom: 1px solid var(--border);
}
.table td {
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.table th:last-child, .table td:last-child { padding-right: 0; }
.table tbody tr:hover td { background: var(--bg); }
.table th.sortable { cursor: pointer; user-select: none; }
.table th.sorted-asc::after { content: " ▲"; font-size: 10px; }
.table th.sorted-desc::after { content: " ▼"; font-size: 10px; }

/* ---------- chip ---------- */
.chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--muted);
}
.chip.ok { color: var(--ok); background: var(--ok-bg); border-color: var(--ok-border); }
.chip.stale { color: var(--warn); background: var(--warn-bg); border-color: var(--warn-border); }
.chip.err { color: var(--err); background: var(--err-bg); border-color: var(--err-border); }
.chip.unknown { color: var(--muted); background: var(--bg); border-color: var(--border); }

/* ---------- form controls ---------- */
.input {
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  width: 100%;
  font: inherit;
  /* A field is a control: it keeps the page's own text size, so a longer
     label does not make the form taller than the card it sits in. */
  font-size: var(--read-base-body);
  color: var(--text);
  background: var(--surface);
}
.input:focus {
  outline: none;
  border-color: var(--accent);
}
select.input {
  width: auto;
  padding-right: var(--sp-2);
}

/* ---------- login ---------- */
.login-wrap {
  min-height: 100vh;
  display: grid;
  place-items: center;
}
.login-card {
  width: 100%;
  max-width: 360px;
}
.login-card h1 {
  font-size: calc(var(--read-base-login-title) * var(--read-scale));
  font-weight: 600;
  text-align: center;
  margin: 0 0 var(--sp-5);
}
.login-card label {
  display: block;
  margin-bottom: var(--sp-4);
  font-size: calc(var(--read-base-login-label) * var(--read-scale));
  color: var(--muted);
}
.login-card label .input { margin-top: var(--sp-1); }
.login-card .btn { width: 100%; }
.login-card .error {
  background: var(--err-bg);
  border: 1px solid var(--err-border);
  border-radius: var(--radius-sm);
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-4);
}
/* A visitor who came here from the portal needs a way back to it. */
.login-back {
  margin: var(--sp-4) 0 0;
  text-align: center;
  font-size: calc(var(--read-base-login-label) * var(--read-scale));
}
/* The rules a name and a password have to follow, so that a reader
   learns them before the form refuses them. */
.login-note {
  margin: 0 0 var(--sp-3);
  font-size: calc(var(--read-base-login-label) * var(--read-scale));
  line-height: calc(1.6 * var(--read-line-scale));
}

/* ---------- status page ---------- */
/* The four live charts: two columns on a wide screen, one on a phone. */
.status-charts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-4);
}
.status-chart-card {
  display: flex;
  flex-direction: column;
}
.status-chart-value {
  font-size: calc(var(--read-base-stat) * var(--read-scale));
  font-weight: 600;
  font-family: var(--mono);
  line-height: 1.3;
}
/* The legend: which line is which. Each entry is a dot and its name, so
   the dot stays next to the name it belongs to and the entries are
   spaced apart from each other. */
.chart-key {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  font-size: calc(var(--read-base-sub) * var(--read-scale));
  color: var(--muted);
  margin-top: var(--sp-1);
}
.chart-key-item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
}
.chart-key-dot {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  display: inline-block;
}
.chart-key-dot.read { background: var(--accent); }
.chart-key-dot.write { background: var(--warn); }
.status-chart {
  display: block;
  width: 100%;
  height: 90px;
  margin-top: var(--sp-2);
}
/* The network rates are numbers that get compared against each other, so
   they sit in narrow right-aligned columns instead of spreading across the
   width of the card. */
.status-nic-table td:not(:first-child),
.status-nic-table th:not(:first-child) {
  width: 120px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.status-services {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}
.status-service {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.status-service .name {
  font-size: calc(var(--read-base-sub) * var(--read-scale));
  color: var(--muted);
}

/* ---------- usage page ---------- */
/* The quota windows of one subscription. On a wide screen they stand
   side by side; a phone gives each window a row of its own (the
   narrow-screen rule below). */
.usage-grid {
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
/* One quota window: the share of the quota that is used up, a bar for
   it, and the time the window has left to run. */
.usage-window {
  min-width: 0;
}
/* The line that reads "额度剩余 8% · 窗口还有 4 小时 12 分" is one
   statement; a column narrow enough to break it after "12" would make
   the reader put the two halves together themselves. It wraps between
   its two halves, never inside one. */
.usage-meta > span {
  white-space: nowrap;
}
/* The bar says how much of the quota is gone, so its colour says it
   too: green while there is plenty, amber when it is getting short,
   red when there is barely any left. */
.usage-bar.ok > i { background: var(--ok); }
.usage-bar.warn > i { background: var(--warn); }
.usage-bar.err > i { background: var(--err); }
/* The number wears the same colour as its bar, so that the reading
   does not depend on which part of the card the eye lands on. */
.usage-value.usage-ok { color: var(--ok); }
.usage-value.usage-warn { color: var(--warn); }
.usage-value.usage-err { color: var(--err); }
/* The plan's name sits next to the subscription's name; the two are
   one line, not two. */
.usage-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

/* The button that reads one subscription's numbers again, without
   waiting for the minute to pass.
 *
 * It is a control, not text: it keeps its own size whatever the reader
 * has set the reading size to, and it sits at the right of the card's
 * heading, out of the way of the name. */
.usage-refresh {
  margin-left: auto;
  width: 26px;
  height: 26px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--muted);
  border-radius: var(--radius-sm);
  cursor: pointer;
  line-height: 1;
}
.usage-refresh:hover { background: var(--bg); color: var(--text); }
.usage-refresh:disabled { cursor: default; }
.usage-refresh svg { width: 14px; height: 14px; flex: none; }
/* While the numbers are on their way the arrow turns, and the button
   says so.
 *
   The upstream may take a few seconds to answer, and a greyed-out
   button that only spins says nothing about how long the wait will be.
   The button grows into a small pill that says what it is doing, which
   reads at a glance and leaves the reader free to do something else. */
.usage-refresh .usage-refresh-text { display: none; }
.usage-refresh.busy {
  width: auto;
  padding: 0 8px;
  gap: 4px;
  color: var(--accent);
}
.usage-refresh.busy .usage-refresh-text { display: inline; font-size: 12px; }
.usage-refresh.busy svg { animation: usage-spin .9s linear infinite; }
@keyframes usage-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .usage-refresh.busy svg { animation: none; }
}

/* The line that says where a subscription's numbers come from, for the
   one whose numbers are a summary of another site's page. */
.usage-source {
  margin-top: var(--sp-2);
  font-size: 12px;
}
.usage-source-link {
  color: var(--accent);
  text-decoration: none;
}
.usage-source-link:hover { text-decoration: underline; text-underline-offset: 3px; }
.usage-source-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/* The field that takes in a credential a subscription needs, for the
   subscriptions whose numbers cannot be read from the proxy's
   configuration.
 *
 * It opens itself when the card could not be read, because that is the
 * moment the reader has to do something, and folds away otherwise so
 * that a card that is working is not carrying an empty box. */
.usage-credential {
  margin-top: var(--sp-3);
  border-top: 1px solid var(--border);
  padding-top: var(--sp-3);
}
.usage-credential-toggle {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex-wrap: wrap;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  color: var(--muted);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.usage-credential-toggle:hover .usage-credential-what { color: var(--text); }
.usage-credential-what {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.usage-credential-hint { font-size: 12px; }
.usage-credential-form { margin-top: var(--sp-3); }
.usage-credential-label {
  display: block;
  font-size: 12px;
  color: var(--muted);
  margin-bottom: var(--sp-1);
}
/* The cookie is one long line of characters with no spaces in it, so
   the field wraps anywhere rather than running off the card. */
.usage-credential-input {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.5;
  word-break: break-all;
  resize: vertical;
}
.usage-credential-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.usage-credential-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-top: var(--sp-2);
}
.usage-credential-save {
  padding: 5px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.usage-credential-save:hover { background: var(--bg); }
.usage-credential-save:disabled { cursor: default; color: var(--muted); }
.usage-credential-msg { font-size: 12px; color: var(--muted); }

/* ---------- legacy pages (kuaixun / profit forecast) ---------- */
.news-item {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-3);
  box-shadow: var(--shadow);
}
.news-time { font-size: 12px; color: var(--muted); margin-bottom: var(--sp-1); }
/* The title is a step above the body, and both follow the reading
   settings: the headline is read as part of the same block. */
.news-title {
  font-size: calc(var(--read-base-news-title) * var(--read-scale));
  font-weight: 600;
  line-height: calc(1.6 * var(--read-line-scale));
  margin-bottom: var(--sp-2);
}
.news-summary {
  font-size: calc(var(--read-base-news-summary) * var(--read-scale));
  color: var(--text);
  line-height: calc(1.6 * var(--read-line-scale));
  margin-bottom: calc(var(--read-base-news-gap) * var(--read-gap-scale));
}
.news-stocks { display: flex; gap: var(--sp-1); flex-wrap: wrap; }
.stock-tag {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--accent);
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  font-size: 12px;
  font-family: var(--mono);
}
.rating-badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  font-size: 12px;
  font-weight: 600;
}
.rating-buy { color: var(--ok); background: var(--ok-bg); }
.rating-add { color: var(--accent); background: var(--accent-bg); }
.pagination {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  justify-content: center;
  margin-top: var(--sp-4);
}
.pagination span { color: var(--muted); font-size: 13px; }
.controls {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  margin-bottom: var(--sp-4);
  flex-wrap: wrap;
}
.controls .input { width: auto; flex: 1; min-width: 220px; }
.truncate {
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- quick switcher (Ctrl+O) ---------- */
.switcher-mask {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  display: grid;
  place-items: start center;
  padding-top: 12vh;
  z-index: 50;
}
.switcher-panel {
  width: min(640px, 92vw);
  max-height: 60vh;
  overflow: auto;
  padding: var(--sp-3);
}
.switcher-input {
  margin-bottom: var(--sp-2);
}
.switcher-item {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.switcher-item.active { background: var(--bg); }
.switcher-item .title { font-weight: 600; }
.switcher-item .path {
  color: var(--muted);
  font-size: 12px;
  font-family: var(--mono);
}
.switcher-empty {
  color: var(--muted);
  font-size: 13px;
  padding: var(--sp-2);
}
.switcher-hint {
  color: var(--muted);
  font-size: 12px;
  padding-top: var(--sp-2);
  border-top: 1px solid var(--border);
  margin-top: var(--sp-2);
}

/* ---------- wiki ---------- */
.wiki-shell {
  max-width: 1400px;
  margin: 0 auto;
}
.wiki-topbar {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}
.wiki-crumbs {
  font-size: 13px;
  color: var(--muted);
  /* The crumb trail does not wrap.
   *
   * A page name can be long, and the trail broke in the middle of it --
   * "202608-end-to-end-…" split across two lines. The top bar does not
   * wrap, so the trail grew taller and the row of buttons no longer sat
   * level with it. The trail scrolls sideways instead, the way the
   * narrow-screen rule already does it.
   *
   * The line is as tall as the buttons beside it, so that the text of
   * the trail and the text of the buttons sit on the same line: a
   * twenty-one-pixel line centred in a thirty-six-pixel row sits lower
   * than the thirty-six-pixel buttons do. */
  white-space: nowrap;
  word-break: normal;
  overflow-x: auto;
  scrollbar-width: none;
  min-width: 0;
  flex: 0 1 auto;
  line-height: 36px;
  height: 36px;
}
.wiki-crumbs::-webkit-scrollbar { display: none; }
.wiki-crumb-sep { color: var(--border); margin: 0 2px; }
.wiki-crumb-current { color: var(--text); }

/* The search box of the library.
 *
 * It sits in the top bar next to the bread crumbs. The box is a fixed
 * width so that the crumbs beside it keep their shape when a search is
 * typed into it. */
.wiki-search {
  display: flex;
  align-items: center;
  /* The button is right against the input, so the two read as one
     control rather than as two things that happen to sit side by side. */
  gap: var(--sp-1);
}
.wiki-search-input {
  width: 200px;
  padding: 5px 9px;
  font-size: 13px;
}
/* The submit button is an icon rather than a word.
 *
 * The word cost the top bar thirty pixels of width, and the top bar
 * does not have them to spare: the search box is the one thing in the
 * row that can give way, so it was the search box that shrank. With the
 * icon the button takes thirty-two.
 *
 * A reader who cannot see the icon still gets the label, which is what a
 * screen reader reads out.
 *
 * The icon is drawn, not typed: a machine without an emoji font would
 * show the character as a blank box. */
.wiki-search-btn {
  width: 32px;
  height: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The button must keep its shape when the bar gets tight; it is the
     input beside it that gives way. */
  flex: none;
}
.wiki-search-btn svg {
  display: block;
  width: 16px;
  height: 16px;
}
.wiki-search-page {
  max-width: 780px;
}
.wiki-search-count {
  margin: var(--sp-3) 0 var(--sp-2);
  font-size: calc(var(--read-base-sub) * var(--read-scale));
}
.wiki-search-hits {
  list-style: none;
  margin: 0;
  padding: 0;
}
.wiki-search-hit {
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--border);
}
.wiki-search-hit-title {
  font-weight: 600;
  color: var(--accent);
}
.wiki-search-hit-excerpt {
  margin-top: var(--sp-1);
  color: var(--text-body);
  /* The excerpt is the part of a hit a reader reads to decide which
     hit to open, so it follows the reader like the rest of the prose. */
  font-size: calc(var(--read-base-search) * var(--read-scale));
  line-height: calc(var(--read-base-search-line) * var(--read-line-scale));
}
.wiki-search-hit-path {
  margin-top: var(--sp-1);
  font-size: 12px;
}
.wiki-layout {
  display: grid;
  /* The article column is the one that has to be wide enough to read: at
     16px, 660px of text is about 38 Chinese characters per line, which is
     the comfortable range. The tree and the backlinks are reference
     material, so they take what is left. */
  grid-template-columns: 240px minmax(0, 1fr) 240px;
  gap: var(--sp-4);
  align-items: start;
}
/* The original opened beside the page.
 *
 * Two panes: the page on the left, the original on the right. The tree
 * and the backlinks step aside for the original -- they are navigation
 * aids, and a reader who has the document open is reading, not looking
 * for another page.
 *
 * The article keeps its reading width, so the text does not stretch when
 * the original opens; the original takes what is left. */
.wiki-layout.pdf-open {
  grid-template-columns: minmax(0, 1fr) minmax(480px, 1fr);
}
.wiki-layout.pdf-open .wiki-tree,
.wiki-layout.pdf-open .wiki-side {
  display: none;
}
.wiki-pdf-panel[hidden] { display: none; }
.wiki-pdf-panel {
  position: sticky;
  top: 72px;
  height: calc(100vh - 96px);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
/* The names of the documents a page was written from.
 *
 * A page may name six papers, and six names do not fit across the panel:
 * measured at 1440px, the row came to 1479px in a panel 614px wide, so
 * four of the six sat outside it. Scrolling was possible but showed
 * nothing about how many there were -- the third name ended against the
 * edge, which reads as a broken row rather than a long one.
 *
 * Squeezing them was worse: at 132px each they came out as "Cont, K…"
 * and "Cont, Cu…", and two papers by Cont could not be told apart.
 *
 * So the names take the room they need, and the row wraps. The panel
 * gives up about seventy pixels of the document to say which documents
 * these are; the document is 750px tall and does not miss them. */
.wiki-pdf-doc-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  padding: var(--sp-2);
  border-bottom: 1px solid var(--border);
  background: var(--surface-alt);
}
.wiki-pdf-tab {
  padding: 4px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  /* A name breaks between its words when it must, never inside one. */
  text-align: left;
}
.wiki-pdf-tab:hover { background: var(--bg); color: var(--text); }
.wiki-pdf-tab.active {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}
/* One original: there is nothing to choose between, so the panel names
   the document instead of offering a tab. */
.wiki-pdf-single {
  font-size: 13px;
  color: var(--text-soft);
  padding: 4px 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.wiki-pdf-frame {
  flex: 1;
  width: 100%;
  border: 0;
  background: var(--bg);
}
.wiki-tree, .wiki-side {
  position: sticky;
  top: 72px;
  max-height: calc(100vh - 96px);
  overflow: auto;
}
.wiki-tree-body {
  font-size: 13px;
}
/* The tree fold: on a wide screen the heading is an ordinary section title
   and the tree is always open; on a narrow screen the heading becomes the
   switch that opens the tree. The checkbox is the switch and the label is
   the clickable heading -- no JavaScript, no user-agent sniffing. */
.wiki-tree-toggle { display: none; }
.wiki-tree-summary {
  display: block;
  cursor: default;
  /* The tree is a control, so its heading keeps the page's own size and
     its own spacing rather than following the reader's; it carries
     .section-title, which does follow. */
  font-size: var(--read-base-section);
  margin-bottom: var(--sp-3);
  /* Nothing on a wide screen toggles the tree, so the heading must not
     forward clicks to the (hidden) checkbox. */
  pointer-events: none;
}
.wiki-tree-summary::after { content: none; }
.wiki-tree-folder > summary {
  cursor: pointer;
  padding: 3px 0;
  font-weight: 500;
  list-style: none;
}
.wiki-tree-folder > summary::-webkit-details-marker { display: none; }
.wiki-tree-folder > summary::before {
  content: "▸";
  display: inline-block;
  width: 14px;
  color: var(--muted);
}
.wiki-tree-folder[open] > summary::before { content: "▾"; }
.wiki-tree-folder > * { margin-left: 12px; }
.wiki-tree-file {
  display: block;
  padding: 3px 6px;
  border-radius: var(--radius-sm);
  color: var(--text);
  line-height: 1.35;
}
.wiki-tree-file:hover { background: var(--bg); text-decoration: none; }
.wiki-tree-file.active { background: var(--bg); color: var(--accent); }
.wiki-tree-title { display: block; }
.wiki-tree-path {
  display: block;
  font-size: 11px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wiki-content { min-width: 0; position: relative; }
/* With the original open, the article is one of two panes. It keeps the
   width it is comfortable at rather than stretching to fill the pane. */
.wiki-layout.pdf-open .wiki-content { max-width: 760px; }
.wiki-notice {
  background: var(--bg);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-3);
  font-size: 13px;
  color: var(--muted);
}
.wiki-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1);
  margin-bottom: var(--sp-4);
}
/* The reading column.
 *
 * The type is set for reading Chinese prose in a narrow column: 16px with a
 * 1.75 line height puts about 30 characters on a line, which is the width
 * Chinese text is comfortable at. The line height also has to leave room for
 * a formula, which is taller than the text around it.
 *
 * `overflow-wrap: anywhere` lets a long URL break instead of stretching the
 * column, but it also lets a word break in the middle of a Chinese sentence;
 * `word-break: normal` keeps the Chinese intact and leaves the URL rule to
 * the anchors themselves.
 */
.wiki-body {
  overflow-wrap: anywhere;
  word-break: normal;
  font-size: calc(var(--read-base-size) * var(--read-scale));
  line-height: calc(var(--read-base-line) * var(--read-line-scale));
  color: var(--text-body);
  /* The article is the thing that scrolls sideways, not the page.
   *
   * A table at the reader's larger size can be wider than the reading
   * column -- its header cells do not wrap -- and without this the whole
   * page grew a horizontal scrollbar: the top bar and the tree moved out
   * of view with it. The article scrolls on its own instead. At the
   * page's own size nothing overflows, so a reader who has not touched
   * the panel sees the page unchanged. */
  overflow-x: auto;
}
.wiki-body > *:first-child { margin-top: 0; }
.wiki-body p { margin: calc(var(--read-base-gap) * var(--read-gap-scale)) 0; }

/* The heading levels. The corpus runs h1 to h4; h5 and h6 are styled too so
   a page that uses them is not left at the browser's default size, which is
   smaller than the body text and reads as an accident.
 *
 * The gap above a heading grows with its level, so the section break is
 * visible without the heading having to shout. */
.wiki-body h1 {
  font-size: calc(var(--read-base-wiki-h1) * var(--read-scale));
  line-height: 1.35;
  font-weight: 600;
  margin: var(--sp-6) 0 var(--sp-3);
}
.wiki-body h2 {
  font-size: calc(var(--read-base-wiki-h2) * var(--read-scale));
  line-height: 1.4;
  font-weight: 600;
  margin: var(--sp-6) 0 var(--sp-3);
  padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--border);
}
.wiki-body h3 {
  font-size: calc(var(--read-base-wiki-h3) * var(--read-scale));
  line-height: 1.45;
  font-weight: 600;
  margin: var(--sp-5) 0 var(--sp-2);
}
.wiki-body h4 {
  font-size: calc(var(--read-base-wiki-h4) * var(--read-scale));
  line-height: 1.5;
  font-weight: 600;
  margin: var(--sp-4) 0 var(--sp-2);
  color: var(--text-soft);
}
.wiki-body h5 {
  font-size: calc(var(--read-base-wiki-h5) * var(--read-scale));
  line-height: 1.5;
  font-weight: 600;
  margin: var(--sp-4) 0 var(--sp-2);
  color: var(--muted);
}
.wiki-body h6 {
  font-size: calc(var(--read-base-wiki-h6) * var(--read-scale));
  line-height: 1.5;
  font-weight: 600;
  margin: var(--sp-3) 0 var(--sp-1);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .04em;
}
/* A heading that is the first thing in the body has no gap above it. */
.wiki-body > h1:first-child,
.wiki-body > h2:first-child { margin-top: 0; }

/* Lists. The marker is muted so it does not compete with the text, and the
   item spacing is tighter than the paragraph spacing -- a list is read as
   one block. */
.wiki-body ul, .wiki-body ol { margin: var(--sp-3) 0; padding-left: 1.6em; }
.wiki-body li { margin: var(--sp-1) 0; }
.wiki-body li > ul, .wiki-body li > ol { margin: var(--sp-1) 0; }
.wiki-body li::marker { color: var(--muted); }
.wiki-body ul ul, .wiki-body ol ol,
.wiki-body ul ol, .wiki-body ol ul { list-style-type: circle; }

/* Tables. The row height comes from the padding rather than from a border on
   every cell, and only the horizontal rules are drawn: a table with a grid
   of grey boxes around every cell reads as noise at this width. */
.wiki-body table {
  width: 100%;
  border-collapse: collapse;
  font-size: calc(var(--read-base-wiki-table) * var(--read-scale));
  line-height: calc(1.6 * var(--read-line-scale));
  margin: var(--sp-4) 0;
}
.wiki-body th, .wiki-body td {
  border: none;
  border-bottom: 1px solid var(--border);
  padding: 8px 10px;
  text-align: left;
  vertical-align: top;
}
.wiki-body th {
  background: var(--bg);
  font-weight: 600;
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
}
.wiki-body tbody tr:last-child td { border-bottom: none; }

.wiki-body code {
  font-family: var(--mono);
  font-size: 0.88em;
  background: var(--bg);
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  /* A long inline identifier must not stretch the column. */
  overflow-wrap: anywhere;
}
.wiki-body pre {
  background: var(--bg);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--radius-sm);
  overflow: auto;
  line-height: 1.6;
  font-size: calc(var(--read-base-wiki-pre) * var(--read-scale));
  margin: var(--sp-4) 0;
}
.wiki-body pre code { background: none; padding: 0; font-size: inherit; }
.wiki-body blockquote {
  margin: var(--sp-4) 0;
  padding: var(--sp-2) var(--sp-4);
  border-left: 3px solid var(--accent);
  background: var(--surface-alt);
  color: var(--text-soft);
}
.wiki-body blockquote p { margin: var(--sp-2) 0; }
.wiki-body hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: var(--sp-5) 0;
}
.wiki-body img { max-width: 100%; }

/* The mermaid diagrams.
 *
 * mermaid writes the size it wants into the SVG: a width of 100% and the
 * real size as the viewBox. The browser then shrinks the drawing to fit
 * the column, which is right for a diagram that fits and wrong for a wide
 * one -- a chain of twelve nodes shrinks to a strip of unreadable text.
 *
 * So the figure keeps the width mermaid asked for and scrolls sideways
 * when the column is narrower. The SVG is told not to shrink, and the
 * figure is the thing that scrolls.
 */
.wiki-mermaid {
  margin: var(--sp-4) 0;
  overflow-x: auto;
  text-align: center;
}
.wiki-mermaid svg {
  max-width: none;
  height: auto;
  display: inline-block;
}

/* A diagram that the parser refused. The code block stays, with a note
   saying why, so the reader can still read what was written. */
.wiki-mermaid-fail {
  border-left: 3px solid var(--warn);
}
.wiki-mermaid-note {
  color: var(--warn);
  font-size: 13px;
  margin: var(--sp-2) 0;
}

/* A link inside the text. The underline is faint and permanent, because a
   reader has to be able to tell a link from a word before the pointer
   arrives -- the corpus is full of pages whose whole point is the links
   between them. */
.wiki-body a {
  color: var(--accent);
  border-bottom: 1px solid rgba(37, 99, 235, .35);
  transition: border-color .12s ease;
}
.wiki-body a:hover {
  text-decoration: none;
  border-bottom-color: var(--accent);
}
/* A wikilink to a page that does not exist yet leads to the new-page form,
   so it is marked as broken rather than as a normal link. */
.wiki-body a.wikilink-missing {
  color: var(--err);
  border-bottom: 1px dashed var(--err);
}

/* ---------- wiki formulas (KaTeX) ---------- */
.wiki-content .katex { font-size: 1.05em; }
.wiki-content .katex-display {
  overflow-x: auto;
  overflow-y: hidden;
  padding: 6px 0;
}

.wikilink-missing { color: var(--err); border-bottom: 1px dashed var(--err); }
.wikilink-anchor { color: var(--accent); }

/* ---------- wiki table of contents ----------
   The table of contents is a button at the top-right of the article that
   opens a panel over the text. The right column already holds the
   backlinks, so the contents cannot live there without one of them being
   pushed out.

   The button sits in the article's header row (next to the tags), so it
   never covers a line of text; only the panel, which the reader opened on
   purpose, floats over the article. */
.wiki-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
}
.wiki-head .wiki-tags { margin-bottom: 0; }
/* The button sits in the header row, so it never covers a line of text.
   The panel floats over the article. Its top offset is set by the script
   (--wiki-toc-top) to clear the page title: a panel that covers the title
   looks broken. */
.wiki-toc {
  position: static;
  flex: none;
}
.wiki-toc-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 3px 10px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  user-select: none;
}
.wiki-toc-toggle:hover {
  color: var(--text);
  border-color: var(--border-strong);
}
.wiki-toc-toggle::after {
  content: "▾";
  font-size: 10px;
}
.wiki-toc-toggle[aria-expanded="true"]::after { content: "▴"; }

.wiki-toc-panel {
  display: none;
  position: absolute;
  /* The article is the positioning context; the offset is set by the
     script so the panel starts below the page title. */
  top: var(--wiki-toc-top, 56px);
  right: var(--sp-4);
  z-index: 30;
  width: 250px;
  max-height: 60vh;
  overflow-y: auto;
  padding: var(--sp-2) var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 4px 16px var(--shadow-2);
}
.wiki-toc.open .wiki-toc-panel { display: block; }
.wiki-toc-panel ol {
  list-style: none;
  margin: 0;
  padding: 0;
}
.wiki-toc-panel li { margin: 0; }
.wiki-toc-panel a {
  display: block;
  padding: 3px 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--muted);
  border-bottom: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.wiki-toc-panel a:hover {
  color: var(--accent);
  text-decoration: none;
}
.wiki-toc-panel a.current { color: var(--accent); font-weight: 600; }
/* The levels are indented, so the reader can see the shape of the page. */
.wiki-toc-panel .toc-lv3 { padding-left: 14px; font-size: 12px; }
.wiki-toc-panel .toc-lv4 { padding-left: 28px; font-size: 12px; }
.wiki-toc-panel .toc-note {
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--border);
  font-size: 11px;
  color: var(--muted);
}
.wiki-backlinks {
  list-style: none;
  margin: 0;
  padding: 0;
}
.wiki-backlinks li {
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--border);
}
.wiki-backlinks li:last-child { border-bottom: none; }
.wiki-backlink-path {
  font-size: 11px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- wiki editing ---------- */
.wiki-editor {
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.6;
  min-height: 60vh;
  resize: vertical;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}
.wiki-edit-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}
.wiki-field-label {
  display: block;
  font-size: 13px;
  color: var(--muted);
  margin-bottom: var(--sp-1);
}
.wiki-new-error {
  background: var(--err-bg);
  border: 1px solid var(--err-border);
  border-radius: var(--radius-sm);
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-4);
}

/* ---------- wiki graph ---------- */
.wiki-graph {
  height: 70vh;
  min-height: 420px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.wiki-graph-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}
.wiki-legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  font-size: 13px;
  color: var(--muted);
}
.wiki-legend-dot {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
}

/* The legend doubles as the folder switches: a folder that is switched off
   leaves the graph, so the entry is a button rather than a label. */
.wiki-legend-switch {
  background: none;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 10px;
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
  gap: 5px;
}
.wiki-legend-switch:hover {
  border-color: var(--border-strong);
  color: var(--text);
}
.wiki-legend-switch.off {
  opacity: .45;
  text-decoration: line-through;
}
.wiki-legend-count {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  text-decoration: none;
}

/* ---------- graph filters ---------- */
.wiki-graph-filters {
  margin-bottom: var(--sp-4);
}
.wiki-filter-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-2);
}
.wiki-filter-row:last-of-type { margin-bottom: 0; }
.wiki-filter-label {
  flex: none;
  width: 76px;
  font-size: 13px;
  color: var(--muted);
}
.wiki-filter-search {
  flex: 1 1 auto;
  max-width: 420px;
}
.wiki-filter-slider {
  flex: 1 1 auto;
  max-width: 420px;
  accent-color: var(--accent);
}
.wiki-filter-value {
  flex: none;
  font-size: 13px;
  color: var(--text);
  min-width: 52px;
}
.wiki-filter-legend { align-items: flex-start; }
.wiki-filter-legend .wiki-filter-label { padding-top: 3px; }
.wiki-filter-foot {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}
.wiki-filter-note {
  flex: 1 1 auto;
  font-size: 12px;
}
/* The controls are switched off until the graph has arrived: a filter set
   before the data is there would be wiped by the data arriving. */
.wiki-graph-filters .input:disabled,
.wiki-filter-slider:disabled {
  opacity: .55;
  cursor: not-allowed;
}
.wiki-legend-switch:disabled {
  opacity: .4;
  cursor: not-allowed;
}
.wiki-graph-help {
  margin: 0;
  padding-left: 1.4em;
  /* The notes under the graph tell the reader what the three filters
     do. They are read, so they follow the reader. */
  font-size: calc(var(--read-base-sub) * var(--read-scale));
  color: var(--muted);
}
.wiki-graph-help li { margin: var(--sp-1) 0; }

/* ---------- narrow screens ----------
   Every rule here keys off the viewport width, never off the user agent:
   a desktop browser dragged down to 700px deserves the same layout a phone
   gets, and a user agent string can be forged anyway. */

/* Below this width the three columns no longer leave the article enough
   room to read: the backlinks move to a row of their own and the tree
   narrows. The article keeps at least 660px, which is 38 characters per
   line at the reading size. */
@media (max-width: 1180px) {
  .wiki-layout { grid-template-columns: 220px minmax(0, 1fr); }
  .wiki-side { grid-column: 1 / -1; position: static; max-height: none; }
  /* With the backlinks out of the way the article could grow to the full
     width, but past about 45 characters per line the eye loses the start
     of the next line, so the column is held at the reading width and the
     rest of the row stays empty. */
  .wiki-content { max-width: 760px; }
}

/* The original beside the page needs two panes of a decent width, and a
   PDF viewer on a phone is a poor thing anyway. Below this width the
   button is not offered. */
@media (max-width: 1180px) {
  .wiki-pdf-btn { display: none; }
  .wiki-layout.pdf-open { grid-template-columns: minmax(0, 1fr); }
  .wiki-layout.pdf-open .wiki-pdf-panel { display: none; }
}

/* Narrow screens: the layout the phone gets. */
@media (max-width: 720px) {
  /* Two columns instead of one: the stat cards keep their shape and the
     page stops doubling in height. */
  .grid.cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* The quota windows of one subscription get a row each.
   *
   * Two windows side by side on a phone leave each of them about 160px,
   * which is too narrow for the window's own name ("5 小时额度窗 · 文本
   * 模型") and for the line under the bar ("剩 100% · 2 天 8 小时") --
   * both would break in the middle, and the reader is left putting the
   * halves together. Three across is worse: the name wraps to three
   * lines and the big number has to shrink.
   *
   * One window to a row makes the name one line and the number the one
   * big thing on the row, and the reader can tell at a glance which
   * window belongs to which subscription, because the rows run down
   * under the subscription's own name. */
  .usage-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-3);
  }
  .usage-window {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "label value"
      "bar bar"
      "meta meta";
    align-items: baseline;
    column-gap: var(--sp-2);
  }
  .usage-window-label { grid-area: label; }
  .usage-window > .stat-value {
    grid-area: value;
    text-align: right;
  }
  .usage-window > .stat-bar {
    grid-area: bar;
    margin-top: var(--sp-1);
  }
  .usage-window > .usage-meta {
    grid-area: meta;
    display: flex;
    flex-wrap: wrap;
  }
  .usage-window > .usage-meta > span { white-space: normal; }

  .status-charts { grid-template-columns: minmax(0, 1fr); }
  .nav-link { padding: 0 6px; font-size: 13px; }
  .brand { font-size: 14px; margin-right: var(--sp-2); }
  .user-chip { display: none; }
  .wiki-layout { grid-template-columns: minmax(0, 1fr); }

  /* The card loses some padding so a stat number still fits its column. */
  .card { padding: var(--sp-3); }
  .container { padding: var(--sp-4) var(--sp-3); }

  /* The type of a wiki page on a phone.
   *
   * The desktop size is 16px, which on a 390px screen leaves room for
   * twenty-one characters a line -- seven English words, or a Chinese
   * clause that has to be broken before it is read. The text is a size
   * down here, which fits twenty-three characters on the line.
   *
   * The gaps grow at the same time. A paragraph at 16px on a wide screen
   * is a block of eight or nine lines; the same paragraph on a phone is
   * eleven lines of five, and without a bigger gap above it the blocks
   * run into each other. The list items, which are four pixels apart on
   * the desktop, get the same treatment: a list of links is read item
   * by item, and four pixels is not enough to tell them apart.
   *
   * These are the base values the reader's own multipliers work on, so
   * that a reader who sets the size to a factor of 1.2 gets twenty
   * percent more than what they see now, not twenty percent of the
   * desktop size.
   */
  :root {
    --read-base-size: 15px;
    --read-base-line: 1.8;
    --read-base-gap: 24px;
    /* The headings come down with the text here, and the code comes down
       with it too: on a phone a 26px heading over a 15px paragraph is a
       heading that eats the screen. */
    --read-base-wiki-h1: 22px;
    --read-base-wiki-h2: 19px;
    --read-base-wiki-h3: 17px;
    --read-base-wiki-h4: 15px;
    --read-base-wiki-pre: 12px;
    --read-base-stat-sm: 13px;
  }
  .wiki-body p { margin: calc(var(--read-base-gap) * var(--read-gap-scale)) 0; }
  .wiki-body ul, .wiki-body ol { margin: var(--sp-4) 0; }
  .wiki-body li { margin: var(--sp-2) 0; }
  .wiki-body li > ul, .wiki-body li > ol { margin: var(--sp-2) 0; }
  /* The headings come down with the text, so that the gap between a
     heading and the text below it still reads as a gap. They take their
     phone sizes from the block above and the reader's factor on top of
     them, so the setting still works on a phone.
   *
   * Each rule states the phone's own size first and the factor second.
   * The second declaration is the one a browser uses; the first is what
   * the rule says where `calc()` is not understood, and it is also the
   * readable statement of the size this viewport was set at. */
  .wiki-body h1 {
    font-size: 22px;
    font-size: calc(var(--read-base-wiki-h1) * var(--read-scale));
    margin: var(--sp-5) 0 var(--sp-3);
  }
  .wiki-body h2 {
    font-size: 19px;
    font-size: calc(var(--read-base-wiki-h2) * var(--read-scale));
    margin: var(--sp-5) 0 var(--sp-3);
  }
  .wiki-body h3 {
    font-size: 17px;
    font-size: calc(var(--read-base-wiki-h3) * var(--read-scale));
    margin: var(--sp-4) 0 var(--sp-2);
  }
  .wiki-body h4 {
    font-size: 15px;
    font-size: calc(var(--read-base-wiki-h4) * var(--read-scale));
  }
  /* The code is the one thing that does not shrink with the rest: it is
     read letter by letter, and the monospace font is already narrow. */
  .wiki-body pre {
    font-size: 12px;
    font-size: calc(var(--read-base-wiki-pre) * var(--read-scale));
  }

  /* The long stat values (load 1/5/15, net rx/tx) carry several numbers;
     at the desktop size they would run past the column edge. */
  .stat-value.sm { font-size: calc(var(--read-base-stat-sm) * var(--read-scale)); }

  /* The top bar: the links may wrap instead of squashing each other, and
     the row loses its fixed height so a wrapped row is not clipped. */
  .topnav {
    height: auto;
    min-height: 44px;
    flex-wrap: wrap;
    row-gap: var(--sp-1);
    /* The top of the padding is the notch, as it is on a wide screen. */
    padding: calc(var(--sp-1) + env(safe-area-inset-top)) var(--sp-3)
             var(--sp-1);
  }
  .nav-link { line-height: 28px; }

  /* The reading panel on a phone.
   *
   * The top bar wraps on a narrow screen, so the button the panel hangs
   * from ends up on the second line and a panel hanging below it
   * covers the first. The panel becomes a sheet at the bottom of the
   * screen instead: it is within reach of a thumb, and it covers only
   * the page, never the bar.
   */
  .settings-panel {
    position: fixed;
    top: auto;
    bottom: var(--sp-3);
    right: var(--sp-3);
    left: var(--sp-3);
    width: auto;
    max-width: 420px;
    margin: 0 auto;
  }

  /* The menu panel on a phone.
   *
   * It still hangs below its switch, the way it does on a wide screen:
   * a menu that opens at the bottom of the screen is a long way from the
   * button that opened it, and the reader has to look away from the top
   * bar to find it.
   *
   * But the top bar wraps on a narrow screen, and a switch near the
   * right edge would push its panel off the screen. So the panel is
   * aligned to the switch, and the script marks a switch that sits on
   * the right of the bar -- the panel of such a switch opens to the
   * left, ending where the switch ends, and stays on the screen. */
  .nav-menu-panel {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    right: auto;
    bottom: auto;
    width: auto;
    max-width: calc(100vw - 2 * var(--sp-3));
    margin: 0;
  }
  .nav-menu.right .nav-menu-panel {
    left: auto;
    right: 0;
  }
  .nav-menu-link { padding: var(--sp-2) var(--sp-3); }

  /* The wiki top bar: the breadcrumb takes the first line, the buttons the
     second. */
  .wiki-topbar { flex-wrap: wrap; row-gap: var(--sp-2); }
  .wiki-topbar .spacer { display: none; }
  .wiki-crumbs {
    /* Scroll the crumb trail sideways rather than let it wrap and push the
       buttons off the screen. */
    overflow-x: auto;
    white-space: nowrap;
    word-break: normal;
    max-width: 100%;
    flex: 1 1 100%;
    scrollbar-width: none;
  }
  .wiki-crumbs::-webkit-scrollbar { display: none; }

  /* The search box takes the width it can get and sits under the crumbs. */
  .wiki-search { flex: 1 1 100%; }
  .wiki-search-input { width: 100%; }

  /* The tree folds away so the article starts at the top of the screen. */
  .wiki-tree { position: static; max-height: none; }
  .wiki-tree-summary {
    cursor: pointer;
    pointer-events: auto;
    margin-bottom: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .wiki-tree-summary::after {
    content: "▸";
    color: var(--muted);
    font-size: 13px;
  }
  .wiki-tree-toggle:checked ~ .wiki-tree-summary::after { content: "▾"; }
  .wiki-tree-body { display: none; }
  .wiki-tree-toggle:checked ~ .wiki-tree-body {
    display: block;
    /* A tall tree must not push the article far down the page. */
    max-height: 50vh;
    overflow: auto;
    margin-top: var(--sp-3);
  }

  /* Tables that carry many columns scroll sideways inside their card
     instead of stretching the page. */
  .table-wrap { overflow-x: auto; }
  .table-wrap .table { min-width: 480px; }
  /* A cell that wraps its label across two lines ("买入" split in half)
     reads badly in a table that is already scrolling. */
  .table-wrap .table th,
  .table-wrap .table td { white-space: nowrap; }
  /* The desktop table relies on the columns being wide apart; with the
     labels on one line they would touch each other. The last column keeps
     its zero padding, as on a wide screen. */
  .table-wrap .table th { padding-right: var(--sp-3); }
  .table-wrap .table td { padding-right: var(--sp-3); }
  .table-wrap .table th:last-child,
  .table-wrap .table td:last-child { padding-right: 0; }
  /* The interface table has three columns and fits the narrow card without
     scrolling, so it opts out of the minimum width the wider tables need. */
  .table-wrap .status-nic-table { min-width: 0; }
  .table-wrap .status-nic-table td:not(:first-child),
  .table-wrap .status-nic-table th:not(:first-child) { width: auto; }

  /* A markdown table in a wiki page scrolls the same way. */
  .wiki-body table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
  }

  /* The contents panel takes the whole width of the article: on a phone a
     250px panel would sit over most of the text it points at. */
  .wiki-toc-panel {
    right: 0;
    left: 0;
    width: auto;
    max-height: 55vh;
  }

  /* The graph filters stack: the label takes its own line so the search
     box and the slider get the whole width. */
  .wiki-filter-row {
    flex-wrap: wrap;
    gap: var(--sp-2);
  }
  .wiki-filter-label {
    width: 100%;
    margin-bottom: var(--sp-1);
  }
  .wiki-filter-search {
    max-width: none;
    flex: 1 1 100%;
  }
  /* The slider and its value stay on one line: the value is short and
     reads as a part of the slider, not as a setting of its own. */
  .wiki-filter-slider {
    max-width: none;
    flex: 1 1 200px;
  }
  .wiki-filter-value {
    min-width: 0;
    flex: 0 0 auto;
  }
}

/* Very narrow phones: the middle levels of the breadcrumb are dropped so
   only the current page is named. */
@media (max-width: 560px) {
  .wiki-crumbs .wiki-crumb-mid { display: none; }
}

/* The chat: a list of conversations beside the talk itself.
 *
 * The shape is DeepSeek's: the list is a panel against the left edge
 * of the page, and the talk is divided from it by a line rather than
 * by a gap. DeepSeek's own list is `calc(54vw)` wide at the widest
 * and never narrower than a third of the screen; here it is a fixed
 * column, because the names of the conversations are short and a
 * column that grows with the reader's setting is a column that takes
 * the room away from the answer.
 *
 * On a phone the list becomes a drawer that slides down from the top
 * of the screen, the way DeepSeek's does; the rules for that are in
 * the narrow-screen block further down.
 *
 * The switch that opens the drawer lives in the site's own top bar, at
 * the left of the brand -- the way DeepSeek's own chat puts it in the
 * bar it has at the top of the talk. It used to have a bar of its own
 * under the site's bar, and that bar was a row of 44px holding nothing
 * but that one switch.
 *
 * On a wide screen the list is already beside the talk, so there is
 * nothing to open and the switch is not shown. */
.chat-drawer-toggle { display: none; }
.chat-drawer-close { display: none; }

/* The page of the chat itself is not a column that is centred, and it
 * is as tall as the screen.
 *
 * Every other page of the site sits in a column of 1100px in the
 * middle of the screen. The chat page does not, because the list of
 * the conversations is meant to sit against the left edge of the
 * screen -- the way DeepSeek's does. Its shell is a flex column as
 * wide as the window, with nothing in it that centres anything, so
 * the list is at x=0 and the talk takes the rest.
 *
 * The page is one screen tall as well: the list and the talk fill it
 * from the top bar to the foot of the screen, and the talk scrolls
 * inside itself. Otherwise the page grew past the screen and the
 * bottom of it was empty paper under the two of them. */
body.page-chat {
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
  /* The page is allowed to reach into the notch and the home bar, and
     this keeps the sides clear of them. The top is the top bar's own
     business, and the bottom is the box the question is written in's:
     both of them are in the flow, and both of them are where a finger
     or a thumb lands. */
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
body.page-chat > .topnav { flex: none; }
body.page-chat > main {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  max-width: none;
  margin: 0;
  padding: 0;
}
.chat-shell {
  display: grid;
  flex: 1;
  min-height: 0;
  /* The width of the list of the conversations.
   *
   * DeepSeek's own is `calc(54vw)` with a cap of `calc(54vw - 4rem)`,
   * and `calc(50vw - 4rem)` below 1024px -- a list over half the
   * screen. That is because its conversations are named in English and
   * the names are long. The names here are short, so a fixed column
   * does the same work and leaves the answer the room it needs. */
  --sidebar-width: 240px;
  grid-template-columns: var(--sidebar-width) 1fr;
  gap: 0;
  align-items: stretch;
  /* The names of the conversations are read: a reader looks down the
     list to find the talk they want. So they follow the reader like
     the rest of the page. */
  font-size: calc(var(--read-base-body) * var(--read-scale));
}
.chat-main {
  /* The talk is read, and read at length: it takes the size of the
     prose of a wiki page, which is the size a reader of this site
     already reads at. */
  font-size: calc(var(--read-base-size) * var(--read-scale));
  display: flex; flex-direction: column;
  /* The talk fills the space it is given and the log inside it
     scrolls; the page itself does not grow. */
  min-height: 0; overflow: hidden;
  /* The talk itself has no box around it: it is the page's own paper,
     and the line on the side of the list is the only thing between it
     and the list. */
  background: var(--surface);
  border: none;
  border-radius: 0;
  box-shadow: none;
  padding: 0;
}
/* The list of the conversations: a panel against the edge of the
   page, with the site's own paper colour behind it. The line on its
   right is the one that divides it from the talk.
 *
 * The panel is against the edge of the screen, but its contents are
 * not: they keep a little space from the edge. It is a little only --
 * the panel is 240px wide, and every pixel of it goes to the names of
 * the conversations, which are long enough already. */
.chat-side {
  padding: var(--sp-4) var(--sp-1) var(--sp-4) var(--sp-2);
  background: var(--surface);
  border: none;
  border-right: 1px solid var(--chat-border);
  border-radius: 0;
  box-shadow: none;
}
/* The heading over the list of the conversations, and the button that
   opens a new one, are operated rather than read: they keep their own
   size, the way the heading over the tree of the wiki does. */
.chat-side .section-title { font-size: var(--read-base-section); }
/* The heading over the list, and the button that closes the drawer.
 *
 * The button is only there on a phone, where the list is a drawer that
 * covers the talk; on a wide screen the list is a column and there is
 * nothing to close. */
.chat-side-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
}
.chat-new {
  width: 100%; margin-bottom: var(--sp-3);
  border-radius: 12px;
  background: transparent;
  border-color: var(--chat-border);
}
.chat-new:hover { background: var(--chat-hover); }
.chat-list { list-style: none; margin: 0; padding: 0; }
/* The row of a conversation: its name, and the button that throws it
   away. The button sits on top of the name's right-hand end, so the
   name gets out of its way when the button appears. */
.chat-item { margin-bottom: var(--sp-1); position: relative; }
/* The row of a conversation: its name, and the button that throws it
   away.
 *
 * The name is written out in full, on as many lines as it takes. It
 * used to be cut off at the end of the column, and the reader who
 * could not see the rest of it had to guess which conversation it was
 * -- a name that ends in the middle of a sentence says nothing about
 * what the conversation is about. The name of a conversation is the
 * whole of what the reader has to go on, so it is all there. */
.chat-item-link {
  display: block; padding: 9px 6px 9px 10px;
  border-radius: 12px;
  color: var(--chat-text); text-decoration: none;
  overflow-wrap: anywhere;
}
.chat-item-link:hover { background: var(--chat-hover); }
.chat-item-link.active {
  background: var(--chat-accent);
  color: var(--chat-on-accent);
}
/* The button is out of the way until the row is pointed at, but it is
   always there for the keyboard: a small target is a target that is
   hard to hit.
 *
 * It is made invisible rather than taken away, because a button that
 * is not in the page is not in the tab order either, and a reader who
 * uses the keyboard would never find it. */
.chat-item-remove {
  position: absolute; top: 50%; right: var(--sp-1);
  transform: translateY(-50%);
  width: 24px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: var(--radius-sm);
  background: transparent; color: var(--muted);
  cursor: pointer; padding: 0; line-height: 1;
  opacity: 0; pointer-events: none;
  transition: opacity .12s ease;
}
.chat-item:hover .chat-item-remove,
.chat-item-remove:focus,
.chat-item-remove:focus-visible { opacity: 1; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) {
  .chat-item-remove { transition: none; }
}
.chat-item-remove:hover { background: var(--err-bg); color: var(--err); }
.chat-item-remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.chat-item-remove-icon { font-size: 13px; }
/* The note that says what just happened. */
.chat-notice {
  font-size: 13px; color: var(--muted);
  padding: var(--sp-2); margin-bottom: var(--sp-2);
  border-radius: var(--radius-sm); background: var(--bg);
}
/* The talk, and the box the question is written in, share one column.
 *
 * DeepSeek's answer runs from 344 to 1096 and the reader's bubble ends
 * at 1096: the two line up on both edges, so the page reads as one
 * column of text with the reader's own words set to the right of it.
 * Here the column is 752px -- the same width as the answer -- and the
 * log and the form are both inside it.
 */
.chat-log {
  flex: 1; overflow-y: auto;
  /* Without this the log does not shrink inside the column it is in,
     and a long answer pushes the box the question is written in off
     the foot of the screen. */
  min-height: 0;
  padding: var(--sp-4);
  width: 100%;
  max-width: calc(752px + var(--sp-4) * 2);
  margin: 0 auto;
}
.chat-hint { color: var(--chat-text-soft); }
.chat-row { margin-bottom: var(--sp-5); }

/* The two sides of a talk.
 *
 * The reader speaks in a bubble on the right; the assistant answers in
 * plain text on the left, without a box of its own. A box around every
 * answer says "this is a piece of text in a box" and makes a long
 * answer harder to read than it needs to be. */
.chat-row { display: flex; flex-direction: column; }
.chat-user { align-items: flex-end; }
.chat-bubble {
  max-width: 100%;
  /* The leading is a factor of the page's own leading, the way the wiki
     article has it: at a factor of one the answer is set exactly as it
     was, and the reader's slider moves it from there. */
  line-height: calc(var(--read-base-line) * var(--read-line-scale));
  white-space: pre-wrap; word-break: break-word;
}
/* The reader's own words, in a bubble that sits on the right.
 *
 * The width of the bubble is the width of the column less a fixed
 * margin, the way DeepSeek's is. It used to be a share of the column
 * (70%), which on a 752px column is 526px -- a long question had to
 * break sooner than it needed to. */
.chat-user .chat-bubble {
  background: var(--chat-accent-bg);
  border-radius: 22px;
  padding: 10px 16px;
  max-width: calc(100% - 88px);
  text-align: left;
}
/* The drawn answer.
 *
 * The answer arrives as HTML, and the markup the renderer writes has a
 * newline between the paragraphs. The bubble keeps white-space:
 * pre-wrap, so that the newlines of a plain answer are kept -- and
 * those newlines would otherwise put a whole empty line between two
 * paragraphs, on top of the margin below.
 *
 * So the drawn answer folds its whitespace the way a page does, and the
 * space between its paragraphs is the margin below: the reader's.
 *
 * The text is the chat page's own text, which is a shade deeper than
 * the site's: the answer is read at length and the site's grey is a
 * shade lighter than that. */
.chat-answer {
  white-space: normal;
  color: var(--chat-text);
}
/* The paragraphs of an answer take the same gap the wiki article's
   paragraphs take, so that a conversation and an article read alike. */
.chat-assistant .chat-bubble p {
  margin: calc(var(--read-base-gap) * var(--read-gap-scale)) 0;
}
.chat-answer > p:first-child { margin-top: 0; }
/* The last paragraph has nothing under it, so it has no margin below:
   the gap after the answer would be a gap after nothing. */
.chat-answer > p:last-child { margin-bottom: 0; }

/* The layout of an answer.
 *
 * An answer is a document: the model writes headings, lists, tables and
 * code into it, and the browser's own styles for those are a shrug --
 * a heading is 1.17em of bold with a huge gap above it, a list is
 * indented by 40px. DeepSeek lays an answer out as a document, and the
 * numbers below are its own:
 *
 *   h1  700 24px/34px   margin 32px 0 16px
 *   h2  700 22px/32px   margin 32px 0 16px
 *   h3  700 20px/30px   margin 32px 0 16px
 *   h4  600 16px/28px   margin 16px 0
 *   h5  600 16px/28px   margin 16px 0
 *   h6  600 16px/28px   margin 16px 0
 *   ul/ol  padding-left 18px, margin 16px 0
 *   li    margin-top 6px
 *   strong  600
 *   blockquote  2px line on the left, 14px of space after it
 *   th/td  padding 10px 16px
 *
 * The sizes are the reader's: a heading that stayed at 24px while the
 * text around it grew to 40px would be a heading that reads as part of
 * the paragraph above it. */
.chat-answer h1, .chat-answer h2, .chat-answer h3 {
  font-weight: 700;
  margin: calc(var(--sp-6) * var(--read-gap-scale)) 0
          calc(var(--sp-4) * var(--read-gap-scale));
}
/* Each heading gets a line box of its own.
 *
 * Without this the line box is the body's -- 1.75 times the body size,
 * so a heading of 24px is set in a line of 42px, and the space above
 * it looks like a gap rather than part of the heading.
 *
 * DeepSeek gives every level its own: 24px in 34px, 22px in 32px,
 * 20px in 30px, and 16px in 28px for the smaller ones. The ratio is
 * written out rather than left to the body, because a heading is
 * tighter than the paragraph under it.
 *
 * The ratio is a factor of the reader's own leading slider, the way
 * the body's is: a reader who asks for more space between the lines
 * wants it between the lines of a heading too. */
.chat-answer h1 {
  font-size: calc(var(--read-base-size) * 1.5 * var(--read-scale));
  line-height: calc(1.42 * var(--read-line-scale));
}
.chat-answer h2 {
  font-size: calc(var(--read-base-size) * 1.375 * var(--read-scale));
  line-height: calc(1.45 * var(--read-line-scale));
}
.chat-answer h3 {
  font-size: calc(var(--read-base-size) * 1.25 * var(--read-scale));
  line-height: calc(1.5 * var(--read-line-scale));
}
.chat-answer h4, .chat-answer h5, .chat-answer h6 {
  font-weight: 600;
  font-size: calc(var(--read-base-size) * var(--read-scale));
  line-height: calc(1.75 * var(--read-line-scale));
  margin: calc(var(--sp-4) * var(--read-gap-scale)) 0;
}
.chat-answer ul, .chat-answer ol {
  padding-left: 18px;
  margin: calc(var(--sp-4) * var(--read-gap-scale)) 0;
}
/* The space is between the items, not above the first one.
 *
 * The first item has the list's own margin above it, which is the
 * space the reader sees between the paragraph before the list and the
 * list. With a margin on the item as well there was that space twice
 * over, and the first line of the list sat lower than the rest. */
.chat-answer li:not(:first-child) { margin-top: 6px; }
/* A list inside a list does not step in again.
 *
 * Its numbers and its marks sit in the text of the item they belong
 * to, the way DeepSeek's do, so that a list of lists does not walk
 * further and further to the right as it goes down. */
.chat-answer ul ol, .chat-answer ol ol {
  padding-left: 0;
  list-style-position: inside;
}
.chat-answer li > ul, .chat-answer li > ol { margin-top: 4px; }
.chat-answer li::marker { color: var(--chat-text-soft); }
.chat-answer strong { font-weight: 600; }
/* The line at the left of a quotation.
 *
 * It is a shade lighter than the faintest of the greys: it is a mark
 * that says "this is quoted", not a piece of the text, so it does not
 * have to be read. */
.chat-answer blockquote {
  border-left: 2px solid var(--chat-caption);
  margin: calc(var(--sp-4) * var(--read-gap-scale)) 0 0;
  padding-left: 14px;
  /* The words are the colour of the words around them: what is
     different about a quotation is the line at its left, not the
     colour of its text. */
}
/* A piece of code inside a sentence.
 *
 * It is in the monospaced face: a word of code that is not in that face
 * does not read as code, it reads as a word that happens to be short. */
.chat-answer code {
  background: var(--chat-code-bg);
  border-radius: 6px;
  padding: 0 5px;
  font-family: var(--mono);
  font-size: .875em;
}
/* A block of code is set in the monospaced face, a size smaller than
   the answer, and on a paper a half step away from the page.
 *
 * It used to be the size of the answer and in the same face, so the
 * code in a long answer was bigger than the text around it and drew
 * more attention than the answer did. */
.chat-answer pre {
  background: var(--chat-code-block-bg);
  border-radius: 12px;
  padding: var(--sp-4);
  overflow-x: auto;
  margin: calc(var(--sp-4) * var(--read-gap-scale)) 0;
  font-family: var(--mono);
  font-size: calc(var(--read-base-size) * 0.8125 * var(--read-scale));
  line-height: calc(1.7 * var(--read-line-scale));
  /* A long line is wrapped rather than scrolled sideways.
   *
   * A line of code that runs off the side of the box has to be dragged
   * across to be read -- a line of four hundred characters was four and
   * a half screens of dragging. DeepSeek wraps it, and so does this
   * now. */
  white-space: pre-wrap;
  word-break: break-all;
}
.chat-answer pre code { background: none; padding: 0; font-size: inherit; }
/* A formula that stands on a line of its own.
 *
 * It is as wide as it needs to be, and a formula is often wider than
 * the column of text -- on a phone much wider. So it scrolls sideways
 * inside its own box, the way a block of code does and the way a
 * formula on a wiki page does. Without this the formula ran off the
 * side of the screen and the reader saw only the left of it. */
.chat-answer .katex-display {
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 6px 0;
}
.chat-answer table {
  border-collapse: collapse;
  margin: calc(var(--sp-4) * var(--read-gap-scale)) 0;
  font-size: calc(var(--read-base-size) * 0.9375 * var(--read-scale));
  line-height: calc(1.67 * var(--read-line-scale));
}
/* A table of three columns or fewer takes the whole width of the
 * column; one of four or more is as wide as it needs to be.
 *
 * A short table that shrank to its contents was a small box in the
 * middle of the text, with its columns squeezed to whatever was left
 * over: a table of two columns and two words was 78px wide. A table
 * of four columns or more is left as it is, because at that width the
 * columns need the room and the table has to be read sideways.
 *
 * This is DeepSeek's own rule. */
.chat-answer table:not(:has(th:nth-child(4), td:nth-child(4))) {
  width: 100%;
}
/* The columns of a wide table keep their room.
 *
 * A table of four columns or more is read sideways: the reader drags
 * it along to see the last column. But the columns were being squeezed
 * to fit the screen instead -- a column of 46px, with the words in it
 * wrapped onto two lines -- so the table did not scroll at all and
 * there was nothing to drag.
 *
 * DeepSeek keeps every column at least 100px wide and at most 30% of
 * the screen, so a table of four columns on a 390px screen is 380px
 * wide and is read by dragging it. A short table is left alone: it
 * takes the whole column, and its columns have room enough.
 *
 * The 30% is a share of the screen rather than of the column, so it
 * says the same on a phone as on a wide screen. */
.chat-answer table:has(th:nth-child(4), td:nth-child(4)) th,
.chat-answer table:has(th:nth-child(4), td:nth-child(4)) td {
  min-width: 100px;
  max-width: min(30vw, 320px);
}
/* The line under the head of the table is a shade stronger than the
 * ones under the body, the way DeepSeek's are. */
.chat-answer th, .chat-answer td {
  border-bottom: 1px solid var(--chat-border);
  padding: 10px 16px;
  text-align: left;
}
.chat-answer th {
  font-weight: 500;
  border-bottom-color: var(--chat-border-strong);
}
/* The table lines up with the text around it: the first column starts
 * where the paragraph starts, and the last one ends where the
 * paragraph ends. Without this the table is a box inside the column,
 * narrower than the text on both sides. */
.chat-answer th:first-child,
.chat-answer td:first-child { padding-left: 0; }
.chat-answer td:last-child { padding-right: 0; }
/* A link in the answer is the chat page's blue, not the site's.
 *
 * The site's own links are blue too, but a different blue, and the
 * answer is the one place a reader is reading a long piece of text
 * rather than navigating: the blue there should be the one the rest
 * of the page around the answer uses. */
.chat-answer a {
  color: var(--chat-accent-text);
  text-decoration: none;
}
.chat-answer a:hover { text-decoration: underline; }
.chat-answer a:focus-visible {
  outline: 2px solid var(--chat-accent-text);
  outline-offset: 1px;
}
.chat-answer hr {
  border: none; height: 1px;
  background: var(--chat-border);
  margin: calc(var(--sp-6) * var(--read-gap-scale)) 0;
}
/* The assistant's answer: the text itself, with no box around it. */
.chat-assistant .chat-bubble {
  padding: 0;
  background: none;
  /* The answer is a column of text, and a column that is too wide is
     hard to read: the eye loses the start of the next line. The width
     is the one DeepSeek uses for its own answers. */
  max-width: 752px;
  width: 100%;
}
/* The name of the one who is speaking. The reader's own words carry no
   name: they are on the right, and that says who wrote them. */
.chat-role {
  font-size: 12px; color: var(--chat-text-soft);
  margin-bottom: var(--sp-1);
}
.chat-user .chat-role { display: none; }

.chat-link { color: var(--chat-accent-text); }

/* A reference in the answer: a small grey tag with the number of the
   source in it.
 *
 * The tag is what the reader clicks to go to the page the answer came
 * from, and the title of that page is what they see when the mouse
   rests on it. The title is not in the text itself: a paper's title is
 * long enough to break the sentence it stands in, and two of them in a
   row read as one long run of words.
 *
 * The selector says ".chat-answer a.chat-ref" and not just ".chat-ref"
 * because the tag is a link, and the answer's own rule for links
 * (".chat-answer a") is more specific than a single class. Without the
 * extra name the number in the tag is the answer's blue rather than the
 * grey it is meant to be. */
.chat-answer a.chat-ref {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The tag is part of the answer, so it grows with the answer: a tag of
     nine pixels in a text of twenty-six pixels is a speck, and the
     reader cannot see which number it is. */
  font-size: calc(var(--read-base-chat-ref) * var(--read-scale));
  font-weight: 500;
  /* The numbers line up: the tag is a small box, and the numbers in it
     should stand in a column when several of them are stacked. */
  font-variant-numeric: tabular-nums;
  line-height: 1;
  /* The number is in the monospaced face, the way DeepSeek's is: its
     tag is drawn with the code font, and a number in a proportional
     face sits off the middle of the tag. */
  font-family: var(--mono);
  /* The number is grey, not the answer's blue. The tag is a link, and
     the answer's rule for links is more specific than the tag's own
     rule, so the colour has to be given here to survive it. */
  color: var(--chat-cite-text);
  background: var(--chat-cite-bg);
  /* A hairline around it, the way DeepSeek's has. */
  box-shadow: inset 0 0 0 .5px var(--chat-border-faint);
  border-radius: 6px 6px 6px 0;
  padding: 0 4px;
  /* DeepSeek's tag is a box of its own, 14px tall, lifted a little
     above the line of text it stands in. */
  min-width: 14px;
  height: 14px;
  margin: 0 2px;
  position: relative;
  top: -5px;
  text-decoration: none;
  cursor: pointer;
  flex-shrink: 0;
}
/* The tag under the mouse: only its ground changes. DeepSeek leaves
   the number in the same grey and gives the tag a darker ground, and
   that is enough to show where the reader is pointing. */
.chat-answer a.chat-ref:hover { background: var(--chat-cite-hover-bg); }

/* The sources the answer came from, one to a line. The titles are long,
   so two of them side by side ran into each other. */
/* What the model thought before it answered.
 *
 * The model writes its reasoning out before it answers, and that
 * reasoning is the model's own words about the question. DeepSeek shows
 * it under a row the reader can open and close, and so does this: a
 * reader who wants to know how the answer was reached can read it, and
 * a reader who does not can leave it shut.
 *
 * The row says how long the thinking took, because that is the one
 * number a reader wants before deciding to open it. The text itself is
 * a size smaller than the answer and a colour a step back, because it
 * is the machine talking about the question rather than the answer. */
.chat-thinking {
  margin-bottom: var(--sp-3);
  max-width: 752px;
}
.chat-thinking-head {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 4px 0;
  border: none;
  background: none;
  /* A shade lighter than the answer, the way DeepSeek's row is: it
     reads as a footnote that can be opened, not as a heading of the
     answer. */
  color: var(--chat-text-soft);
  font: inherit;
  /* The row is the answer's own size, not a size below it: DeepSeek
     draws "Thought for 7 seconds" in the body face, and the whole
     answer is read at one size. */
  font-size: calc(var(--read-base-size) * var(--read-scale));
  font-weight: 400;
  line-height: calc(1.75 * var(--read-line-scale));
  cursor: pointer;
  text-align: left;
}
.chat-thinking-head:hover { color: var(--chat-text); }
/* The little mark in front of the row: DeepSeek's is a blue icon. */
.chat-thinking-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  flex: none;
}
.chat-thinking-icon-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--chat-accent);
}
.chat-thinking-label { flex: 1; }
/* The caret that says the row opens and closes. */
.chat-thinking-caret {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  flex: none;
  color: var(--chat-text-faint);
}
.chat-thinking-caret-icon {
  width: 8px;
  height: 8px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .2s ease;
}
.chat-thinking-head[aria-expanded="true"] .chat-thinking-caret-icon {
  transform: rotate(-135deg) translate(-2px, -2px);
}
/* The thinking itself: a size smaller than the answer, and a rule down
   its left side, the way DeepSeek draws it. */
.chat-thinking-body {
  padding: 5px 0 5px 22px;
  position: relative;
}
.chat-thinking-body::before {
  content: "";
  position: absolute;
  top: 5px;
  bottom: 5px;
  left: 7px;
  border-left: 1px solid var(--chat-dimmed);
}
.chat-thinking-text {
  font-size: calc(var(--read-base-body) * var(--read-scale));
  line-height: calc(1.7 * var(--read-line-scale));
  color: var(--chat-text-soft);
}
.chat-thinking-text p { margin: 12px 0; }
.chat-thinking-text p:first-child { margin-top: 0; }
.chat-thinking-text p:last-child { margin-bottom: 0; }
@media (prefers-reduced-motion: reduce) {
  .chat-thinking-caret-icon { transition: none; }
}

.chat-citations {
  margin-top: var(--sp-3);
  /* The titles of the sources are read, so they follow the reader. */
  font-size: calc(var(--read-base-card-desc) * var(--read-scale));
  max-width: 752px;
}
.chat-citations-label { color: var(--chat-text-soft); margin-bottom: var(--sp-1); }
.chat-citation-list {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: chat-source;
}
.chat-citation { margin-bottom: var(--sp-1); }
.chat-citation-link { color: var(--chat-accent-text); text-decoration: none; }
.chat-citation-link:hover { text-decoration: underline; }

.chat-form {
  /* The box the question is written in keeps its own height at the
     foot of the talk; the log above it takes what is left.
   *
   * There is no line above it. The line was there because the box used
   * to be a field of its own on the page's paper; now the box is a
   * shape of its own with a colour and a border of its own, and a line
   * above it only says where the shape begins, which it says already. */
  flex: none;
  padding: var(--sp-3) var(--sp-4);
}
/* The box the question is written in, and the button at its end, sit
   in the same column as the answer.
 *
   * The shape of it is DeepSeek's: a rounded shell with a colour of
   * its own, and the button at the end of the row inside it. The shell
   * is the box the reader sees; the field inside it has no border and
   * no background of its own. */
.chat-form-inner {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-2);
  width: 100%;
  max-width: 752px;
  margin: 0 auto;
  /* DeepSeek's shell: 24px of rounding, a border, and a background a
     step away from the paper the answer is on. */
  border: 1px solid var(--chat-border);
  border-radius: 24px;
  background: var(--chat-input-bg);
  padding: 12px 8px 12px 16px;
}
/* The field inside the shell. It has no border and no background of
   its own: the shell is the box the reader sees.
 *
 * The text in it is the size of the answer, and it moves with the
   reader's own size slider. It used to be the size of the site's
   smaller text -- two sizes below the answer -- so the reader wrote
   the question in one size and read the answer in another. */
.chat-input {
  flex: 1;
  resize: none;
  min-height: 24px;
  max-height: 200px;
  overflow-y: auto;
  border: none;
  border-radius: 0;
  padding: 0;
  font-size: calc(var(--read-base-size) * var(--read-scale));
  line-height: calc(1.5 * var(--read-line-scale));
  background: none;
  color: var(--text);
}
.chat-input:focus { outline: none; }
/* The copy of the text that is measured to find out how tall the field
   has to be. It is laid out exactly like the field, so the height it
   is measured at is the height the text needs. */
.chat-input-mirror {
  position: absolute;
  left: -9999px;
  top: 0;
  visibility: hidden;
  white-space: pre-wrap;
  word-break: break-word;
  font: inherit;
  font-size: calc(var(--read-base-size) * var(--read-scale));
  line-height: calc(1.5 * var(--read-line-scale));
  padding: 0;
  border: none;
}
/* The button is DeepSeek's blue, the one the chat page uses.
 *
 * The button carries the site's own `btn primary` classes as well, and
 * that rule sets the site's blue. It has two classes and this one had
 * one, so the site's rule won and the page ended up with two blues in
 * it. Naming both classes here gives this rule the same weight, and it
 * is written later, so the chat page's blue is the one that shows. */
.btn.primary.chat-send {
  flex: none;
  align-self: flex-end;
  /* A round button at the end of the row inside the shell. */
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  white-space: nowrap;
  background: var(--chat-accent);
  border-color: var(--chat-accent);
  color: var(--chat-on-accent);
}
.chat-send-icon { width: 17px; height: 17px; }
/* While the machine is thinking the arrow turns, and the button is
   greyed out by the rule for a disabled button. */
.chat-send-busy .chat-send-icon {
  animation: chat-send-spin .9s linear infinite;
}
@keyframes chat-send-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .chat-send-busy .chat-send-icon { animation: none; }
}

@media (max-width: 720px) {
  /* On a phone the chat takes the whole screen, the way DeepSeek's does.
   *
   * Three things change. The list of the conversations becomes a drawer
   * that slides down from the top and covers the talk; the talk scrolls
   * inside the space that is left; and the box the question is written
   * in stays at the foot of the screen, where the thumb is.
   *
   * Before this, the list was a strip above the talk: the answer came
   * at y=495 on a 390px screen, which is below the fold, and the box
   * the question is written in was at the very end of the page, so it
   * had to be scrolled to. A reader on a phone came to the page and
   * saw the list of the conversations, not the answer.
   *
   * The answer is not made smaller: DeepSeek keeps 16px on a phone, and
   * so does this page. Only the space around it changes. */

  /* The page is a column of two: the site's own bar and the talk. The
     talk takes what is left of the screen and the rest scrolls inside
     it.
   *
   * The height is the height of the screen as it is now, not as it
   * was when the page was opened. `100vh` is the height of the screen
   * with the keyboard away, so with the keyboard up the box the
   * question is written in was under it and the reader could not see
   * what they were writing. `100dvh` is the height with the keyboard
   * up, and the box sits on the top of it. */
  body.page-chat {
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
    /* The answer keeps the size and the spacing it has on a wide
     * screen.
     *
     * The narrow-screen block of this stylesheet gives the whole site a
     * base of 15px and a gap of 24px, because the article of a wiki page
     * on a 390px screen fits two more characters a line at a size down,
     * and because the same paragraph runs from eight or nine lines to
     * eleven and the blocks run into each other without a bigger gap.
     *
     * DeepSeek does not do that to an answer: its answer on a phone is
     * 16px with a 16px gap, the same as on a wide screen. So the chat
     * page takes the wide-screen values back.
     *
     * The reader's own slider still moves both: these are the base
     * values the slider multiplies.
     *
     * The leading is taken back as well: the narrow-screen block gives
     * it 1.8, which is for the article of a wiki page, where a
     * paragraph of fifteen-pixel words needs the room. An answer is
     * read at sixteen pixels with a leading of 1.75, the same as on a
     * wide screen. */
    --read-base-size: 16px;
    --read-base-line: 1.75;
    --read-base-gap: 16px;
  }
  body.page-chat > .topnav { flex: none; }
  body.page-chat > main {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: 0;
    max-width: none;
    margin: 0;
  }

  /* The switch that opens the drawer, on a row of its own under the
   * site's top bar.
   *
   * It used to be in the top bar itself, at the left of the brand.
   * There it was one small box among the small boxes of a bar that
   * wraps onto two lines on a phone, and a reader looking for the list
   * of the conversations did not see it.
   *
   * It is a row of its own now, where there is nothing else to
   * mistake it for. The row is the height of the button and nothing
   * else, and the talk comes right below it -- it is not a bar of its
   * own, with a line under it and paper around it.
   *
   * The row is above the drawer and above the dim layer, so the
   * switch is there to be used even while the drawer is open. */
  .chat-drawer-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 40px;
    height: 32px;
    padding: 0;
    border: none;
    background: none;
    color: var(--text);
    border-radius: var(--radius-sm);
    cursor: pointer;
  }
  .chat-drawer-toggle:hover { background: var(--bg); }
  .chat-drawer-toggle:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
  }
  .chat-drawer-toggle svg { width: 22px; height: 22px; flex: none; }

  /* The drawer: the list of the conversations, slid in from the left.
   *
   * It slides sideways rather than down: the list is a column beside
   * the talk on a wide screen, and on a phone it is the same column,
   * folded away at the edge of the screen and pulled in when the
   * reader wants it. Sliding it down from the top made it a different
   * shape on a phone than it is on a wide screen.
   *
   * It is placed against the chat area rather than against the screen,
   * so that its top edge is the bottom of the site's top bar however
   * tall that bar happens to be. The bar wraps on a narrow screen, so
   * its height is not a number that can be written down. */
  .chat-side {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 1000;
    /* The width of the column on a wide screen, so the list is the
       same thing on both. */
    width: var(--sidebar-width);
    max-width: 82vw;
    background: var(--surface);
    border: none;
    /* The rounded corners are on the right, the side the drawer comes
       in from is the straight one. */
    border-radius: 0 24px 24px 0;
    box-shadow: none;
    padding: var(--sp-3) var(--sp-1) var(--sp-4) var(--sp-2);
    overflow-y: auto;
    /* It slides rather than appears, so it is moved out of the way
       rather than taken away -- taking it away leaves no slide. */
    transform: translateX(-100%);
    transition: transform .2s ease-out;
    visibility: hidden;
  }
  .chat-side.open {
    transform: translateX(0);
    visibility: visible;
  }
  .chat-drawer-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--chat-text-soft);
    cursor: pointer;
  }
  .chat-drawer-close:hover { background: var(--chat-hover); }
  .chat-drawer-close svg { width: 16px; height: 16px; }

  /* The dim layer behind the drawer.
   *
   * It covers the talk and nothing else: the site's top bar stays
   * clear, so the switch that opens the drawer is still there to be
   * used. It is taken out of the flow, because it is not a third
   * column of the shell. */
  .chat-dim {
    position: absolute;
    inset: 0;
    z-index: 999;
    /* Its own colour, not the site's: the site's overlay is a shade
       deeper (35%), and this one is DeepSeek's (24%). The talk
       behind the drawer stays readable at this depth. */
    background: var(--chat-overlay);
    /* It is taken out of the flow: it is a layer over the talk, not a
       third column of the shell. */
    grid-column: 1 / -1;
    grid-row: 1;
  }
  .chat-dim[hidden] { display: none; }

  /* The talk: the space that is left, with the log scrolling inside it
     and the box the question is written in staying put.
   *
   * The shell is also the box the drawer slides in: the drawer and the
   * dim layer are placed against it, so that their top edge is the
   * bottom of the site's top bar. And it keeps what is inside it --
   * the drawer is moved out of the way with a transform, and a
   * transform does not take the element out of the flow of the page's
   * height. Without that the page grew to twice the height of the
   * screen and there was a screen of blank paper under the talk. */
  .chat-shell {
    position: relative;
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1fr);
    flex: 1;
    min-height: 0;
    overflow: hidden;
  }
  .chat-main {
    padding: 0;
    min-height: 0;
    overflow: hidden;
  }
  /* The space at the sides of the talk.
   *
   * It is a little wider than the site's own narrow-screen margin
   * (16px): DeepSeek gives the messages 20px on a phone, and a column
   * of text that runs to the very edge of the screen is hard to read. */
  .chat-log {
    flex: 1;
    min-height: 0;
    padding: var(--sp-3) 20px;
  }
  .chat-form {
    flex: none;
    /* The bottom of the box keeps clear of the home bar, so the send
       button is not under a thumb that is already holding the phone. */
    padding: var(--sp-3) 20px calc(var(--sp-3) + env(safe-area-inset-bottom));
  }
  /* The bubble of the reader's own words is narrower on a phone, the
     way DeepSeek's is: the margin beside it is 68px rather than 88px,
     because the screen is narrower. */
  .chat-user .chat-bubble { max-width: calc(100% - 68px); }

  /* A table in an answer that is wider than the phone.
   *
   * The table is drawn by the renderer, so it has no scrolling shell of
   * its own the way a table on a wiki page has. Made a block of its
   * own, it scrolls sideways instead of running off the screen; the
   * cells inside are still a table, so the columns still line up. */
  .chat-answer table {
    display: block;
    overflow-x: auto;
  }
}

/* A drawer that slides is a drawer that moves; a reader who has asked
   for less motion does not get it. */
@media (prefers-reduced-motion: reduce) {
  .chat-side { transition: none; }
}

/* The caret that shows the answer is still being written. */
.chat-typing::after {
  content: "▍";
  color: var(--muted);
  margin-left: 2px;
  animation: chat-blink 1s step-end infinite;
}
@keyframes chat-blink {
  50% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .chat-typing::after { animation: none; }
}

/* ---------- the home page ---------- */
/* The home page is one box in the middle of the screen, the way a chat
   window is. It is a page of its own: it does not sit in the site's
   1100px column, because a box in the middle of a column is a box in
   the middle of a column, and not a box in the middle of the screen.
   #
   # The page is a column of the site's own top bar and the box, the
   * way the chat page is: the bar is in the flow, and the box takes
   * what is left of the screen. The top bar is sticky, so it stays in
   * the flow and the box is measured against the rest of the screen. */
body.page-home {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  /* The page is allowed to reach into the notch and the home bar, and
     this keeps the sides clear of them. The chat page has the same
     padding, for the same reason: in landscape the notch cuts into
     the side of the screen, and a box in the middle of the screen
     would otherwise have its edge under it. */
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
body.page-home > .topnav { flex: none; }
body.page-home > main {
  flex: 1;
  max-width: none;
  margin: 0;
  padding: 0;
  display: flex;
}
.home-shell {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* The box sits a little above the middle: the empty paper below it is
     the reader's own, and the page looks as though the answer will come
     up from under it. The padding at the bottom is what lifts it, and
     it lifts it by half of what it is. */
  padding: var(--sp-5) var(--sp-4) 10vh;
}
.home-brand {
  font-size: calc(var(--read-base-title) * var(--read-scale));
  font-weight: 600;
  color: var(--text);
  margin: 0 0 var(--sp-2);
}
.home-hint {
  font-size: calc(var(--read-base-sub) * var(--read-scale));
  color: var(--muted);
  text-align: center;
  margin: 0 0 var(--sp-5);
  max-width: 560px;
}
.home-form {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-2);
  width: 100%;
  max-width: 752px;
  border: 1px solid var(--border);
  border-radius: 24px;
  background: var(--surface);
  padding: 12px 8px 12px 16px;
}
.home-input {
  flex: 1;
  resize: none;
  min-height: 24px;
  max-height: 200px;
  overflow-y: auto;
  border: none;
  border-radius: 0;
  padding: 0;
  font-size: calc(var(--read-base-size) * var(--read-scale));
  line-height: calc(1.5 * var(--read-line-scale));
  background: none;
  color: var(--text);
}
.home-input:focus { outline: none; }
/* The copy of the text that is measured to find out how tall the box has
   to be. It is laid out exactly like the box. */
.home-input-mirror {
  position: absolute;
  left: -9999px;
  top: 0;
  visibility: hidden;
  white-space: pre-wrap;
  word-break: break-word;
  font: inherit;
  font-size: calc(var(--read-base-size) * var(--read-scale));
  line-height: calc(1.5 * var(--read-line-scale));
  padding: 0;
  border: none;
}
/* The round button at the end of the box. It carries the site's own
   `btn primary` classes as well, and this rule is written later, so
   that the site's blue is the one that shows. */
.btn.primary.home-send {
  flex: none;
  align-self: flex-end;
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  white-space: nowrap;
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.home-send-icon { width: 17px; height: 17px; }
.home-note {
  font-size: calc(var(--read-base-sub) * var(--read-scale));
  color: var(--muted);
  margin: var(--sp-3) 0 0;
}

/* ---------- the subscription button in the corner ---------- */
/* The button and the panel are fixed to the corner of the screen, so
   they are there on every page without every page having to know
   about them. */
.usage-fab {
  position: fixed;
  right: var(--sp-4);
  bottom: var(--sp-4);
  /* The button is above the page's own content and below the site's
     own furniture: the top bar (and the panels that hang from it) and
     the quick switcher's cover are all above it, and they have to be
     -- a panel that covers the corner of the screen has to cover the
     button in that corner too, or the reader clicks the button when
     they meant to press something in the panel.
   *
   * The top bar's panels are inside the bar, so the bar's own level is
   * the level they are seen at, whatever z-index they carry. The
   * button therefore has to be below the bar's 20. */
  z-index: 15;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-2);
}
/* The button is operated, not read: it keeps its own size whatever the
   reader has set the reading size to. */
.usage-fab-toggle {
  width: 44px;
  height: 44px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-soft);
  border-radius: 50%;
  cursor: pointer;
  font-size: var(--read-base-body);
  box-shadow: var(--shadow-2);
}
.usage-fab-toggle:hover { background: var(--bg); color: var(--text); }
.usage-fab-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.usage-fab-toggle svg { width: 20px; height: 20px; }
.usage-fab-panel {
  width: min(320px, calc(100vw - var(--sp-4) * 2));
  max-height: min(60vh, 420px);
  overflow-y: auto;
  padding: var(--sp-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
}
.usage-fab-panel[hidden] { display: none; }
.usage-fab-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}
.usage-fab-title { font-size: var(--read-base-body); font-weight: 600; }
/* The moment the numbers were read. It is operated rather than read, so
   it keeps its own size. */
.usage-fab-updated { font-size: 12px; color: var(--muted); }
/* One subscription, a row of its own. */
.usage-fab-sub {
  padding: var(--sp-1) 0;
  border-top: 1px solid var(--border);
}
.usage-fab-sub:first-child { border-top: none; padding-top: 0; }
.usage-fab-sub-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
}
.usage-fab-sub-name {
  font-size: calc(var(--read-base-card-desc) * var(--read-scale));
  font-weight: 600;
  color: var(--text);
}
.usage-fab-sub-plan { font-size: 12px; color: var(--muted); }
.usage-fab-windows {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-3);
  margin-top: var(--sp-1);
}
/* One quota window: a small ring, the name of the window, and the
 * share that is left, all on a row of their own.
 *
 * The ring is the share of the quota that is left. It is drawn with a
 * border that is turned by an angle, so the whole circle is that share
 * and the rest of it is the ground it stands on. */
.usage-fab-window {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  min-width: 0;
}
.usage-fab-ring {
  width: 14px;
  height: 14px;
  flex: none;
  border-radius: 50%;
  /* The ground of the ring: the colour of the border is the colour of
     the share, and the rest of the circle is the ground behind it. */
  background: conic-gradient(var(--ring-colour) var(--ring-turn),
                              var(--border) var(--ring-turn));
  /* The hole in the middle of the ring, so that only the rim is drawn. */
  box-shadow: inset 0 0 0 2px var(--surface);
}
.usage-fab-ring.usage-ok { --ring-colour: var(--ok); }
.usage-fab-ring.usage-warn { --ring-colour: var(--warn); }
.usage-fab-ring.usage-err { --ring-colour: var(--err); }
/* The share wears the colour of its own ring. The colour is put on the
   row rather than on the ring alone, so that the number beside the ring
   is coloured too -- otherwise the reader has to look at the ring to
   know what the number means. */
.usage-fab-window.usage-ok .usage-fab-window-value { color: var(--ok); }
.usage-fab-window.usage-warn .usage-fab-window-value { color: var(--warn); }
.usage-fab-window.usage-err .usage-fab-window-value { color: var(--err); }
/* A ring whose share the upstream did not give is a ring of the ground
   alone: writing it as empty would say the quota is gone, which is a
   different claim from "nobody said". */
.usage-fab-ring.usage-unknown {
  background: var(--border);
  box-shadow: inset 0 0 0 2px var(--surface);
}
.usage-fab-window-label {
  font-size: calc(var(--read-base-card-desc) * var(--read-scale));
  color: var(--text-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The share that is left. It wears the colour of its ring, so that the
   reading does not depend on which part of the row the eye lands on. */
.usage-fab-window-value {
  font-size: var(--read-base-body);
  font-weight: 600;
  font-family: var(--mono);
  color: var(--text);
  line-height: 1.2;
  flex: none;
}
.usage-fab-error {
  font-size: calc(var(--read-base-card-desc) * var(--read-scale));
  color: var(--err);
}
.usage-fab-link { font-size: 12px; }
.usage-fab-note { font-size: var(--read-base-body); color: var(--muted); margin: 0; }
.usage-fab-more {
  display: block;
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--accent);
}

/* On the chat page the button would sit on top of the box the question
   is written in: both are in the corner, and the send button is a round
   one. The button is lifted by the height of that box.
   #
   # The height is measured by the chat page's own script, because the
   # box grows with the question: a number written down here would be
   # right for an empty box and wrong for a long one. The 72px is the
   # fallback for the moment before the script has measured anything. */
body.page-chat-fab .usage-fab {
  bottom: calc(var(--chat-fab-offset, 72px) + var(--sp-4));
}

/* On a phone the button is a little smaller and sits closer to the
   corner, so that it does not cover what is behind it. */
@media (max-width: 720px) {
  .usage-fab { right: var(--sp-3); bottom: var(--sp-3); }
  .usage-fab-toggle { width: 40px; height: 40px; }
  body.page-chat-fab .usage-fab {
    bottom: calc(var(--chat-fab-offset, 72px) + var(--sp-3));
  }
}
