/*
 * 中华族谱网 - what Bootstrap has no component for.
 *
 * The site is Bootstrap 5.3.8 (www/assets/vendor/bootstrap/), in its default light theme with its
 * default primary colour, and nothing else: one theme, no switcher. This file holds only what the
 * framework cannot say - the 世系图 chart, the census charts, the serif face names are set in, and
 * a handful of widgets that are specific to a genealogy - and it says them in Bootstrap's own
 * variables (--bs-*), so the page stays one design.
 *
 * EVERY CLASS HERE STARTS zp-. Bootstrap owns .btn, .card, .row, .col, .progress, .disabled and
 * hundreds more; a class of ours with one of those names would quietly fight the framework's. The
 * prefix makes a collision impossible rather than unlikely.
 *
 * Nothing is loaded from anywhere else: no web font, no CDN. The faces named below are the ones a
 * reader's own machine has.
 */

:root {
  /* Bootstrap's system stack, with the Chinese faces put in where a reader's machine will have one:
     Noto Sans SC (Linux, and many Windows machines), PingFang SC (Apple), Microsoft YaHei (Windows).
     A glyph the Latin faces lack falls through to them, in this order. */
  --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans",
    "Liberation Sans", Arial, "Noto Sans SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
    "ZupuRare", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  /* 宋体 for names: a genealogy is a printed book, and its names are set in a Song face.
     ZupuRare, in both stacks, is a genealogy's own rare characters cut from Jigmo (app/RareFont.php):
     declared only on its pages, and before the generic name, because a font after serif or
     sans-serif is never reached - the system's own fallback would draw its box first. */
  --zp-serif: "Noto Serif SC", "Source Han Serif SC", "Songti SC", "STSong", "SimSun", "ZupuRare", serif;
  /* How tall the sticky site header is, measured by app.js; the chart's own header pins under it. */
  --zp-headerh: 3.5rem;
}
/* An English page's Latin in Manrope, as https://123phototool.com/ sets its text (the owner's,
   2026-10-05): declared in layout.php for English pages only, and first in the same stack, so the
   Chinese on the page - which the face does not carry - is drawn exactly as it was. */
html[lang="en"] {
  --bs-font-sans-serif: "Manrope", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans",
    "Liberation Sans", Arial, "Noto Sans SC", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
    "ZupuRare", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}

.zp-name, .zp-brand { font-family: var(--zp-serif); }
.zp-brand { font-weight: 600; letter-spacing: .02em; }
/* A place's pinyin beside its name, on an English page (place_html): quieter than the name it reads. */
.zp-py { color: var(--bs-secondary-color); font-size: .88em; font-weight: normal; }
/* The logo (tools/build_logo.py): the seal and ChinaZupu, 2rem tall, then the site's name as text in
   楷书 - ZupuKai holds only the name's own characters; anything else falls through to the serif. A
   little smaller on the narrowest phones, where the menu button needs its room too. */
.zp-logo { height: 2rem; width: auto; vertical-align: middle; }
/* The home page's slogan: the name's 楷书 and the seal's red, a size under the heading */
.zp-slogan { font-family: "ZupuKai", var(--zp-serif); font-size: 1.55rem; color: #b8312f; letter-spacing: .06em;
  margin: -.25rem 0 .75rem; }
/* ...on one line on a phone: at the desk's size its last character wrapped alone */
@media (max-width: 575.98px) { .zp-slogan { font-size: 1.2rem; letter-spacing: .02em; } }
.zp-brand-name { margin-left: .55rem; font-family: "ZupuKai", var(--zp-serif); font-size: 1.3rem;
  font-weight: 400; letter-spacing: .04em; vertical-align: middle; }
@media (max-width: 399.98px) {
  .zp-logo { height: 1.7rem; }
  .zp-brand-name { font-size: 1.1rem; margin-left: .4rem; }
}

/* Flash messages: over the page rather than in it, so a message about a save is seen wherever the
   save sent the reader. Without JavaScript nothing could dismiss them, so layout.php's <noscript>
   puts them back in the flow. */
.zp-flashes {
  position: fixed; top: calc(var(--zp-headerh) + .5rem); left: 50%; transform: translateX(-50%);
  z-index: 1080; width: min(40rem, calc(100% - 2rem));
}
.zp-flashes .alert { box-shadow: var(--bs-box-shadow); }

/* The picture WeChat makes its share card from: off-screen, of a real size, never display:none.
   See the note in layout.php before tidying it away. */
.zp-wxcard { position: absolute; left: -9999px; top: 0; width: 300px; height: 157px; }

/* The bar's menus, 我的族谱, the account's and the languages': <details> dressed as Bootstrap's
   drop-downs, so they open with no script. Under the ☰ they open in place, in the list. */
.zp-navmenu summary { list-style: none; cursor: pointer; }
.zp-navmenu summary::-webkit-details-marker { display: none; }
.zp-navmenu details[open] > .dropdown-menu { display: block; }
.zp-navname { display: inline-block; vertical-align: bottom; max-width: 16em; }

/* The languages sit apart from the rest of the menu on a wide screen. There the menu is one row,
   and no label may break in two (族/谱 stacked in a column read as two words), nor a long account
   name push the rest out: tools/navbar_probe.js measures both. */
@media (min-width: 1200px) {
  .zp-langs { border-left: var(--bs-border-width) solid var(--bs-border-color); padding-left: .5rem; }
  #topnav .nav-link { white-space: nowrap; }
  /* ten links, 我的族谱, the account and the language since 视频 came (2026-10-20): a little less room round each,
     and the account's name cut at 6em (it was 8em) - whole in its title */
  .navbar-expand-xl #topnav .navbar-nav .nav-link { padding-left: .3125rem; padding-right: .3125rem; }
  .zp-navname { max-width: 6em; }
  /* ...and eleven since 历史族谱 (2026-10-21): the links a size down, a pixel less round each and the
     mark's margin halved. At 1140px the longest Chinese row had run 41px past the bar, and the English
     one 51px at 1320, where its name shows beside the mark. */
  .navbar-expand-xl #topnav .navbar-nav .nav-link { font-size: .9375rem; padding-left: .25rem; padding-right: .25rem; }
  #zp-top .navbar { --bs-navbar-brand-margin-end: .5rem; }
  .zp-navmenu details { position: relative; }
  .zp-navmenu .dropdown-menu { position: absolute; top: 100%; right: 0; left: auto; margin-top: .25rem; }
  /* 我的族谱, on the left of the bar, opens rightwards from under its own label */
  .zp-navmenu-start .dropdown-menu { left: 0; right: auto; }
}

/* A pinned element's scroll-margin, so a jump to an anchor does not land under the sticky header. */
:target { scroll-margin-top: calc(var(--zp-headerh) + 1rem); }

/* ---- 族谱简介 on a genealogy's front page ----------------------------------------------------
   Five lines, then a choice. Clamped by height rather than by -webkit-line-clamp: the description
   is paragraphs and lists, not one block of text, and line-clamp does not reach across children.
   The class is added by app.js only when the content actually overflows, so a short description
   never grows a button and one with JavaScript off is never truncated. */
.zp-about { max-width: 46rem; }
.zp-about-body.is-clamped { max-height: 9.4em; overflow: hidden; position: relative; }
.zp-about-body.is-clamped::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2.6em;
  background: linear-gradient(to bottom, transparent, var(--bs-card-bg, var(--bs-body-bg))); }
.zp-about-body p { margin: .45rem 0; line-height: 1.8; }
.zp-about-body p:first-child { margin-top: 0; }
.zp-about-body > :last-child { margin-bottom: 0; }
.zp-about-body ul, .zp-about-body ol { margin: .45rem 0 .7rem; padding-left: 1.5rem; line-height: 1.8; }
/* A provenance note naming a source URL is one unbreakable seventy-character word, and at phone
   width it used to push the whole page sideways. The editor too: what the writer sees in the box
   should be what the reader gets. */
.zp-about, .zp-richedit .richbox { overflow-wrap: anywhere; }

/* Tables in a 简介 are drawn ENTIRELY here: Html.php keeps no attribute on any part of a table - no
   border, no width, no colspan - so the stylesheet is the only thing that decides how one looks.
   The editor and the published page load the same rules, so what an admin arranges is what a
   reader gets. border-collapse, so two neighbouring cells share one line. */
.zp-about-body table, .zp-richedit .richbox table { border-collapse: collapse; margin: .6rem 0; max-width: 100%; }
.zp-about-body th, .zp-about-body td,
.zp-richedit .richbox th, .zp-richedit .richbox td { border: var(--bs-border-width) solid var(--bs-emphasis-color); padding: .45em .7em;
  vertical-align: top;
  /* an empty cell is still something to aim at, with a caret on a desk and a finger on a phone */
  min-width: 2.5em; }
.zp-about-body th, .zp-richedit .richbox th { background: var(--bs-secondary-bg); font-weight: 700; text-align: start; }
.zp-about-body table p, .zp-richedit .richbox table p { margin: 0; }
/* Only the READER's copy scrolls: a wide table would otherwise push a phone's whole page sideways.
   The editor keeps a real table box - display:block reparents the rows into an anonymous table,
   and inside a contenteditable that moves the caret about in ways an author would have to fight. */
.zp-about-body table { display: block; overflow-x: auto; width: max-content; }

/* ---- a person's page ------------------------------------------------------------------------ */
.zp-facts { display: flex; flex-wrap: wrap; gap: .4rem .9rem; color: var(--bs-secondary-color); margin: .2rem 0 1rem; }
.zp-facts b { color: var(--bs-emphasis-color); }
.zp-rem { background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis);
  border-radius: var(--bs-border-radius); padding: .05em .5em; }
.zp-honour { display: inline-block; background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis);
  border: var(--bs-border-width) solid var(--bs-primary-border-subtle); border-radius: var(--bs-border-radius);
  padding: 0 .5em; font-size: .9em; }
/* the line up to him, left to right, one › between each - it is a chain, and reads as one */
.zp-ancestry { padding-left: 0; list-style: none; display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }
.zp-ancestry li { display: flex; align-items: center; gap: .3rem; }
.zp-ancestry li + li::before { content: "›"; color: var(--bs-border-color); margin-right: .2rem; }
.zp-originstrip { font-size: .85rem; color: var(--bs-secondary-color); line-height: 1.7; }
.zp-originstrip { margin-bottom: 1rem; }
.zp-originstrip a { font-weight: 600; }
/* ...a section the reader may close whole (origin_box): its heading the summary, the strip under it */
.zp-originstrip > summary { cursor: pointer; }
.zp-originstrip[open] > summary { margin-bottom: .1rem; }
/* 族谱溯源 past ORIGIN_SHOWN rows: the older behind 展开更早的 N 代, above the latest (app/views/origin.php, origin_strip) */
.zp-origin-older > summary { color: var(--bs-link-color); cursor: pointer; }
.zp-origin-older > .zp-originline { margin-bottom: 0; }
.zp-originline.zp-origin-latest { margin-top: 0; }
.zp-proposal { font-size: .9rem; }
/* children, spouses, cross-references: a cell each. .zp-col is one to a line, because a family reads
   DOWN the page in the order they were born, and a grid fills across before it fills down */
.zp-kids { list-style: none; padding: 0; margin: 0; display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: .35rem; }
.zp-kids li { padding: .45rem .7rem; border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: var(--bs-border-radius); background: var(--bs-body-bg); }
.zp-kids li a { font-weight: 500; }
.zp-kids.zp-col { grid-template-columns: 1fr; }
.zp-rank { color: var(--bs-secondary-color); font-size: .85em; margin-right: .35rem; }
.zp-ranked li { display: flex; align-items: center; gap: .3rem; }
.zp-ranked .zp-rank { display: inline-block; min-width: 3.6em; }
.zp-ranked .zp-nm { font-weight: 500; }
.zp-ranked .btn { padding: .1rem .45rem; line-height: 1.2; }
.zp-reorder .zp-kids { margin: .6rem 0; }
/* a name nobody may see: *** is drawn quieter than a name, so it does not read as one */
.zp-masked { color: var(--bs-secondary-color); letter-spacing: .1em; }
/* the book's own words and the site's 传记, in the book's face */
.zp-bio { font-family: var(--zp-serif); font-size: 1.05rem; line-height: 2; text-align: justify;
  margin: .5rem 0; padding-left: .9rem; border-left: 3px solid var(--bs-primary-border-subtle); }
.zp-src { font-size: .85rem; margin: .3rem 0 0; }
/* a 历史族谱's 传记: the history's paragraphs, a man filed under this one set off, the rest behind 展开全文 */
.zp-bio-fu { font-weight: 600; margin-top: 1rem; }
/* ...and the page's headings (a year, a man, 贊), as headings: no rule beside them, the 现代文's with its year AD */
.zp-bio.zp-bio-h { font-weight: 700; margin-top: 1.1rem; margin-bottom: .2rem; padding-left: 0; border-left: 0; background: none; text-align: left; }
.zp-bio-more > summary { color: var(--bs-link-color); cursor: pointer; margin: .2rem 0 .6rem; }
.zp-bio-more[open] > summary { margin-bottom: .2rem; }
.zp-bio-src { margin-top: .4rem; }
/* ...with its modern Chinese beside it (person.php, tools/build_hist_modern.py): 原文 left, 现代文 right, each
   paragraph on a grid row of its own (--i) so a pair lines up; data-align="0" lets the two columns flow
   as they are; data-view picks both or one; a phone gets each paragraph with its modern one under it */
.zp-cmp-grid, .zp-cmp-head { display: grid; grid-template-columns: 1fr 1fr; column-gap: 1.2rem; }
.zp-cmp-head { padding-left: .9rem; margin-bottom: .2rem; }
.zp-cmp-col { display: contents; }
.zp-cmp-orig > p { grid-column: 1; grid-row: var(--i); }
.zp-cmp-modern > p { grid-column: 2; grid-row: var(--i); }
.zp-bio-mod { border-left-color: var(--bs-success-border-subtle); background: var(--bs-tertiary-bg); padding-right: .6rem; }
.zp-bio-same { color: var(--bs-secondary-color); }
.zp-cmp-edit { font-size: .75rem; margin-left: .35rem; white-space: nowrap; opacity: .7; }
.zp-cmp-edit:hover, .zp-cmp-edit:focus { opacity: 1; }
.zp-cmp-edited { font-size: .75rem; margin-left: .25rem; color: var(--bs-secondary-color); white-space: nowrap; }
.zp-hm-orig { white-space: normal; padding: .5rem .75rem; border-left: 3px solid var(--bs-border-color); background: var(--bs-tertiary-bg); line-height: 1.9; }
.zp-cmp-note { margin-top: .6rem; }
[data-align="0"] .zp-cmp-col { display: block; }
[data-align="0"] .zp-cmp-orig { grid-column: 1; }
[data-align="0"] .zp-cmp-modern { grid-column: 2; }
[data-align="0"] .zp-cmp-col > p { grid-column: auto; grid-row: auto; }
[data-view="orig"] .zp-cmp-grid, [data-view="orig"] .zp-cmp-head,
[data-view="modern"] .zp-cmp-grid, [data-view="modern"] .zp-cmp-head { grid-template-columns: 1fr; }
[data-view="orig"] .zp-cmp-modern, [data-view="orig"] .zp-cmp-head span:nth-child(2),
[data-view="modern"] .zp-cmp-orig, [data-view="modern"] .zp-cmp-head span:nth-child(1) { display: none; }
[data-view="orig"] .zp-cmp-col, [data-view="modern"] .zp-cmp-col { grid-column: 1; }
[data-view="orig"] .zp-cmp-col > p, [data-view="modern"] .zp-cmp-col > p { grid-column: 1; }
@media (max-width: 767.98px) {
  .zp-cmp-grid, .zp-cmp-head { grid-template-columns: 1fr; }
  .zp-cmp-head { display: none; }
  .zp-cmp-orig > p { grid-column: 1; grid-row: calc(var(--i) * 2 - 1); }
  .zp-cmp-modern > p { grid-column: 1; grid-row: calc(var(--i) * 2); }
  [data-align="0"] .zp-cmp-orig { grid-column: 1; grid-row: 1; }
  [data-align="0"] .zp-cmp-modern { grid-column: 1; grid-row: 2; }
  [data-view="modern"] .zp-cmp-modern > p { grid-row: var(--i); }
  [data-view="orig"] .zp-cmp-orig > p { grid-row: var(--i); }
  /* one column: the heading once, the 现代文's (it has the year AD) */
  [data-view="both"][data-align="1"] .zp-cmp-orig > .zp-bio-h { display: none; }
}
.zp-bio-elsewhere { font-size: .9rem; margin: .2rem 0 .4rem; }
.zp-annot-list { list-style: none; padding: 0; margin: .4rem 0 0; display: flex; flex-wrap: wrap; gap: .4rem; }
.zp-annot-list li { font-family: var(--zp-serif); background: var(--bs-body-bg); border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: var(--bs-border-radius); padding: .25rem .7rem; }
.zp-cands { max-height: 300px; overflow: auto; }
.zp-cands:empty { display: none; }
.zp-lbl { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: .15rem .6rem; }
.zp-sugg .list-group-item { background: var(--bs-tertiary-bg); }
.zp-notes li { margin: .3rem 0; }
/* English beside a 历史族谱's Chinese (en_beside): quieter than what it translates; under a note, a line of its own */
.zp-en { color: var(--bs-secondary-color); font-weight: normal; }
.zp-honour .zp-en { color: inherit; opacity: .75; }
.zp-notes .zp-en { display: block; }
.zp-ico { width: 1em; height: 1em; vertical-align: -.125em; }

/* ---- counters under a text box: 传记 and 族谱简介 --------------------------------------------- */
.zp-charcount { margin: .25rem 0 0; text-align: right; }
.zp-charcount .n { font-variant-numeric: tabular-nums; font-weight: 700; }
/* Over the limit. Colour ALONE would say nothing to a reader who cannot see it, so the number is
   also announced - the counter is aria-live - and the browser refuses the form with a sentence. */
.zp-charcount.over, .zp-charcount.over .n { color: var(--bs-danger-text-emphasis); }

/* ---- the book's prose, and 族谱溯源 ------------------------------------------------------------ */
.zp-measure { max-width: 48rem; }
.zp-foreword .zp-bio { padding-left: 0; border-left: 0; }   /* the 　　 is in the text itself */
/* the line above 世1: a rule down the left with a dot per man, the join to the book in primary */
.zp-originline { list-style: none; padding: 0; margin: 1rem 0; max-width: 760px; }
.zp-originline li { position: relative; padding: .55rem 0 .55rem 1.6rem; border-left: 2px solid var(--bs-border-color); }
.zp-originline li::before { content: ''; position: absolute; left: -5px; top: 1.05rem; width: 8px; height: 8px;
  border-radius: 50%; background: var(--bs-border-color); }
.zp-originline li.zp-apex { border-left-color: transparent; }
/* Rows above the numbering stand apart from the numbered line - the break before 世1 is the point -
   but not from each other: a forebear put above the apex is his father, so a line runs from each such
   row's dot down to the next one's. Without :has() the rows simply stand unjoined. */
.zp-originline li.zp-apex:has(+ li.zp-apex)::after { content: ''; position: absolute; left: -2px;
  top: calc(1.05rem + 4px); bottom: calc(-1.05rem - 4px); border-left: 2px solid var(--bs-border-color); }
.zp-originline li.zp-apex::before { background: var(--bs-secondary-color); }
.zp-originline li.zp-joins { border-left-color: var(--bs-primary); }
.zp-originline li.zp-joins::before { background: var(--bs-primary); width: 10px; height: 10px; left: -6px; }
.zp-originline .zp-who { font-weight: 600; font-size: 1.05rem; }
.zp-originline .zp-g { color: var(--bs-secondary-color); font-size: .85rem; margin-right: .4rem; }
.zp-originline .zp-zi { margin-left: .35rem; }
.zp-originline .zp-gc { color: var(--bs-secondary-color); font-size: .8rem; margin-left: .35rem; }
.zp-originline .zp-honour { margin-left: .35rem; }
/* a page about him elsewhere: small, at the end of his line, never broken across two */
.zp-xlink { font-size: .82rem; margin-left: .45rem; white-space: nowrap; }
.zp-originline .zp-onote { margin: .3rem 0 0; font-size: .9rem; }
.zp-originline .zp-src { margin: .2rem 0 0; font-size: .82rem; color: var(--bs-secondary-color); }
/* 附注 and 出处 keep the line breaks they were typed with - two citations on two lines stay two */
.zp-originline .zp-onote, .zp-originline .zp-src { white-space: pre-line; }
/* 族谱溯源 editor: a row not yet written is dashed, so the three spares do not read as three men */
.zp-dashed { border-style: dashed; }
/* 字辈 worked out from the 字辈诗 rather than written down (Tree::genChar): dashed, in a lighter hand.
   !important because Bootstrap's .border and .text-primary are. */
.zp-gens .zp-proj { border-style: dashed !important; }
.zp-gens .zp-proj b { color: var(--bs-secondary-color) !important; font-weight: 400; }
.zp-gctable .zp-spare td { background: var(--bs-tertiary-bg); }
.zp-oinsert > summary { width: max-content; max-width: 100%; }

/* ==== 世系图 =====================================================================================
   The chart keeps its own short class names on the parts INSIDE it - node, tgl, g, zi, f, sp, more,
   desc, nodemenu, and li.focus / li.anc / li.collapsed - because app.js builds five thousand of them
   and the probes read them. Every rule for them is scoped under .zp-tree, so none can meet a class
   of Bootstrap's or be met by one. */

/* Chips a node and a person page share. 「侨居美国」 in info, 出继/入继 in warning: a place and an
   adoption between branches are different kinds of fact, and in a line of five hundred nodes the
   branch that went abroad or changed hands should be findable by eye. */
.zp-place, .zp-succ { font-size: .74rem; padding: .05em .45em; border-radius: .25rem; white-space: nowrap;
  border: var(--bs-border-width) solid; }
.zp-place { color: var(--bs-info-text-emphasis); background: var(--bs-info-bg-subtle); border-color: var(--bs-info-border-subtle); }
.zp-succ { color: var(--bs-warning-text-emphasis); background: var(--bs-warning-bg-subtle); border-color: var(--bs-warning-border-subtle); }

/* The connectors are drawn the way the `tree` command draws them - a vertical down from each parent,
   a tee into every child, an elbow into the last. --tv-mid is the drop from the top of a row to the
   middle of its node box, where the tee meets it; --tv-in is the indent per generation, and the
   vertical sits --tv-arm to the left of the child. */
.zp-tree { font-size: .95rem; --tv-mid: 1.2rem; --tv-in: 1.6rem; --tv-arm: 1rem; --tv-line: var(--bs-secondary-border-subtle);
  /* The chart scrolls sideways rather than wrapping: a name belongs on one line with its 字 and its
     counts, so the rows keep their length and the box moves under them. Never up and down: the box
     is as tall as the chart and the PAGE scrolls it. Said outright, because overflow:auto both ways
     gave a chart whose last ▾ - twice the text's size - stood a pixel below the box a scrollbar of
     its own down the side (the owner's, 2026-10-05). */
  overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; }
.zp-tree ul { list-style: none; margin: 0; padding-left: var(--tv-in); }
/* width:max-content is what lets the box scroll: without it the rows are as wide as the box and the
   flex items shrink below their own text, one character to a line */
.zp-tree > ul { padding-left: 0; width: max-content; min-width: 100%; }
/* padding, not margin: the rows have to touch, or the vertical breaks between every sibling */
.zp-tree li { margin: 0; padding: .15rem 0; position: relative; }
.zp-tree ul li::before, .zp-tree ul li::after { content: ""; position: absolute; background: var(--tv-line); }
/* the vertical runs the whole height of the row INCLUDING its descendants, so it carries on down the
   side of a subtree while later brothers are still to come... */
.zp-tree ul li::before { left: calc(var(--tv-arm) * -1); top: 0; bottom: 0; width: 1px; }
/* ...and stops at the elbow on the last child, which is what closes a family off */
.zp-tree ul li:last-child::before { bottom: auto; height: var(--tv-mid); }
.zp-tree ul li::after { left: calc(var(--tv-arm) * -1); top: var(--tv-mid); width: var(--tv-arm); height: 1px; }
/* the founder has nothing above him to hang from */
.zp-tree > ul > li::before, .zp-tree > ul > li::after { display: none; }
/* Everything below 本人 is drawn darker, so his own descent reads as the subject of the page rather
   than one more grey line among everybody else's. */
.zp-tree li.focus > ul li::before, .zp-tree li.focus > ul li::after { background: var(--bs-gray-700); }
.zp-tree li.collapsed > ul { display: none; }

.zp-tree .node { display: inline-flex; align-items: center; gap: .45rem; padding: .28rem .6rem;
  border-radius: var(--bs-border-radius); background: var(--bs-body-bg); border: var(--bs-border-width) solid var(--bs-border-color); }
.zp-tree .node > * { flex: 0 0 auto; white-space: nowrap; }
.zp-tree .node:hover { border-color: var(--bs-primary); }
/* The name, not underlined (the owner's, 2026-10-01): the node is already a box that lights up on
   hover. The line comes back under the pointer and the keyboard's focus, which says it is a link. */
.zp-tree .node > a { font-family: var(--zp-serif); text-decoration: none; }
.zp-tree .node > a:hover, .zp-tree .node > a:focus-visible { text-decoration: underline; }
.zp-tree .node .g, .zp-tree .node .desc { font-size: .72rem; color: var(--bs-secondary-color); background: var(--bs-secondary-bg);
  padding: .05em .45em; border-radius: .25rem; }
.zp-tree .node .zi { color: var(--bs-secondary-color); font-size: .88rem; }
.zp-tree .node .f { color: var(--bs-pink); font-size: .8rem; }
/* 妻何氏、蓝氏、苏氏 - set like the 字/号 beside it, because it is the same kind of fact */
.zp-tree .node .sp { color: var(--bs-secondary-color); font-size: .82rem; }
.zp-tree .node .zp-honour { font-size: .78rem; }
.zp-tree .node .badge { font-weight: 500; }
.zp-tree .more { color: var(--bs-secondary-color); font-size: .85rem; margin-left: .3rem; }
/* 本人: tinted, outlined, and his name bold - on a chart of a thousand nodes the tint is easy to
   scroll past; a bold name is not */
.zp-tree .node.focus { background: var(--bs-primary-bg-subtle); border-color: var(--bs-primary); }
.zp-tree .node.focus a { font-weight: 600; }
/* An ancestor on the spine above the person asked for: unfilled, and the NAME bold - these are the
   twenty-odd people the reader descends FROM, in a page of five hundred they are merely related to */
.zp-tree .node.line { background: transparent; color: var(--bs-gray-700); }
.zp-tree .node.line a { color: var(--bs-gray-700); font-weight: 600; }
/* A son the book draws in ANOTHER man's line. Dashed, because the row is a pointer and not a member
   of this family; a solid node would say he belongs to it twice over. */
.zp-tree .node.xref { border-style: dashed; background: transparent; }
.zp-tree .node.xref a { font-weight: 500; }
.zp-tree .tgl { width: 1.35rem; height: 1.35rem; border-radius: .25rem; border: var(--bs-border-width) solid var(--bs-secondary-border-subtle);
  background: var(--bs-body-bg); color: var(--bs-gray-700); font: 600 .8rem/1 var(--bs-body-font-family); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; padding: 0; margin: 0; }
.zp-tree .tgl:hover { border-color: var(--bs-primary); color: var(--bs-primary); }
.zp-tree .tgl.leaf { visibility: hidden; }
/* What you can do from a member, at the back of the row. Always there on a touch screen, where there
   is no hover to reveal it with; with a real mouse it waits until the row is pointed at, so five
   thousand rows are not five thousand buttons. visibility, so the row keeps its width either way. */
/* The ▾ twice the size of the text beside it (the owner's, 2026-10-05): at the text's own size it was
   a hard thing to see and a harder thing to hit with a finger. A line-height under 1, so the bigger
   glyph does not make the row it sits in any taller. */
.zp-tree .nodemenu { margin-left: .15rem; padding: 0 .35rem; border: 0; border-radius: .25rem; background: none;
  color: var(--bs-secondary-color); font: inherit; font-size: 2em; line-height: .6; vertical-align: -.12em; cursor: pointer; }
.zp-tree .nodemenu:hover, .zp-tree .nodemenu[aria-expanded="true"] { background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
@media (hover: hover) and (pointer: fine) {
  .zp-tree .nodemenu { visibility: hidden; }
  .zp-tree .node:hover .nodemenu, .zp-tree .node:focus-within .nodemenu,
  .zp-tree .nodemenu[aria-expanded="true"] { visibility: visible; }
}
/* Twenty generations at the full indent is 300px of empty margin, most of a phone's screen. The
   step and the arm shrink together so the elbows stay in proportion. */
@media (max-width: 600px) {
  .zp-tree { font-size: .9rem; --tv-in: .85rem; --tv-arm: .6rem; --tv-mid: 1.1rem; }
  .zp-tree .node { gap: .3rem; padding: .24rem .45rem; }
  .zp-tree .tgl { width: 1.2rem; height: 1.2rem; }
}
/* 两人关系 draws with the same markup: the shared ancestor tinted, the two lines down from him heavy */
.zp-reltree { margin-top: .8rem; }
.zp-reltree .node.common { border-color: var(--bs-primary); background: var(--bs-primary-bg-subtle); }
.zp-reltree .node.common > a { font-weight: 700; }
.zp-reltree .node.onpath { border-color: var(--bs-gray-700); font-weight: 600; }
.zp-reltree .node.onpath a { font-weight: 700; color: var(--bs-emphasis-color); }

/* One popup for the whole chart, parked on <body> - a child of the scrolling chart would be clipped
   at its edges, which is exactly where the last nodes of a wide generation are. position:fixed,
   because it is placed from getBoundingClientRect, which is viewport coordinates. */
.zp-nodepop { position: fixed; z-index: 1040; min-width: 11rem; padding: .3rem; display: flex; flex-direction: column;
  background: var(--bs-body-bg); border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  box-shadow: var(--bs-box-shadow); }
.zp-nodepop a, .zp-nodepop button { padding: .5rem .7rem; border-radius: .25rem; color: var(--bs-body-color); font: inherit;
  font-size: .9rem; white-space: nowrap; text-align: left; background: none; border: 0; cursor: pointer; width: 100%;
  text-decoration: none; }
.zp-nodepop a:hover, .zp-nodepop button:hover { background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis); }
.zp-nodepop hr { margin: .25rem .3rem; }
/* the whole row is a way into its box (app.js); pointer, because an iPhone hands a tap on a plain
   element to the page's click handler only when it is styled as something to press */
.zp-nodepop .togen { display: flex; align-items: center; gap: .4rem; padding: .35rem .7rem; cursor: pointer; }
.zp-nodepop .togen label { display: flex; align-items: center; gap: .4rem; margin: 0; font-size: .9rem; white-space: nowrap; }
/* 16px, not less: an iPhone zooms the page in on a box set smaller the moment it is tapped, and the
   menu it sits in is pinned to the screen (2026-10-05). */
.zp-nodepop .togen input { width: 4.2rem; padding: .25rem .4rem; font-size: 16px; cursor: text;
  border: var(--bs-border-width) solid var(--bs-border-color); border-radius: .25rem; }
.zp-nodepop .togen .togo { width: auto; padding: .25rem .7rem; border: var(--bs-border-width) solid var(--bs-secondary-border-subtle); }

/* 添加子女 from the ▾: the add form in a native <dialog> dressed as a card (app.js addChild), and the
   row it adds lit for a moment once it is in the chart. */
/* position and height said again: Bootstrap's .card sets position: relative and a height of its own
   variable, which undo the browser's own fixed, fit-content <dialog> - and the box then stretched to
   the full height of the window whatever was in it. */
.zp-adddlg { position: fixed; height: fit-content; width: min(34rem, calc(100vw - 2rem)); max-height: calc(100vh - 2rem); overflow: auto; }
.zp-adddlg::backdrop { background: rgba(0, 0, 0, .35); }
@keyframes zp-justadded { from { background: var(--bs-warning-bg-subtle); } to { background: transparent; } }
.zp-tree li.zp-justadded > .node { animation: zp-justadded 3s ease-out; border-radius: .25rem; }
@media (prefers-reduced-motion: reduce) { .zp-tree li.zp-justadded > .node { animation: none; outline: 2px solid var(--bs-warning); } }

/* Shown while a line is being expanded: 展开所有后代 on the founder is five thousand people, long
   enough that without this the page looks broken rather than busy. */
.zp-busy { position: fixed; inset: 0; z-index: 1060; display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, .35); }
/* One width, whatever the screen: the box is a .card-body too, whose flex: 1 1 auto let it grow
   across the whole overlay - a bar 1,600 px long on a wide screen for one line of words. */
.zp-busy .busy-card { flex: 0 0 auto; width: min(24rem, 86vw); }

/* A second horizontal scrollbar, fixed to the foot of the window, driving the chart's own - whose
   bar is at the foot of the chart, thousands of rows below where a phone reader is. Shown by the
   script whenever the chart is wider than its box AND on screen, whatever the window. No
   backdrop-filter: it would re-composite the moving chart under it on every frame of the swipe. */
.zp-hscroll { position: fixed; left: 0; right: 0; bottom: 0; z-index: 1030; height: 15px; overflow-x: auto; overflow-y: hidden;
  background: rgba(255, 255, 255, .94); border-top: var(--bs-border-width) solid var(--bs-border-color); }
.zp-hscroll > div { height: 1px; }

/* ---- the chart's header, and how much of it follows you down the page -----------------------
   The whole block is sticky - the chart is the one page here you scroll a long way down, and losing
   WHOSE line you are reading is what happens. Once it sticks the crumbs and the toolbar fold away,
   the title shrinks to a line, and ▲▼ brings the rest back. No transform: a promoted layer reports
   its position a frame behind, and this box's position decides whether it has pinned itself. */
.zp-chartmark { height: 0; }
/* Not d-flex: Bootstrap's display utilities are !important, and the fold below has to be able to
   take this row away. */
.zp-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .25rem; }
.zp-charthead { position: sticky; top: var(--zp-headerh); z-index: 1010; background: var(--bs-body-bg); margin: 0 0 1rem; }
/* Scroll anchoring off, on chart pages only (app.js puts the class on <html>). When the header pins
   itself it folds two rows away; Chrome would scroll by the same amount to keep what you were
   looking at in place, which unpins it, which unfolds it - a flip every frame. See
   tools/chart_flicker_probe.js, which drives a real Chrome and reproduces it. */
html.zp-noanchor, html.zp-noanchor body { overflow-anchor: none; }
.zp-charthead.stuck { border-bottom: var(--bs-border-width) solid var(--bs-border-color); padding-top: .55rem; }
.zp-charthead.stuck .zp-crumbs, .zp-charthead.stuck .zp-toolbar { display: none; }
/* Opened out again, with the fold still in the corner: room for it at the end of the crumbs line */
.zp-charthead.stuck.open .zp-crumbs { display: block; padding-right: 2.4rem; }
.zp-charthead.stuck.open .zp-toolbar { display: flex; }
.zp-charthead.stuck.open h1 { padding-right: 0; }
/* A heading when you arrive, a label once you are past it, kept clear of the corner controls - the
   fold, 分享本页 and the padlock - which are out of flow. 16rem is measured: the widest case is
   English, where the corner came to 243px. */
.zp-charthead.stuck h1 { font-size: 1.15rem; margin: .15rem 0 0; padding-right: 16rem; }
/* ...and on a phone the labels go instead of the title: the corner becomes three glyphs */
@media (max-width: 700px) {
  .zp-charthead.stuck .zp-headtools .sharebtn .lbl { display: none; }
  .zp-charthead.stuck h1 { padding-right: 9.5rem; }
}
/* The folded header's own 🔑/🔓 and 分享本页, BESIDE the fold and never beneath it. A flex group,
   so its width is measured by the browser in every language rather than guessed per button. */
.zp-headtools { display: none; }
.zp-charthead.stuck .zp-headtools { display: flex; align-items: center; gap: .35rem; position: absolute; top: .25rem; right: 2.4rem; z-index: 2; }
.zp-charthead.stuck.open .zp-headtools { display: none; }
/* :not([hidden]) is load-bearing: share.js clears `hidden` only where the browser can share or copy */
.zp-charthead .zp-sharehead { display: none; }
.zp-charthead.stuck .zp-headtools .zp-sharehead:not([hidden]) { display: inline-block; }
.zp-charthead .zp-unlockhead .lbl { display: none; }
/* Offered only while something is folded away. The arrow is the state it will put you IN. */
.zp-pinhead { display: none; position: absolute; right: 0; top: .25rem; z-index: 2; padding: .1rem .45rem; line-height: 1;
  font-size: .75rem; border: var(--bs-border-width) solid var(--bs-secondary-border-subtle); border-radius: .25rem;
  background: var(--bs-body-bg); color: var(--bs-secondary-color); cursor: pointer; }
.zp-charthead.stuck .zp-pinhead:not([hidden]) { display: block; }
/* hover only where there is a pointer to hover WITH: on a touch screen :hover latches after a tap */
@media (hover: hover) { .zp-pinhead:hover { color: var(--bs-primary); border-color: var(--bs-primary); } }
.zp-pinhead:focus:not(:focus-visible) { outline: none; }
.zp-pinhead:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: 1px; }
.zp-findfocus { font-size: .9rem; vertical-align: .08em; line-height: 1; }

/* 在本族谱中查名, kept within reach inside the sticky header. On a phone the label goes (to screen
   readers only - they still have it through for="tree-q"): the placeholder says the same thing. */
.zp-treefind { position: relative; display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin: 0; padding: .55rem 0; }
.zp-treefind-box { flex: 1; max-width: 26rem; min-width: 10rem; }
.zp-recentbtn { line-height: 1; }
@media (max-width: 600px) {
  .zp-treefind > label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
}
/* the results under a search box, over whatever is below it */
.zp-drop { position: absolute; top: 100%; left: 0; right: 0; z-index: 1035; margin-top: .25rem; max-height: 300px; overflow: auto;
  box-shadow: var(--bs-box-shadow-lg); max-width: 36rem; }

/* ==== 族谱统计 and 世代名录 ====================================================================== */
/* A box that scrolls sideways rather than squeezing what is in it: a chart whose text would shrink
   to five pixels at phone width, a table whose columns would wrap a number across two lines. */
.zp-scroll { overflow-x: auto; }
.zp-census { min-width: 30rem; }
.zp-census td, .zp-census th { text-align: right; white-space: nowrap; }
.zp-census thead th:first-child, .zp-census tbody th, .zp-census tfoot th, .zp-census .gc { text-align: left; }
.zp-census tbody th { font-weight: 600; }
/* 行辈字 in the book's face and the site's colour, in the table and under the charts' bars alike */
.zp-census .gc { font-family: var(--zp-serif); color: var(--bs-primary); }
.zp-census tfoot th, .zp-census tfoot td { border-top: 2px solid var(--bs-border-color); border-bottom: none; font-weight: 600; }
.zp-census .barcell { width: 34%; min-width: 8rem; }
/* 男 / 女 / 未注明 inside one bar, divided by flex-grow: the browser splits the row up rather than
   this site doing arithmetic it could get wrong. Three hues that stay apart at eight pixels high -
   primary, teal, and grey for the unrecorded. min-width: 世1 is one man against a thousand, and a
   truthful 0.1% bar is an invisible one. */
.zp-census .bar { display: flex; height: .5rem; min-width: 3px; border-radius: var(--bs-border-radius); overflow: hidden;
  background: var(--bs-secondary-bg); }
.zp-census .bar i { display: block; height: 100%; min-width: 1px; }
.zp-census .bar i.m { background: var(--bs-primary); }
.zp-census .bar i.f { background: var(--bs-teal); }
.zp-census .bar i.u { background: var(--bs-gray-500); }
@media (max-width: 600px) { .zp-census .barcell { display: none; } .zp-census { min-width: 0; } }

/* The two charts, as inline SVG from app/views/_census_charts.php. The min-width holds the text at
   a legible size and .zp-scroll lets it move sideways instead of shrinking. */
.zp-censuschart { display: block; width: 100%; min-width: 34rem; height: auto; }
.zp-censuschart .cbar, .zp-censuschart .cdot, .zp-censuschart .cover { fill: var(--bs-primary); }
.zp-censuschart .grid { stroke: var(--bs-border-color); stroke-width: 1; }
.zp-censuschart .axisline { stroke: var(--bs-secondary-border-subtle); stroke-width: 1; }
.zp-censuschart .axis { fill: var(--bs-secondary-color); font-size: 11px; font-family: var(--bs-body-font-family); }
/* 行辈字 under its 世, a point smaller than the number: the number is what the eye counts along and
   the character is what it recognises, so the two should not compete */
.zp-censuschart .gchar { fill: var(--bs-primary); font-size: 10px; font-family: var(--zp-serif); }
/* one series, one colour: a second colour on the dots would say there were two things to compare */
.zp-censuschart .cline { fill: none; stroke: var(--bs-primary); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.zp-censuschart .overlabel { fill: var(--bs-primary); font-size: 10px; }
/* 1.00, the generation that held its own: a rule to read the line against, not a value */
.zp-censuschart .hold { stroke: var(--bs-secondary-color); stroke-width: 1; stroke-dasharray: 4 3; }
.zp-censuschart .hold-label { fill: var(--bs-secondary-color); font-size: 10px; }

.zp-roster { min-width: 34rem; }
.zp-roster .idx { width: 3.5em; text-align: right; color: var(--bs-secondary-color); }
.zp-roster .num { text-align: right; white-space: nowrap; }
.zp-roster .idx, .zp-roster .num { font-variant-numeric: tabular-nums; }
@media (max-width: 600px) { .zp-roster { min-width: 0; } .zp-roster .idx { display: none; } }

/* ---- the 族谱简介 editor (tree_admin.php, app.js) ---------------------------------------------
   The editor's own parts keep short names - richbar, richbox, richsrc - scoped under .zp-richedit;
   the table menu and the size dialog live on <body> and carry prefixed names of their own. */
.zp-richedit .richbox { min-height: 8rem; max-height: 22rem; overflow-y: auto; line-height: 1.7; }
.zp-richedit .richbox p { margin: 0 0 .6rem; }
.zp-richedit .richbox ul, .zp-richedit .richbox ol { margin: 0 0 .6rem; padding-left: 1.4rem; }
.zp-richedit .richbox > :last-child { margin-bottom: 0; }
.zp-richedit .richbar .btn { min-width: 2.2rem; }
.zp-richedit .richbar .btn[aria-pressed="true"] { background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis); }
.zp-richedit .richbar .btn[disabled] { opacity: .4; }
/* The row and column menu. position:fixed because it is placed from clientX/clientY, which are
   viewport coordinates. The items are 44px high: on a touch screen this menu is the ONLY way to
   reach these commands, so they are the size of a finger, not a pointer. */
.zp-tblmenu { position: fixed; z-index: 1070; min-width: 11rem; display: flex; flex-direction: column; padding: .25rem;
  background: var(--bs-body-bg); border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius);
  box-shadow: var(--bs-box-shadow-lg); }
.zp-tblmenu button { width: 100%; text-align: start; font: inherit; color: var(--bs-body-color); background: none; border: 0;
  border-radius: .25rem; padding: .55rem .8rem; min-height: 2.75rem; cursor: pointer; }
.zp-tblmenu button:hover, .zp-tblmenu button:focus { background: var(--bs-primary-bg-subtle); color: var(--bs-primary-text-emphasis); outline: none; }
/* the two that take something away, set apart from the four that add */
.zp-tblmenu button.cut { margin-top: .25rem; padding-top: .7rem; border-top: var(--bs-border-width) solid var(--bs-border-color); }
.zp-tbldlg { width: min(22rem, calc(100vw - 2rem)); }
.zp-tbldlg::backdrop { background: rgba(0, 0, 0, .35); }
/* 待审修改: the bar that acts on what is ticked stays in view while the list scrolls under it */
.zp-batchbar { position: sticky; top: calc(var(--zp-headerh) + .25rem); z-index: 1000; }
.zp-batchnote { flex: 1 1 12rem; min-width: 8rem; width: auto; }
.zp-diff td:first-child { width: 9em; color: var(--bs-secondary-color); }

/* ==== the two export pages: 下载族谱 PDF and 世系长图 =========================================== */
/* A choice card is a .btn-check radio and its label (opt_card() in I18n.php). Bootstrap would fill
   a chosen outline button solid grey with white text, which buries a cover picture; here the chosen
   card keeps its colours and gets the primary border over the primary tint instead. */
.zp-opt { --bs-btn-color: var(--bs-body-color); --bs-btn-border-color: var(--bs-border-color);
  --bs-btn-hover-color: var(--bs-body-color); --bs-btn-hover-bg: var(--bs-tertiary-bg); --bs-btn-hover-border-color: var(--bs-secondary-border-subtle);
  --bs-btn-active-color: var(--bs-body-color); --bs-btn-active-bg: var(--bs-primary-bg-subtle); --bs-btn-active-border-color: var(--bs-primary);
  font-weight: normal; padding: .65rem .75rem; }
.btn-check:checked + .zp-opt { box-shadow: inset 0 0 0 1px var(--bs-primary); }
.zp-opt b { margin-bottom: .15rem; }
.zp-opt small { color: var(--bs-secondary-color); }

/* THE SPECIMEN of a face: set large, because at body size two 宋 weights are a guess. Not italic, not
   synthesised: the browser must draw exactly the face that arrived. */
.zp-spec { display: block; font-size: 1.55rem; line-height: 1.45; margin: -.1rem 0 .45rem; word-break: break-all;
  font-style: normal; font-synthesis: none; }
.zf-serif400, .zf-sans400 { font-weight: 400; }
.zf-serif700, .zf-sans700 { font-weight: 700; }

/* The cover picture, at the proportions of the paper it is drawn on: aspect-ratio rather than a
   height, so the box keeps the shape of a book before the image arrives - and the swatch colour
   under it is what shows then. */
.zp-opts .coverpic { display: block; width: 7.5rem; max-width: 100%; height: auto; aspect-ratio: 1 / 1.414;
  margin: 0 auto .55rem; object-fit: cover; border-radius: var(--bs-border-radius); }
/* a layout sample is a PAGE of the book, so it is shown whole, on paper */
.zp-opts .coverpic.sample { object-fit: contain; background: #fff; border: var(--bs-border-width) solid var(--bs-border-color); }
/* Two controls on a cover card are NOT part of choosing it - the zoom over the picture and the photo
   bar under it - so they are siblings of the label, and the card is their positioning context.
   Centred by flex: ⤢ carries its own ascent, which a line-height cannot correct. */
.zp-opts .coverzoom { position: absolute; top: .9rem; right: .9rem; width: 1.7rem; height: 1.7rem; padding: 0; border: 0;
  border-radius: 50%; background: rgb(0 0 0 / .42); color: #fff; font-size: .85rem; display: flex; align-items: center;
  justify-content: center; line-height: 1; cursor: pointer; opacity: 0; transition: opacity .15s ease-in-out; z-index: 2; }
.zp-opts .opt-cover:hover .coverzoom, .zp-opts .coverzoom:focus-visible { opacity: 1; }
@media (hover: none) { .zp-opts .coverzoom { opacity: .85; } }     /* a finger has no hover to reveal it with */
.zp-opts .photobar { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem; margin-top: .35rem; }
.zp-opts .pmsg { color: var(--bs-secondary-color); }
.zp-opts .pmsg.bad { color: var(--bs-danger-text-emphasis); }
/* The cover as a page, not a thumbnail. A plain <dialog>, so Esc and the top layer come free. */
dialog.coverbig { border: 0; padding: 0; background: #0000; max-width: 92vw; max-height: 92vh; }
dialog.coverbig::backdrop { background: rgb(0 0 0 / .72); }
dialog.coverbig img { display: block; max-width: 92vw; max-height: 92vh; width: auto; height: auto; box-shadow: 0 1.2rem 3rem rgb(0 0 0 / .5); }
/* NOT THEME COLOURS: pictures of the physical cover stock - red cloth, black ink, jade - and the
   printed book is not themed. The traditional ones show their STRUCTURE, the same drawing the cover
   itself makes: 瓷青's title slip, 朱丝栏's ruled columns, 线装's cloth spine. */
.sw-red { background: linear-gradient(135deg, #8b1a1a, #b8322a); }
.sw-ink { background: linear-gradient(135deg, #1c1c1c, #3a3a3a); }
.sw-jade { background: linear-gradient(135deg, #2e5e4e, #4b8a72); }
.sw-qingzhi { background: linear-gradient(90deg, #1d344f 0 38%, #f3ead6 38% 62%, #1d344f 62% 100%); }
.sw-zhulan { background: repeating-linear-gradient(90deg, #bf242a 0 1px, #0000 1px 13px), #f4eddd; }
.sw-xianzhuang { background: linear-gradient(90deg, #4f2e21 0 86%, #3e2218 86% 100%); }
.sw-cangqing { background: linear-gradient(90deg, #ede8db 0 80%, #26436b 80% 100%); }
.sw-jinjin { background: linear-gradient(135deg, #241a16, #42331f); }
.sw-kuaimian { background: linear-gradient(180deg, #1b1d21 0 40%, #bf242a 40% 43%, #e8dcc8 43% 100%); }
.sw-zhumian { background: #bd3227; }
/* The 世次栏 styles: a tall strip repeating the pattern each one rules. A pattern is CSS's to draw;
   the covers above are not, which is why they are pictures from the renderer and these are not. */
.zp-opts .rulerpic { width: 22px; height: 54px; margin: 0 0 .5rem; border-radius: 2px; }
.rp-boxed { border: 1px solid #8a8a8a; background: repeating-linear-gradient(180deg, #0000 0 12px, #8a8a8a 12px 13px); }
.rp-plain { background: repeating-linear-gradient(180deg, #0000 3px 12px, #b5b5b5 12px 13px, #0000 13px); }
.rp-tint { background: repeating-linear-gradient(180deg, #e5e2d9 0 11px, #0000 11px 13px); }
.rp-tab { background: repeating-linear-gradient(180deg, #4d4843 0 11px, #0000 11px 13px); }
/* 圆角标 is the tab rounded and pulled in off the edge */
.rp-pill { background: repeating-linear-gradient(180deg, #171717 0 10px, #0000 10px 13px); background-clip: content-box;
  padding: 0 2px; border-radius: 4px; }

/* the page a render is waited on */
.zp-genbox { max-width: 42rem; }
/* Looks unavailable and still takes the click, so the script can say 「还没好」. Bootstrap's own
   .disabled would swallow the click with pointer-events:none. */
.zp-notready { opacity: .6; cursor: not-allowed; }

/* ---- one page of the scanned book (no scans are served in v1; the page is kept working) ------ */
/* Every sheet one click away: a strip of page buttons, either numbering, the current one filled. */
.zp-scanpages { margin: .4rem 0 .6rem; }
.zp-scanpages .scanstrip { position: relative; display: flex; gap: .25rem; overflow-x: auto; padding: .2rem 0 .45rem; }
.zp-scanpages .pg { flex: 0 0 auto; min-width: 2.3em; padding: .2rem .4rem; text-align: center; font-size: .85rem;
  border: var(--bs-border-width) solid var(--bs-border-color); border-radius: .25rem; text-decoration: none; color: var(--bs-body-color); }
.zp-scanpages a.pg:hover { border-color: var(--bs-primary); }
.zp-scanpages .pg.cur { background: var(--bs-primary); color: #fff; border-color: var(--bs-primary); font-weight: 600; cursor: default; }
.zp-scanpages[data-num="pdf"] .nb, .zp-scanpages[data-num="book"] .np { display: none; }
.zp-scanpages .nb.none { color: var(--bs-secondary-color); }
.zp-scanpages .pg.cur .nb.none { color: inherit; }
.zp-scanpages .numtoggle .btn[aria-pressed="true"] { background: var(--bs-secondary); color: #fff; }
/* a shadow rather than a border: the scan has its own paper edge */
.zp-scanimg { display: block; width: 100%; max-width: 1000px; height: auto; margin: 0 auto 1rem; box-shadow: var(--bs-box-shadow); }

/* ---- 变更记录: the old value struck through and the new one plain, so which way a change went is
   legible without reading the arrow. An undone change stays in the list, set back. */
.zp-histlist .zp-undone { opacity: .62; }
.zp-histdiff li { display: flex; flex-wrap: wrap; gap: .35rem; align-items: baseline; padding: .1rem 0; }
.zp-histdiff .hf { color: var(--bs-secondary-color); min-width: 4.5rem; }
.zp-histdiff .hfrom { text-decoration: line-through; color: var(--bs-secondary-color); }
.zp-histdiff .harrow { color: var(--bs-secondary-color); }

/* 🔓 once this reader has the family's code: the same button, saying it is already open */
.unlockbtn.is-open { --bs-btn-color: var(--bs-success-text-emphasis); --bs-btn-border-color: var(--bs-success-border-subtle);
  --bs-btn-hover-bg: var(--bs-success-bg-subtle); --bs-btn-hover-color: var(--bs-success-text-emphasis);
  --bs-btn-hover-border-color: var(--bs-success-border-subtle); }

/* Where WeChat's ··· menu is, said over the page (share.js). A layer over everything, because what it
   points at is WeChat's own bar above the viewport. The arrow is a character: ↗ is in every font a
   phone ships with, costs nothing to load, and points the way at any size. */
.zp-wxguide { position: fixed; inset: 0; z-index: 2000; background: rgba(0, 0, 0, .72); display: flex; flex-direction: column;
  align-items: flex-end; justify-content: flex-start; padding: .6rem 1rem 0; gap: .4rem; color: #fff; text-align: right;
  cursor: pointer; -webkit-tap-highlight-color: transparent; }
.zp-wxguide::before { content: '\2197'; font-size: 2.6rem; line-height: 1; margin-right: .2rem; animation: zp-wxpoint 1.4s ease-in-out infinite; }
.zp-wxguide .wxsay { margin: 0; font-size: 1.05rem; font-weight: 600; max-width: 15rem; line-height: 1.5; }
.zp-wxguide .wxnote { margin: 0; font-size: .85rem; opacity: .75; max-width: 15rem; }
@keyframes zp-wxpoint { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(3px, -3px); } }
@media (prefers-reduced-motion: reduce) { .zp-wxguide::before { animation: none; } }
/* The registration form's trap for scripts (routes.php, /register): off-screen rather than
   display:none, which some form fillers know to skip. aria-hidden and tabindex=-1 keep it from
   a person using a screen reader or the keyboard. */
.zp-hp { position: absolute; left: -10000px; top: auto; width: 1px; height: 1px; overflow: hidden; }

/* 开始建谱's steps (wizard_steps): four numbered circles joined by a line, done ones filled with a
   tick, the current one ringed. The line into each step is drawn by that step, from the middle of
   the one before it, so it needs no measuring and no script - and so it runs under the circle
   before it, which comes earlier in the page and would otherwise be painted over: the circles are
   lifted above every line. */
.zp-steps { display: flex; list-style: none; padding: 0; margin: 0 0 1.25rem; }
.zp-step { flex: 1 1 0; position: relative; display: flex; flex-direction: column; align-items: center;
  text-align: center; font-size: .85rem; color: var(--bs-secondary-color); }
.zp-step::before { content: ""; position: absolute; top: 1rem; left: -50%; right: 50%; height: 2px;
  margin-top: -1px; background: var(--bs-border-color); z-index: 0; }
.zp-step:first-child::before { display: none; }
.zp-step.is-done::before, .zp-step.is-now::before { background: var(--bs-primary); }
.zp-step-no { position: relative; z-index: 1; width: 2rem; height: 2rem; border-radius: 50%; display: inline-flex;
  align-items: center; justify-content: center; border: 2px solid var(--bs-border-color);
  background: var(--bs-body-bg); font-weight: 600; margin-bottom: .3rem; }
.zp-step.is-done .zp-step-no { background: var(--bs-primary); border-color: var(--bs-primary); color: #fff; }
.zp-step.is-now .zp-step-no { border-color: var(--bs-primary); color: var(--bs-primary); }
.zp-step.is-now { color: var(--bs-body-color); font-weight: 600; }
/* 邀请族人: the invitation to paste into the family chat, big enough to read before copying */
.zp-invite textarea { font-size: .95rem; line-height: 1.55; resize: vertical; }
/* 使用教程 (/guide): numbered steps, each with its picture. A phone screenshot is drawn at a
   phone's width even on a desk (guide_shot halves its pixels), framed so it reads as a screen
   rather than as part of this page. */
.zp-gstep { margin-bottom: 2.25rem; }
.zp-gstep > h2 { display: flex; align-items: center; gap: .6rem; }
.zp-gno { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 1.9rem;
  height: 1.9rem; border-radius: 50%; background: var(--bs-primary); color: #fff; font-size: 1rem; font-weight: 600; }
.zp-shot { margin: 1rem 0 0; }
.zp-shot img { max-width: 100%; height: auto; border: 1px solid var(--bs-border-color); border-radius: .6rem;
  box-shadow: 0 .15rem .6rem rgba(0, 0, 0, .08); background: var(--bs-body-bg); }
.zp-shot figcaption { font-size: .85rem; color: var(--bs-secondary-color); margin-top: .4rem; }
.zp-tip { background: var(--bs-tertiary-bg); border-left: 4px solid var(--bs-primary); padding: .8rem 1rem;
  border-radius: .3rem; margin: 1.5rem 0; }
.zp-tip > :last-child { margin-bottom: 0; }
.zp-guidecard { color: inherit; transition: box-shadow .15s; }
.zp-guidecard:hover { box-shadow: 0 .25rem .8rem rgba(0, 0, 0, .12); }
.zp-guidecard .card-img-top { height: 10rem; object-fit: cover; object-position: top center;
  border-bottom: 1px solid var(--bs-border-color); }
/* The home page's banner: the owner's photo, the owner's words in 楷书, and 创建族谱 in the seal's
   red. The family sits left of the middle, so on a phone or a tablet the photo comes first and the
   words under it; from 992px the words over its open right side - the hills and the village - under
   a wash that fades out before the family begins (the owner's second photo, 2026-10-18). */
.zp-banner { position: relative; overflow: hidden; border-radius: .5rem; background: #2b2118; }
.zp-banner-img { display: block; width: 100%; height: auto; }
.zp-banner-text { padding: 1.1rem 1.25rem 1.4rem; color: #fff; }
.zp-banner-words { font-family: "ZupuKai", var(--zp-serif); font-size: 1.35rem; line-height: 1.6;
  letter-spacing: .04em; margin-bottom: .2rem; }
.zp-banner-words2 { margin-bottom: 1.1rem; }
.zp-btn-seal { --bs-btn-color: #fff; --bs-btn-bg: #b8312f; --bs-btn-border-color: #b8312f;
  --bs-btn-hover-color: #fff; --bs-btn-hover-bg: #9e2826; --bs-btn-hover-border-color: #9e2826;
  --bs-btn-active-color: #fff; --bs-btn-active-bg: #8a2321; --bs-btn-active-border-color: #8a2321;
  --bs-btn-focus-shadow-rgb: 184, 49, 47; padding-left: 2rem; padding-right: 2rem; letter-spacing: .05em; }
/* 创建族谱, large: the one thing the banner asks */
.zp-banner .zp-btn-seal { font-size: 1.3rem; padding: .7rem 2.6rem; box-shadow: 0 .2rem .6rem rgba(0, 0, 0, .3); }
/* English has no 楷书 and runs long: the serif, a size down, so it keeps to the photo's right side */
html[lang="en"] .zp-banner-words { font-size: 1.15rem; letter-spacing: 0; }
@media (min-width: 992px) {
  /* a wide banner shows the photo's middle band: from the man's head down to the book */
  .zp-banner-img { height: 26rem; object-fit: cover; object-position: 50% 62%; }
  .zp-banner-text { position: absolute; top: 0; bottom: 0; right: 0; width: 40%; display: flex;
    flex-direction: column; justify-content: center; align-items: flex-start; padding: 2rem 2rem 2rem 3.5rem;
    background: linear-gradient(270deg, rgba(25, 18, 12, .3) 0%, rgba(25, 18, 12, .2) 65%, rgba(25, 18, 12, 0) 100%); }
  .zp-banner-words { font-size: 1.9rem; text-shadow: 0 1px 3px rgba(0, 0, 0, .9); }
  html[lang="en"] .zp-banner-words { font-size: 1.35rem; }
  /* Start a genealogy, free: on one line in the right 40% at 992px */
  html[lang="en"] .zp-banner .zp-btn-seal { padding-left: 1.5rem; padding-right: 1.5rem; white-space: nowrap; }
}
@media (min-width: 1200px) {
  .zp-banner-img { height: 28rem; }
  .zp-banner-words { font-size: 2.2rem; }
  html[lang="en"] .zp-banner-words { font-size: 1.6rem; }
  .zp-banner .zp-btn-seal { font-size: 1.45rem; padding: .8rem 3rem; }
}
/* the home page's case for making a genealogy here */
.zp-promo { border: 1px solid var(--bs-primary-border-subtle); background: var(--bs-primary-bg-subtle); }
.zp-promo .zp-promo-item h3 { font-size: 1.05rem; font-weight: 600; margin-bottom: .3rem; text-align: center; }
.zp-promo-icon { display: inline-flex; align-items: center; justify-content: center; width: 3.5rem; height: 3.5rem;
  margin-bottom: .6rem; border-radius: 50%; background: var(--bs-body-bg); color: var(--bs-primary);
  box-shadow: 0 .1rem .4rem rgba(13, 110, 253, .15); }
.zp-promo-icon svg { width: 2.1rem; height: 2.1rem; }
/* each of the three: its icon and heading centred over the words, which read as before (the owner's, 2026-10-01) */
.zp-promo .zp-promo-icon { display: flex; margin-left: auto; margin-right: auto; }
/* 为什么选择中华族谱网: the same icons, each reason a card of its own - styled by the card, not by
   the section, because the about page draws the same cards (_why_cards.php) */
.zp-why-item { display: flex; gap: .9rem; align-items: flex-start; height: 100%; padding: 1rem 1.1rem;
  border: 1px solid var(--bs-border-color); border-radius: .6rem; background: var(--bs-body-bg); }
.zp-why-item h3 { font-size: 1.05rem; font-weight: 600; margin-bottom: .3rem; }
.zp-why-item .zp-promo-icon { flex: 0 0 auto; width: 3rem; height: 3rem; margin-bottom: 0;
  background: var(--bs-primary-bg-subtle); box-shadow: none; }
.zp-why-item .zp-promo-icon svg { width: 1.8rem; height: 1.8rem; }

/* 中国历史年表图 (app/Chronology.php): the chart scrolls inside its own box on a narrow screen; ticks
   and years in the page's greys; each 朝代 its own soft colour, twelve round the wheel, handed out
   by Chronology::hue so two that meet are far apart, its name in dark ink on it; a ring in the
   seal's red under the pointer, 2px, half of it in the 2px gap the chart leaves between bars, so
   the next bar, drawn after, cannot cover it. */
.zp-chrono-scroll { overflow-x: auto; margin: .75rem 0; -webkit-overflow-scrolling: touch; }
svg.zp-chrono { display: block; font-family: var(--bs-body-font-family); }
svg.zp-chrono text { font-size: 12px; fill: var(--bs-body-color); }
svg.zp-chrono .zp-chr-year { font-size: 11px; fill: var(--bs-secondary-color); }
svg.zp-chrono .zp-chr-tick { stroke: var(--bs-border-color); stroke-dasharray: 2 4; }
svg.zp-chrono .zp-chr-reg rect { stroke: none; stroke-width: 2; }
svg.zp-chrono .zp-chr-h0 rect { fill: hsl(0 62% 83%); } .zp-chr-sw.zp-chr-h0 { background: hsl(0 62% 83%); }
svg.zp-chrono .zp-chr-h1 rect { fill: hsl(30 62% 83%); } .zp-chr-sw.zp-chr-h1 { background: hsl(30 62% 83%); }
svg.zp-chrono .zp-chr-h2 rect { fill: hsl(60 62% 83%); } .zp-chr-sw.zp-chr-h2 { background: hsl(60 62% 83%); }
svg.zp-chrono .zp-chr-h3 rect { fill: hsl(90 62% 83%); } .zp-chr-sw.zp-chr-h3 { background: hsl(90 62% 83%); }
svg.zp-chrono .zp-chr-h4 rect { fill: hsl(120 62% 83%); } .zp-chr-sw.zp-chr-h4 { background: hsl(120 62% 83%); }
svg.zp-chrono .zp-chr-h5 rect { fill: hsl(150 62% 83%); } .zp-chr-sw.zp-chr-h5 { background: hsl(150 62% 83%); }
svg.zp-chrono .zp-chr-h6 rect { fill: hsl(180 62% 83%); } .zp-chr-sw.zp-chr-h6 { background: hsl(180 62% 83%); }
svg.zp-chrono .zp-chr-h7 rect { fill: hsl(210 62% 83%); } .zp-chr-sw.zp-chr-h7 { background: hsl(210 62% 83%); }
svg.zp-chrono .zp-chr-h8 rect { fill: hsl(240 62% 83%); } .zp-chr-sw.zp-chr-h8 { background: hsl(240 62% 83%); }
svg.zp-chrono .zp-chr-h9 rect { fill: hsl(270 62% 83%); } .zp-chr-sw.zp-chr-h9 { background: hsl(270 62% 83%); }
svg.zp-chrono .zp-chr-h10 rect { fill: hsl(300 62% 83%); } .zp-chr-sw.zp-chr-h10 { background: hsl(300 62% 83%); }
svg.zp-chrono .zp-chr-h11 rect { fill: hsl(330 62% 83%); } .zp-chr-sw.zp-chr-h11 { background: hsl(330 62% 83%); }
svg.zp-chrono .zp-chr-reg text { fill: #1f1d1a; }
svg.zp-chrono .zp-chr-reg:hover rect { stroke: #b8312f; }
/* the tag of a bar too short to hold its name: its colour, a hairline round it, a line to the bar */
svg.zp-chrono .zp-chr-reg rect.zp-chr-tag { stroke: rgba(0, 0, 0, .22); stroke-width: 1; }
svg.zp-chrono .zp-chr-reg:hover rect.zp-chr-tag { stroke: #b8312f; stroke-width: 2; }
svg.zp-chrono .zp-chr-lead { stroke: var(--bs-secondary-color); stroke-width: 1; }
.zp-chr-sw { display: inline-block; width: .8em; height: .8em; margin-right: .45em; border-radius: 2px; vertical-align: -.05em; }
.zp-chrono-list tr.zp-chr-beside td:first-child { padding-left: 1.4rem; color: var(--bs-secondary-color); }
/* 族谱溯源 against it (app/views/origin_chrono.php): each forebear a bar or a dot in the site's red, a
   faint line across to the dynasties, an estimate a hollow ring; a label moved off his year (so that
   none covers another) joined to his mark by a hairline. A phone gets its own narrower chart. */
svg.zp-chrono .zp-chr-man rect, svg.zp-chrono .zp-chr-man circle { fill: #b8312f; }
svg.zp-chrono .zp-chr-guide { stroke: #b8312f; stroke-opacity: .45; stroke-dasharray: 3 3; }
svg.zp-chrono .zp-chr-point { stroke: var(--bs-secondary-color); stroke-width: .8; }
svg.zp-chrono .zp-chr-man text { font-weight: 600; }
svg.zp-chrono .zp-chr-est circle { fill: var(--bs-body-bg); stroke: #b8312f; stroke-width: 1.5; stroke-dasharray: 2 2; }
svg.zp-chrono .zp-chr-est .zp-chr-guide { stroke-opacity: .25; }
svg.zp-chrono .zp-chr-est text { font-weight: 400; fill: var(--bs-secondary-color); }
.zp-chrono-phone { display: none; }
svg.zp-chrono-narrow { width: 100%; height: auto; }
@media (max-width: 575.98px) {
  .zp-chrono-wide { display: none; }
  .zp-chrono-phone { display: block; }
}

/* ---- 中国地方: a form folded to a link that opens it - 补充旧称, and 删除重复的自然村 for a system
   administrator (app/views/village.php) */
.zp-linkfold > summary { list-style: none; cursor: pointer; display: inline; text-decoration: underline; }
.zp-linkfold > summary::-webkit-details-marker { display: none; }

/* ---- 中国地方: a town's 行政村, each folding over its 自然村 (app/views/village.php). The summary is a
   flex row, which drops the browser's own marker, so the ▾ is drawn here and turns when folded. */
.zp-admin > summary { list-style: none; cursor: pointer; }
.zp-admin > summary::-webkit-details-marker { display: none; }
.zp-admin > summary::before { content: "▾"; display: inline-block; width: 1em; color: var(--bs-secondary-color); transition: transform .15s; }
.zp-admin:not([open]) > summary::before { transform: rotate(-90deg); }
@media (prefers-reduced-motion: reduce) { .zp-admin > summary::before { transition: none; } }

/* ---- 中国地方's map (www/assets/placemap.js, zpmap.js), from TaishanZupu's ----------------------------
   地图 | 列表 over a surname's places; the map's box; its popups. The tiles are light whatever the theme,
   so the controls and the credit on them are too; a popup is the page's own paper and ink. */
.pmapbar [aria-pressed="true"] { background: var(--bs-primary); color: #fff; }
.pmap-canvas { position: relative; min-height: 18rem; border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius); isolation: isolate; overflow: hidden; }
/* Its height, with two classes: zpmap.js makes this box the map, a .zpmap, and .zpmap's height: 100%
   below - of a parent with no height - would win over one class. That is how the map was 18rem on
   every screen from the start, and on a phone and a tablet it stays so. A large screen gives it a box
   to match its width, the container being 960-1320px wide there (the owner's, 2026-10-04); 80vh
   leaves the bar above it in view. tools/place_map_probe.js measures both. */
.pmap .pmap-canvas { height: 18rem; }
@media (min-width: 992px) { .pmap .pmap-canvas { height: min(80vh, 56rem); } }
/* A village's own map (village.php): smaller than a surname's, above the links to the three maps. Each
   village a dot - a 行政村 square - the page's own red; a label beside each, on a light ground to read
   over the tiles, the page's own bold. */
.pmap-here .pmap-canvas { height: 16rem; min-height: 0; }
@media (min-width: 992px) { .pmap-here .pmap-canvas { height: 24rem; } }
.pmap-dot { border-radius: 50%; background: var(--bs-primary); border: 2px solid #fff; box-shadow: 0 0 2px rgba(0, 0, 0, .5); }
.pmap-dot-admin { border-radius: 2px; }
.pmap-dot-here { background: #a3261f; z-index: 2; }
.pmap-tag { font-size: .75rem; line-height: 1.3; color: #212529; background: rgba(255, 255, 255, .85); padding: 0 .25rem; border-radius: 3px; }
.pmap-tag-here { font-weight: 700; z-index: 3; }
/* the page's own place over every other dot and label (z-index, above); a 市's map, of many more places, taller */
.pmap-here.pmap-many .pmap-canvas { height: 22rem; }
@media (min-width: 992px) { .pmap-here.pmap-many .pmap-canvas { height: 32rem; } }
.pmap-loading { position: absolute; inset: 0; margin: 0; display: flex; align-items: center; justify-content: center;
  color: var(--bs-secondary-color); background: var(--bs-body-bg); }
.pmap-pop { max-height: 16rem; overflow: auto; }
.pmap-item + .pmap-item { border-top: 1px solid var(--bs-border-color); margin-top: .45rem; padding-top: .45rem; }
.pmap-xings { margin: .25rem 0; }
.pmap-open { margin-top: .15rem; }

/* The map itself. One finger scrolls the page over it, as anywhere; two are the map's (zpmap.js). */
.zpmap { position: relative; overflow: hidden; touch-action: pan-x pan-y; background: #e9ecef; cursor: grab;
  -webkit-user-select: none; user-select: none; font-size: .9rem; outline: none; height: 100%; }
.zpmap:focus-visible { box-shadow: 0 0 0 .2rem rgba(13, 110, 253, .35); }
.zpmap.zpmap-dragging { cursor: grabbing; }
.zpmap-tiles, .zpmap-markers { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
/* a pinch scales the tiles about a point zpmap.js names in pixels from the box's corner, so from there */
.zpmap-tiles { transform-origin: 0 0; }
.zpmap-markers { pointer-events: none; }
img.zpmap-tile { position: absolute; width: 256px; height: 256px; max-width: none; border: 0; -webkit-user-drag: none; pointer-events: none; }
svg.zpmap-vec { position: absolute; left: 0; top: 0; pointer-events: none; overflow: visible; }
svg.zpmap-vec .zpmap-circle { pointer-events: visiblePainted; cursor: pointer; }
.zpmap-marker { position: absolute; pointer-events: auto; cursor: pointer; }
.zpmap-label { position: absolute; transform: translateY(-50%); white-space: nowrap; pointer-events: none; }
.zpmap-popup { position: absolute; z-index: 3; cursor: auto; -webkit-user-select: text; user-select: text; }
.zpmap-popup-inner { position: relative; background: var(--bs-body-bg); color: var(--bs-body-color); border-radius: 10px;
  box-shadow: 0 3px 14px rgba(0, 0, 0, .3); padding: .6rem 36px .6rem .9rem; line-height: 1.5; }
.zpmap-popup-content { overflow-wrap: anywhere; }
.zpmap-popup-tip { width: 14px; height: 14px; margin: -8px auto 0; background: var(--bs-body-bg); transform: rotate(45deg);
  box-shadow: 3px 3px 6px rgba(0, 0, 0, .15); }
button.zpmap-close { position: absolute; top: 0; right: 0; display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; margin: 0; padding: 0; background: none; border: 0; border-radius: 0 10px 0 6px;
  color: var(--bs-secondary-color); font-size: 1.75rem; line-height: 1; }
button.zpmap-close:hover { color: var(--bs-body-color); }
@media (pointer: coarse) {
  .zpmap-popup-inner { padding-right: 44px; }
  button.zpmap-close { width: 44px; height: 44px; font-size: 2.1rem; }
}
/* + and -, in Google Maps' manner: a white box at the bottom right, over the credit */
.zpmap-ctl { position: absolute; right: 10px; bottom: 26px; z-index: 4; display: flex; flex-direction: column;
  align-items: center; width: 40px; background: #fff; border-radius: 2px; box-shadow: rgba(0, 0, 0, .3) 0 1px 4px -1px; user-select: none; }
.zpmap-ctl button { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; margin: 0;
  padding: 0; border: 0; border-radius: 2px; background: none; color: #666; cursor: pointer; transition: color .15s; }
.zpmap-ctl button:hover, .zpmap-ctl button:active { background: none; color: #333; }
.zpmap-ctl button:focus-visible { box-shadow: inset 0 0 0 2px var(--bs-primary); }
.zpmap-ctl button[aria-disabled="true"], .zpmap-ctl button[aria-disabled="true"]:hover { color: #d0d0d0; cursor: default; }
.zpmap-ctl svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; }
.zpmap-ctl-rule { width: 30px; height: 1px; background: #e6e6e6; }
.zpmap-attrib { position: absolute; right: 0; bottom: 0; z-index: 4; padding: 0 6px; background: rgba(255, 255, 255, .85);
  color: #333; font-size: 11px; line-height: 1.6; }
.zpmap-attrib a { color: #0a58ca; }
/* told to one finger, or to the wheel, moving over the map, for a moment: how to move it instead */
.zpmap-hint { position: absolute; top: 10px; left: 50%; z-index: 5; transform: translateX(-50%); max-width: calc(100% - 20px);
  padding: .35rem .85rem; border-radius: 999px; background: rgba(32, 33, 36, .85); color: #fff; font-size: .9rem;
  line-height: 1.4; text-align: center; width: max-content; box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
  pointer-events: none; opacity: 0; transition: opacity .25s; }
.zpmap-hint.on { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .zpmap-hint { transition: none; } }

/* 申请关联族谱 (0021): a reason or an answer as the applicant wrote it - its lines kept, a long word broken */
.zp-prewrap { white-space: pre-wrap; overflow-wrap: anywhere; }

/* 打卡 (_checkins.php, _checkin_rows.php; the owner's, 2026-10-13, as taishanzupu.com's): one line each, ruled off */
.zp-checkinlist.list-unstyled > li { padding: .45rem 0; border-bottom: 1px solid var(--bs-border-color); }
.zp-checkinlist.list-unstyled > li:last-child { border-bottom: 0; }
.zp-checkinmsg { margin: .15rem 0 0; overflow-wrap: anywhere; }
.zp-checkinpin { vertical-align: .1em; }
.zp-checkin-hidden { opacity: .65; }
/* the × after a line's time: light grey, darker under the pointer */
.zp-checkinx { display: inline; margin: 0; }
.zp-checkinx button { background: none; border: 0; padding: 0 .3rem; margin: 0; font-size: 1rem; line-height: 1;
  color: var(--bs-border-color); cursor: pointer; vertical-align: baseline; }
.zp-checkinx button:hover, .zp-checkinx button:focus-visible { color: var(--bs-secondary-color); }
/* the emoji above the 留言 box: characters in the reader's own emoji font */
.zp-checkinemoji { margin: .35rem 0 .25rem; }
.zp-checkinemoji button[data-e] { background: none; border: 0; color: inherit; padding: .1rem .15rem; margin: 0; min-width: 0;
  font: 1.3rem/1.2 "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif; cursor: pointer; border-radius: 6px; }
.zp-checkinemoji button[data-e]:hover, .zp-checkinemoji button[data-e]:focus-visible { background: var(--bs-tertiary-bg); }
.zp-checkinemoji .zp-emojimore { vertical-align: middle; }
.zp-emojiall { margin: .25rem 0 0; padding: .35rem .5rem; border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius); }
.zp-emojigroup > .small { display: inline-block; min-width: 4.5em; }
/* on a phone the lists under 选择： on a row of their own */
@media (max-width: 575.98px) { .zp-checkinpick { flex-basis: 100%; } }
/* the typed city, and the places it matches offered under it (assets/checkin_place.js) */
/* d-inline-flex is !important and comes after Bootstrap's own [hidden] rule, so it kept the 省/市/县 selects
   on screen for a foreign country; the same !important, with the more specific selector, wins back */
.zp-cnpick[hidden] { display: none !important; }
.zp-citytype { position: relative; max-width: 22rem; }
.zp-cityoffers { position: absolute; z-index: 1000; left: 0; right: 0; margin: 2px 0 0; padding: .25rem 0; max-height: 18rem; overflow-y: auto;
  background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: var(--bs-border-radius); box-shadow: var(--bs-box-shadow-sm); }
.zp-cityoffers > li { padding: .3rem .75rem; cursor: pointer; }
.zp-cityoffers > li:hover, .zp-cityoffers > li[aria-selected="true"] { background: var(--bs-tertiary-bg); }
/* a 历史族谱's 打卡: the link buttons over the 留言 box and what they find (assets/checkin_links.js) */
.zp-linkpanel { max-width: 32rem; }
.zp-linkhits { max-height: 14rem; overflow-y: auto; }
.zp-linkhits > li { padding: .1rem 0; }

/* ==== 姓氏 (app/SurnameWiki.php, sw_*.php; the owner's, 2026-10-20) ================================
   The list's chips; a page as an encyclopedia's, the box of what the site records of the surname beside it on
   a wide screen and above it on a narrow one - taishanzupu.com's 姓氏源流, in this site's colours. */
.zp-swchips { display: flex; flex-wrap: wrap; gap: .4rem; }
.zp-swchip { display: inline-flex; align-items: baseline; gap: .3rem; padding: .2rem .55rem; border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: var(--bs-border-radius); text-decoration: none; color: var(--bs-body-color); }
.zp-swchip:hover { background: var(--bs-primary-bg-subtle); }
.zp-swchar { font-family: var(--zp-serif); font-size: 1.15rem; }
.zp-swchip .zp-py { font-size: .8rem; color: var(--bs-secondary-color); }
.zp-swlocal { font-size: .7rem; color: var(--bs-primary-text-emphasis); background: var(--bs-primary-bg-subtle); border-radius: 1rem; padding: 0 .4rem; }
.zp-swletters a { display: inline-block; min-width: 1.6rem; text-align: center; }
.zp-wtitle { font-family: var(--zp-serif); }
.zp-wiki::after { content: ""; display: block; clear: both; }
.zp-infobox { float: right; width: 17rem; margin: 0 0 1rem 1.25rem; padding: .75rem .9rem; background: var(--bs-tertiary-bg);
  border: var(--bs-border-width) solid var(--bs-border-color); border-radius: var(--bs-border-radius); font-size: .92rem; }
.zp-ibname { text-align: center; margin: 0 0 .4rem; }
.zp-ibchar { font-family: var(--zp-serif); font-size: 2.6rem; line-height: 1.1; }
.zp-ibtrad { color: var(--bs-secondary-color); }
.zp-infobox table { margin: .25rem 0; }
.zp-infobox th, .zp-infobox td { padding: .25rem .3rem; vertical-align: top; }
.zp-infobox th { color: var(--bs-secondary-color); font-weight: normal; white-space: nowrap; }
.zp-ibhead { font-weight: 600; margin: .6rem 0 .2rem; }
.zp-iblist { margin: 0 0 .4rem; padding-left: 1.1rem; }
.zp-iblist ul { padding-left: 1rem; }
.zp-ibmore { margin-top: .6rem; }
.zp-wtoc { display: inline-block; min-width: 12rem; padding: .5rem .9rem; margin: 0 0 1rem; border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: var(--bs-border-radius); background: var(--bs-tertiary-bg); font-size: .92rem; }
.zp-wtoc .zp-ibhead { margin-top: 0; }
.zp-wtoc ol { margin: 0; padding-left: 1.2rem; }
.zp-wtoc ul { margin: 0; padding-left: 1.1rem; list-style-type: circle; }
.zp-wbody { line-height: 1.85; overflow-wrap: anywhere; }
.zp-wbody h2, .zp-wrefshead { font-family: var(--zp-serif); font-size: 1.35rem; border-bottom: var(--bs-border-width) solid var(--bs-border-color);
  padding-bottom: .2rem; margin: 1.5rem 0 .6rem; }
.zp-wbody h3 { font-family: var(--zp-serif); font-size: 1.12rem; margin: 1.1rem 0 .4rem; }
.zp-wbody sup.zp-fn { font-size: .72em; line-height: 0; }
.zp-wbody sup.zp-fn a { text-decoration: none; }
.zp-wbody blockquote { margin: .8rem 0; padding: .1rem 1rem; border-left: 3px solid var(--bs-border-color); color: var(--bs-secondary-color); }
.zp-wbody table { border-collapse: collapse; margin: .6rem 0; max-width: 100%; }
.zp-wbody th, .zp-wbody td { border: var(--bs-border-width) solid var(--bs-border-color); padding: .35em .6em; vertical-align: top; }
.zp-wsecedit { font-family: var(--bs-body-font-family); font-size: .8rem; font-weight: 400; margin-left: .6rem; color: var(--bs-secondary-color); }
.zp-wrefs { font-size: .9rem; padding-left: 1.6rem; overflow-wrap: anywhere; }
.zp-wiki :target { background: var(--bs-warning-bg-subtle); }
.zp-wfoot { clear: both; margin-top: 1.5rem; border-top: var(--bs-border-width) solid var(--bs-border-color); padding-top: .5rem; }
@media (max-width: 720px) { .zp-infobox { float: none; width: auto; margin: 0 0 1rem; } }
/* ...its editor, history and comparison */
.zp-wrich .richbox { min-height: 22rem; max-height: 70vh; overflow-y: auto; line-height: 1.85; }
.zp-wrich .richbox h2 { font-family: var(--zp-serif); font-size: 1.3rem; border-bottom: var(--bs-border-width) solid var(--bs-border-color); margin: 1rem 0 .5rem; }
.zp-wrich .richbox h3 { font-family: var(--zp-serif); font-size: 1.1rem; margin: .8rem 0 .4rem; }
.zp-wrich .richbox blockquote { margin: .6rem 0; padding: .1rem 1rem; border-left: 3px solid var(--bs-border-color); color: var(--bs-secondary-color); }
.zp-wrich .richbox a { text-decoration: underline; }
.zp-wrefrow { display: grid; grid-template-columns: 2.2rem 2fr 2fr 1fr 6rem; gap: .3rem; align-items: center; margin-bottom: .3rem; }
@media (max-width: 720px) { .zp-wrefrow { grid-template-columns: 2.2rem 1fr; } .zp-wrefrow input { grid-column: 2; } }
ul.wdiff { list-style: none; padding: 0; margin: 0; }
ul.wdiff li { margin: .3rem 0; padding: .35rem .6rem; border-radius: var(--bs-border-radius); white-space: pre-wrap; overflow-wrap: anywhere; }
ul.wdiff .d-same { color: var(--bs-secondary-color); }
ul.wdiff .d-del { background: var(--bs-danger-bg-subtle); } ul.wdiff .d-ins { background: var(--bs-success-bg-subtle); }
ul.wdiff .d-chg { background: var(--bs-tertiary-bg); }
ul.wdiff del { background: var(--bs-danger-border-subtle); text-decoration: line-through; }
ul.wdiff ins { background: var(--bs-success-border-subtle); text-decoration: none; }
ul.wdiff .d-fold { color: var(--bs-secondary-color); background: none; }

/* 访问记录 (admin_request_log.php): a line a page answered. Its own scroller on a phone; the page and the
   browser may break anywhere, the rest stays on one line. */
table.zp-reqlog { font-size: .85rem; }
table.zp-reqlog td { white-space: nowrap; vertical-align: top; }
table.zp-reqlog td.zp-reqlog-page { white-space: normal; word-break: break-all; min-width: 14rem; }
.zp-reqlog-days { line-height: 1.9; }
