/* FamilyTree.
 *
 * A reading tool for documents, laid out as rows on a labelled rail rather than
 * as nested cards. Three rules hold it together:
 *
 *   1. The accent means ONE thing: where you are. Never a button, never an
 *      "add" affordance. It used to mean all three, so it meant nothing.
 *   2. Names carry the type scale. They are the content of a genealogy app and
 *      were previously sized like interface chrome.
 *   3. Grouping comes from space and hairlines. A border is for a surface that
 *      is genuinely raised, not for every element on the page.
 *
 * IBM Plex Sans is self-hosted rather than fetched from Google, because an app
 * that hides living people's details should not announce every page view to a
 * third party. Dates use tabular numerals so they align down a column without
 * needing a monospaced face.
 */

@font-face {
  font-family: 'IBM Plex Sans';
  src: url('fonts/plex-sans.woff2') format('woff2');
  font-weight: 100 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --ink:        #26241f;
  --ink-2:      #4a473f;
  /* Both of these carried real content -- a card's dates, places and occupation
     are data, not decoration -- and both failed WCAG AA on the grounds they sit
     on: #7d7a70 was 4.29:1 on white and #a5a196 only 2.58:1. Darkened until they
     clear 4.5:1.

     They are now nearly the same colour, and that is the honest outcome: AA at
     11px leaves no room between "readable secondary text" and "a lighter shade
     of readable secondary text". The hierarchy is carried by size and weight
     instead. --faint stays as a token because it marks intent at hundreds of
     call sites, not because the eye can tell it apart. */
  --muted:      #716e64;
  --faint:      #757166;
  --line:       #e5e1d7;
  --line-2:     #d3cec1;
  --bg:         #f8f6f1;
  --surface:    #ffffff;
  /* THE CHART'S OWN LINE, and its own second surface. Separate tokens because
     the chart asks something of them the rest of the app does not: a card is a
     box on a bare canvas with no heading, no label and nothing else around it
     to say where it begins, so its border is the only thing identifying it --
     which is what WCAG 1.4.11 means by a UI component boundary, and what it
     wants 3:1 for.

     --line-2 gave 1.45:1 on the canvas. A card with a white fill got away with
     it, because the fill was a second signal; a spouse card, which has no fill,
     did not -- its background was #f8f6f1 on #f8f6f1, which is 1.00:1, the same
     colour. It was invisible, and that is what this fixes.

     --card-line is 3.06:1 on the canvas and 3.30:1 on the white fill, so it
     holds on both sides of itself. --card-alt is the fill for a card that is
     NOT descended from the line above -- a spouse married in. It is only 1.11:1
     against the canvas, which is all a light warm palette has room for, and it
     does not need to be more: the border says where the box is, and the fill
     only has to say which KIND of box it is once you can see it.

     Deliberately a SLIGHT tint rather than a strong one. The canvas is nearly
     white, so a card with a grey fill reads as heavier than a card with a white
     one -- and the heavier cards would be the partners married in rather than
     the line the chart is drawn to follow. Weight follows descent. */
  --card-line:  #948d7b;
  --card-alt:   #f2efe7;
  --accent:     #b8791b;
  --accent-soft:#fbf1de;
  --accent-ink: #6b430c;
  --bad:        #a33a2a;
  --ok:         #2f7a4f;
  --radius:     6px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink:        #ece8df;
    --ink-2:      #c6c1b5;
    --muted:      #969186;
    --faint:      #6f6a60;
    --line:       #35332e;
    --line-2:     #46433c;
    --bg:         #1a1917;
    --surface:    #222120;
    /* 3.42:1 on the canvas and 3.13:1 on the card, measured the same way. */
    --card-line:  #726d63;
    --card-alt:   #2b2a27;
    --accent:     #d99a3c;
    --accent-soft:#332818;
    --accent-ink: #e8b96e;
    --bad:        #d97a68;
    --ok:         #63b487;
  }
}

* { box-sizing: border-box; }

body {
  --pagepad: 20px;
  margin: 0;
  /* The header sits at this padding. Pages WITHOUT a header take their extra
     air from the .wrap rule below, so the two are not coupled. */
  padding: 20px 20px 80px;
  background: var(--bg);
  color: var(--ink);
  font: 14px/1.55 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

.wrap { max-width: 620px; margin: 0 auto; }
body > .wrap:first-child { padding-top: 28px; }

/* Anything that states a date, so figures line up down a column. */
.chip-life, .cnode-lifeline, .focus .life, .keys .meta, .trees .meta,
.share-who .meta, .union-meta, .checks .detail { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------- type -- */

h1 {
  font-size: 25px; font-weight: 600; letter-spacing: -0.012em;
  line-height: 1.2; margin: 0 0 4px;
}
.subtitle { color: var(--muted); font-size: 13px; margin: 0 0 26px; }
.prose { font-size: 13.5px; line-height: 1.6; max-width: 62ch; }
.hint { font-weight: 400; color: var(--muted); font-size: 12px; line-height: 1.5; }
.crumb {
  color: var(--muted); font-size: 12px; margin: 0 0 16px;
  overflow-wrap: anywhere; line-height: 1.7;
}
.foot { margin-top: 30px; color: var(--muted); font-size: 12px; }
.mid { margin: 18px 0; }

a { color: var(--accent-ink); }
a:hover { color: var(--ink); }

/* A section label on the left rail. */
h2.section, .rel h2, .rowlabel {
  font-size: 10px; font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--faint); margin: 0;
}

/* ----------------------------------------------------------------- chrome -- */

/* nowrap: with a long tree name beside the brand, wrapping put the avatar on a
   second line and gave back the row this arrangement exists to save. The left
   group shrinks and clips the name instead. */
.topbar {
  display: flex; flex-wrap: nowrap; align-items: center; gap: 12px;
  max-width: 620px; margin: 0 auto 26px; padding-bottom: 10px;
  border-bottom: 1px solid var(--ink);
}
.topbar .brand {
  display: inline-flex; align-items: center; gap: 8px;
  font-weight: 600; font-size: 14px; color: var(--ink);
  text-decoration: none; letter-spacing: -0.01em;
}
.topbar .brand svg { display: block; flex: none; }
/* Where you are, in the bar rather than in a row of its own. The group shrinks;
   the name inside it clips. */
.topbar-left { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1 1 auto; }
.topbar-left .brand { flex: none; }
.topbar-where { color: var(--muted); font-size: 13px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar-where .sep { color: var(--line-2); margin-right: 7px; }
body.home .topbar-where { font-size: 15px; }
.brandline { display: flex; align-items: center; gap: 11px; }
.inline { display: inline; margin: 0; }

/* On the tree list the masthead is the point: there is no data competing for
   the room, and the name of the thing you are in should be the largest thing on
   the page. "Your trees" reads as the label it is, underneath. */
body.home .topbar { padding-bottom: 14px; margin-bottom: 22px; align-items: center; }
body.home .topbar .brand { font-size: 27px; gap: 13px; letter-spacing: -0.022em; }
body.home .topbar .avatar { width: 34px; height: 34px; font-size: 12.5px; }
/* The page title steps down to the app's own section-label idiom, so it sits as
   a peer of "Shared with you" below it rather than competing with the masthead
   above or the tree names beside it. */
body.home h1 {
  font-size: 10px; font-weight: 600; letter-spacing: .09em;
  text-transform: uppercase; color: var(--faint); margin: 0 0 10px;
}
body.home h2.section { margin-top: 26px; }

/* ---------------------------------------------------- a card, to look at -- */

.cardview-top { display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-start; }
.cardview-photo {
  display: block; width: 190px; max-width: 100%; height: auto; flex: none;
  border-radius: var(--radius); border: 1px solid var(--line-2);
}
.cardview-facts { flex: 1 1 220px; min-width: 0; }
.cardview .fly-title { font-size: 22px; margin: 0 0 6px; }
.cardview-life { font-size: 15px; color: var(--ink-2); margin: 0 0 8px; }
.cardview-meta { color: var(--muted); font-size: 13px; margin: 0 0 3px; }
.cardview .note {
  font-size: 12.5px; color: var(--muted); line-height: 1.55;
  margin: 14px 0 0; padding-left: 12px; border-left: 1px solid var(--line);
}
.cardview .hint { margin-top: 12px; }
.cardview-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 22px 0 0; }
.cardview-acts form { margin: 0; }

/* ------------------------------------------------------- the account menu -- */

/* A <details> rather than a scripted popover: it opens with the keyboard and
   without JavaScript, and the script only adds click-away and Escape. */
.usermenu { margin-left: auto; position: relative; flex: none; }
.usermenu > summary {
  list-style: none; cursor: pointer; display: block;
  border-radius: 50%; -webkit-tap-highlight-color: transparent;
}
.usermenu > summary::-webkit-details-marker { display: none; }
.usermenu > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.avatar {
  display: grid; place-items: center; overflow: hidden;
  width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid var(--line-2); background: var(--accent-soft);
  color: var(--accent-ink); font-size: 11.5px; font-weight: 600;
  letter-spacing: 0.02em; text-transform: uppercase; user-select: none;
}
.avatar[data-photo] { background: var(--line); border-color: var(--line-2); }
.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.usermenu > summary:hover .avatar { border-color: var(--muted); }

.usermenu .menu {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 40;
  min-width: 190px; padding: 6px;
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--radius); box-shadow: 0 6px 20px rgba(38, 36, 31, 0.13);
}
.usermenu .menu-who {
  display: flex; flex-direction: column; gap: 1px;
  margin: 0; padding: 7px 9px 9px; border-bottom: 1px solid var(--line);
}
.usermenu .menu-who .name { font-size: 12.5px; font-weight: 600; color: var(--ink); }
.usermenu .menu-who .meta { font-size: 11px; color: var(--faint); }
.usermenu .menu form { margin: 0; }
.usermenu .menu a,
.usermenu .menu button {
  display: block; width: 100%; box-sizing: border-box; text-align: left;
  padding: 8px 9px; border: 0; border-radius: 4px;
  background: none; font: inherit; font-size: 12.5px; color: var(--ink-2);
  text-decoration: none; cursor: pointer;
}
.usermenu .menu a:hover,
.usermenu .menu button:hover { background: var(--bg); color: var(--ink); }

/* The same circle, larger, next to the file field on the account page. */
.avatar-lg { width: 56px; height: 56px; font-size: 18px; }
.avatar-edit { display: flex; align-items: center; gap: 14px; margin-top: 4px; }
.avatar-edit-fields { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.avatar-edit-fields input[type="file"] { font-size: 12px; }
.linkbtn {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 12px; text-decoration: underline;
}
.linkbtn:hover { color: var(--ink); }

/* --------------------------------------------------------------- controls -- */

.btn {
  display: inline-flex; align-items: center; gap: 7px;
  font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer;
  border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 8px 13px; background: var(--surface); color: var(--ink);
  text-decoration: none; white-space: nowrap;
}
.btn:hover { border-color: var(--muted); color: var(--ink); }
/* Ink, not accent: the accent is reserved for marking position. */
.btn.primary { background: var(--ink); border-color: var(--ink); color: var(--bg); }
.btn.primary:hover { background: var(--ink-2); border-color: var(--ink-2); }
.btn.danger { color: var(--bad); }
.btn.danger:hover { border-color: var(--bad); }

.card-form {
  display: flex; flex-direction: column; gap: 14px;
  padding: 0; background: none; border: 0;
}

/* THE CARD FORM, IN TWO COLUMNS. A modifier rather than a change to .card-form:
   that class is on eighteen forms, most of which are four fields and a button
   and read better as one column.

   The pairing is the point, not the column count. A date and the place it
   happened are one fact asked in two boxes, so they sit side by side and the
   eye reads a row rather than a list -- born and where, christened and where,
   died and where, three rows instead of six items. Given names and surname pair
   the same way.

   Anything that is a paragraph rather than a field spans both: notes, the
   photograph, the documents, and every hint that explains the row above it.
   A hint stretched across one narrow column would set the row height for a
   sentence nobody reads twice.

   One column below 620px, where two of anything is one too many. */
/* The GRID IS AN INNER DIV, not the fieldset. A <legend> is a grid item like
   anything else, so making the fieldset itself the grid put the heading in the
   first cell and pushed every row of the group one place along -- which is what
   the first screenshot showed. Floating it does not help either; it is still in
   the box. So the fieldset stays an ordinary block, the legend sits above it in
   normal flow, and the fields are gridded one level down. */
.card-form.two-col > fieldset {
  border: 0; padding: 0; margin: 0; min-width: 0;
}
.card-form.two-col > fieldset > legend {
  padding: 0; margin: 0 0 8px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted);
}
.card-form.two-col .fields {
  display: grid; grid-template-columns: 1fr 1fr; gap: 14px; min-width: 0;
}
.card-form.two-col .span-2,
.card-form.two-col .fields > .hint { grid-column: 1 / -1; }
/* A hint explains the row above it, so it sits tight under it rather than a
   full gap away, which would read as belonging to the row below. */
.card-form.two-col .fields > .hint { margin-top: -8px; }
.card-form.two-col label { min-width: 0; }
@media (max-width: 620px) {
  .card-form.two-col .fields { grid-template-columns: 1fr; }
}
.card-form label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; font-weight: 500; color: var(--ink-2); }
.card-form input, .card-form select, .card-form textarea {
  font: inherit; font-size: 14px; font-weight: 400; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 9px 11px; width: 100%;
}
.card-form input:focus, .card-form select:focus, .card-form textarea:focus {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent;
}
.card-form input[type=file] { font-size: 12.5px; border: 0; padding-left: 0; background: none; }
.card-form button { align-self: flex-start; }
.card-form code {
  background: var(--accent-soft); color: var(--accent-ink);
  padding: 0 4px; border-radius: 3px; font-size: 11.5px;
}
/* `.card-form label` is (0,1,1) and beats a bare `.inline-check` at (0,1,0), so
   a checkbox label nested inside a form label inherited `column` and stacked its
   own box above its own text. Stated at matching specificity instead. */
.inline-check,
.card-form label.inline-check {
  display: inline-flex; flex-direction: row; gap: 7px;
  align-items: center; font-weight: 400;
}
.inline-check input { width: auto; }

/* [hidden] is a UA-stylesheet rule, and any author `display` beats it -- so a
   label set to flex stays visible when script hides it. Stated here at author
   level, with !important, because "hidden" has exactly one meaning and nothing
   in this file should be allowed to argue with it. fields.php hides a type's
   own settings this way. */
[hidden] { display: none !important; }

/* --------------------------------------------------------------- messages -- */

.flash, .error {
  border-radius: var(--radius); padding: 10px 12px; margin: 0 0 18px; font-size: 13px;
  border: 1px solid var(--line-2); background: var(--surface);
}
.flash { border-left: 3px solid var(--accent); }
.error { border-left: 3px solid var(--bad); color: var(--bad); }

.empty {
  border: 1px dashed var(--line-2); border-radius: var(--radius);
  padding: 22px 16px; color: var(--muted); font-size: 13px;
}
.empty p { margin: 0 0 4px; color: var(--ink-2); }

/* ------------------------------------------------------------ list of rows -- */

/* Trees, passkeys, shares and accounts are all the same shape: a row with a
   name, a quiet detail line, and a hairline underneath. */
.trees, .keys, .shares { list-style: none; margin: 0 0 8px; padding: 0; }

/* The row is the flex container now, not the link: the link takes what it needs
   and the actions sit beside it, on the row's own rule. */
.trees li {
  display: flex; align-items: center; gap: 10px;
  border-top: 1px solid var(--line);
}
.trees li:last-child { border-bottom: 1px solid var(--line); }
.tree-open {
  display: flex; align-items: baseline; gap: 14px; flex: 1; min-width: 0;
  padding: 14px 0; text-decoration: none; color: inherit;
}
.trees li:hover .name { color: var(--accent-ink); }
.trees .name { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.trees .meta { margin-left: auto; color: var(--muted); font-size: 12.5px; padding-left: 10px; }

.tree-acts { display: flex; align-items: center; gap: 2px; flex: none; }
.tree-acts form { margin: 0; }

/* Quiet until the row is under the pointer: four icons on every row would
   otherwise compete with the tree names, which are the actual content. On a
   touch screen there is no hover, so they simply stay visible. */
.tree-acts .iconbtn, .tree-acts .tree-edit { opacity: 0.35; transition: opacity 0.12s; }
.trees li:hover .iconbtn, .trees li:hover .tree-edit,
.trees li:focus-within .iconbtn, .trees li:focus-within .tree-edit { opacity: 1; }
@media (hover: none) {
  .tree-acts .iconbtn, .tree-acts .tree-edit { opacity: 1; }
}

.iconbtn {
  display: grid; place-items: center; width: 30px; height: 30px;
  padding: 0; border: 0; border-radius: var(--radius); cursor: pointer;
  background: none; color: var(--muted); text-decoration: none;
  list-style: none;
}
.iconbtn::-webkit-details-marker { display: none; }
.iconbtn:hover { background: var(--bg); color: var(--ink); }
.iconbtn.danger:hover { background: #fbeeeb; color: var(--bad); }
.iconbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Renaming opens over the row rather than pushing it around. */
.tree-edit { position: relative; }
.tree-edit-form {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
  display: flex; gap: 6px; margin: 0; padding: 8px;
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--radius); box-shadow: 0 6px 20px rgba(38, 36, 31, 0.13);
}
.tree-edit-form input {
  font: inherit; font-size: 14px; color: var(--ink); width: 210px; max-width: 44vw;
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 7px 9px;
}
.tree-edit-form input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }

.keys li, .shares li { padding: 14px 0; border-top: 1px solid var(--line); }
.keys li:last-child, .shares li:last-child { border-bottom: 1px solid var(--line); }
.keys .rename { display: flex; gap: 8px; margin: 0; }
.keys .rename input {
  flex: 1; min-width: 0; font: inherit; font-size: 14px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 8px 10px;
}
.keys .meta { color: var(--muted); font-size: 12.5px; margin: 8px 0; }
.keys form { margin: 0; }

/* -------------------------------------------------------- the focus view -- */

/* Label rail down the left, content on the right. */
/* The gutter is sized for the longest label in ANY language, not the language
   it was designed in: 88px fitted "PARTNERSHIPS" and left "PARTNERSCHAFTEN"
   (118px) to run out under the content, with "GESCHWISTER" 2px from the same
   fate. One width for every section keeps the labels on one line down the page,
   which is the whole point of putting them in a gutter. */
.rel {
  display: grid; grid-template-columns: 122px minmax(0, 1fr);
  column-gap: 10px;
  border-top: 1px solid var(--line); padding-top: 12px; margin: 0 0 18px;
}
.rel > h2 { grid-column: 1; grid-row: 1; padding-top: 10px; }
.rel > *:not(h2) { grid-column: 2; }
.rel > .hint { padding: 9px 0 4px; }

.chips { display: flex; flex-direction: column; }
.chip {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 0; border-bottom: 1px solid var(--line);
  text-decoration: none; color: inherit;
}
.chips > .chip:last-child { border-bottom: 0; }
.chip:hover .chip-name { color: var(--accent-ink); }
.chip-photo {
  width: 28px; height: 28px; border-radius: 50%; object-fit: cover;
  flex: none; border: 1px solid var(--line-2);
}
.chip-text {
  display: flex; flex-direction: row; align-items: baseline;
  justify-content: space-between; gap: 14px; flex: 1; min-width: 0;
}
.chip-name { font-size: 14.5px; font-weight: 500; }
.chip-life { font-size: 12.5px; color: var(--muted); white-space: nowrap; }
/* Only ever says what is NOT the default: 'biological' prints as nothing, since
   the reason the column exists is to stop asserting that about everybody. It
   sits INSIDE .chip-name so .chip-text stays a two-item space-between row. */
.chip-rel {
  margin-left: 8px; font-size: 11px; font-weight: 500; color: var(--muted);
  background: var(--bg); border: 1px solid var(--line-2); border-radius: 999px;
  padding: 1px 8px; white-space: nowrap; vertical-align: 1px;
}

.relnote { font-size: 12.5px; color: var(--ink-2); margin: 11px 0 0; }
/* Folded away: 'biological' is right nearly every time, and a select on every
   card with parents would be permanent clutter for a rare correction. */
.reledit { margin-top: 9px; }
.reledit > summary { font-size: 12px; color: var(--muted); cursor: pointer; list-style: none; }
.reledit > summary::-webkit-details-marker { display: none; }
.reledit > summary:hover { color: var(--accent-ink); }
.relform { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 10px; }
.relform select {
  font: inherit; font-size: 14px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 8px 10px; flex: 1 1 160px; min-width: 0;
}
.relform select:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
.relform .btn { padding: 8px 13px; flex: none; }

/* Adding is a quiet action, not the loudest thing on the page. */
.chip.add {
  border-bottom: 0; padding: 10px 0 2px; align-self: flex-start;
  font-size: 12.5px; font-weight: 500; color: var(--muted);
}
.chip.add:hover { color: var(--accent-ink); }
.chip.ghost { color: var(--faint); font-size: 13px; border-bottom: 0; }

.focus {
  display: flow-root; background: var(--surface);
  border: 1px solid var(--line); border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 20px 22px; margin: 0 0 22px;
}
.focus h1 { font-size: 26px; margin: 0 0 5px; }
.focus .life { font-size: 15px; color: var(--ink-2); margin: 0 0 10px; }
.focus .where, .focus .job { color: var(--muted); font-size: 13px; margin: 0 0 2px; }
.focus .note {
  font-size: 12.5px; color: var(--muted); line-height: 1.55;
  margin: 14px 0 0; padding-left: 12px; border-left: 1px solid var(--line);
  max-width: 58ch;
}
.focus .acts { display: flex; flex-wrap: wrap; gap: 8px 10px; margin: 18px 0 0; align-items: center; }
.focus .acts form { margin: 0; }
/* The badge's margin is for sitting in running text; in this row the gap does
   that job, and the two together read as a stray indent. */
.focus .acts .tag { margin-left: 0; vertical-align: baseline; }
.acts-pair { display: inline-flex; align-items: center; gap: 8px; }
.acts-pair form { margin: 0; }
/* A form holding more than a lone button is a row of its own inside the row. */
.focus .acts form { display: inline-flex; align-items: center; gap: 10px; }
.focus-photo {
  float: right; width: 76px; height: 76px; border-radius: var(--radius);
  object-fit: cover; margin: 0 0 12px 16px; border: 1px solid var(--line-2);
}

/* Partnerships: a dated heading, then labelled rows. */
.union { border: 0; background: none; padding: 0; margin: 0 0 20px; }
.union-head { display: flex; align-items: baseline; gap: 9px; padding-bottom: 8px; }
.union-no { font-weight: 600; font-size: 12px; color: var(--faint); }
.union-meta { color: var(--ink-2); font-size: 12.5px; }
.union-edit { margin-left: auto; font-size: 12px; }
.union-row {
  display: grid; grid-template-columns: 78px minmax(0, 1fr);
  align-items: start; border-top: 1px solid var(--line);
}
.rowlabel { padding-top: 18px; }

.danger-zone { margin-top: 34px; padding-top: 14px; border-top: 1px solid var(--line); }
.danger-zone summary { color: var(--muted); font-size: 12.5px; cursor: pointer; }
.danger-zone > * { margin-top: 14px; }

/* --------------------------------------------------------------- passkeys -- */

.wa-box { margin-top: 18px; }
.wa-box .btn { width: 100%; justify-content: center; }
.wa-or {
  display: flex; align-items: center; gap: 10px;
  color: var(--faint); font-size: 11px; margin: 0 0 14px;
}
.wa-or::before, .wa-or::after { content: ''; flex: 1; height: 1px; background: var(--line); }
.photo-preview {
  width: 88px; height: 88px; border-radius: var(--radius); object-fit: cover;
  border: 1px solid var(--line-2); margin-bottom: 6px;
}

/* ---------------------------------------------------------------- shares -- */

h2.section { margin: 30px 0 6px; }
.share-who .name { font-weight: 600; font-size: 15px; display: block; }
.share-who .meta { color: var(--muted); font-size: 12.5px; }
.share-perm { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 12px; }
.share-perm select, .share-perm input[type=text] {
  font: inherit; font-size: 12.5px; color: var(--ink); background: var(--surface);
  border: 1px solid var(--line-2); border-radius: var(--radius); padding: 7px 9px;
}
.share-perm input[type=text] { min-width: 220px; }
.share-perm .btn { font-size: 12px; padding: 7px 11px; }

.admin-users li.suspended { opacity: .6; }
.tag {
  display: inline-block; font-size: 9.5px; font-weight: 600; letter-spacing: .07em;
  text-transform: uppercase; padding: 2px 7px; border-radius: 999px; vertical-align: 2px;
  background: var(--accent-soft); color: var(--accent-ink); border: 1px solid var(--accent);
  margin-left: 7px;
}
.tag.you { background: none; color: var(--faint); border-color: var(--line-2); }
.tag.off { background: none; color: var(--bad); border-color: var(--bad); }
/* Nested inside an account row, so it must not draw its own rule — that made
   it read as a separate account in the list. */
.admin-users .danger-zone { margin-top: 12px; padding-top: 0; border-top: 0; }

/* ------------------------------------------------------------ setup page -- */

body.setup .wrap { max-width: 620px; }
.checks { list-style: none; margin: 0; padding: 0; }
.checks li {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 11px 0; border-top: 1px solid var(--line);
}
.checks li:last-child { border-bottom: 1px solid var(--line); }
.checks .mark { font-size: 14px; line-height: 1.5; flex: none; width: 16px; text-align: center; }
.checks li.ok .mark { color: var(--ok); }
.checks li.bad .mark { color: var(--bad); }
.checks li.warn .mark { color: var(--accent); }
.configdump {
  font-size: 11.5px; line-height: 1.5; overflow: auto; max-height: 340px;
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--radius); padding: 12px 14px; margin: 0 0 18px;
}
.checks .body { display: flex; flex-direction: column; min-width: 0; }
.checks .name { font-weight: 500; }
.checks .detail { color: var(--muted); font-size: 12.5px; word-break: break-word; }

/* --------------------------------------------------------------- the chart -- */

body.wide { padding-bottom: 24px; }
body.wide .wrap, body.wide .topbar { max-width: min(1800px, 98vw); }

.chart-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 12px; }
.chart-tools .btn { padding: 7px 12px; }
.chart-tools a.btn { margin-left: auto; }

/* The chart takes whatever is left, measured by the layout rather than guessed
   at. The old calc(100vh - 200px) was a constant standing in for the height of
   everything above it, and it was already wrong when the toolbar wrapped to two
   rows on a phone -- the chart then ran 56px past the bottom of the window. A
   column flex on the body means the header can be any height it likes. */
/* No frame. The chart IS the canvas -- a border and a rounded corner around it
   spend real estate on saying where it stops, which on a phone is the last thing
   worth paying for. It bleeds past the page inset to the window edges, and the
   page ground shows through so the white cards read as objects on it rather
   than as white on white. */
.chart-scroll {
  overflow: auto; border: 0; border-radius: 0;
  background: none; padding: 14px 4px 0;
  height: calc(100vh - 200px); min-height: 340px;
  touch-action: pan-x pan-y;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  cursor: grab;
}
.chart-scroll.dragging { cursor: grabbing; user-select: none; }

body.chartpage {
  display: flex; flex-direction: column;
  height: 100vh; height: 100dvh;      /* dvh so mobile browser chrome is counted */
  padding-bottom: 0; box-sizing: border-box;
}
/* As flex ITEMS these two size to their content, and their `margin: 0 auto`
   then centres them at that width instead of stretching -- which left the
   masthead floating in the middle of the page. */
body.chartpage .topbar, body.chartpage .wrap { width: 100%; box-sizing: border-box; }
body.chartpage .wrap { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
/* The canvas cancels the page inset below, which reaches the window edge only
   while .wrap is as wide as the page. body.wide caps it at min(1800px, 98vw),
   so past about 1840px of window the cap -- not the inset -- was setting where
   the chart stopped, and the gutter grew with the screen: 40px at 1920, 800px
   at 3440. Uncapped here, so the inset is the only thing between the canvas and
   the edge, whatever the window.

   The cap moves to the toolbar, which is the only other thing in the wrap and
   the one that wants it: it is a row of controls with the export link pushed
   right by margin-left:auto, and on an ultrawide those two ends would sit a
   yard apart. Same expression as .topbar's, so the two line up. */
body.chartpage .wrap { max-width: none; }
body.chartpage .chart-tools {
  max-width: min(1800px, 98vw); width: 100%;
  margin-left: auto; margin-right: auto;
}
/* min-height:0 on both, or a flex item refuses to shrink below its content. */
body.chartpage .chart-scroll {
  flex: 1 1 auto; height: auto; min-height: 200px;
  /* Cancel the page inset on both sides and at the foot, so the canvas runs to
     the window edge. The inset is a variable precisely so this cannot drift
     from it when the narrow breakpoint changes it. */
  margin-left: calc(-1 * var(--pagepad));
  margin-right: calc(-1 * var(--pagepad));
  width: auto;
}
.chart-scroll a, .chart-scroll button { cursor: pointer; }
/* position, so the connector layer can be absolutely placed against it and
   share its coordinate space -- the same space drawDescent() measures in. */
.chart-zoom { display: inline-block; min-width: 100%; position: relative; }

.chart, .chart ul, .chart li { list-style: none; margin: 0; padding: 0; position: relative; }
.chart { display: flex; justify-content: safe center; min-width: max-content; }
.chart li {
  display: flex; flex-direction: column; align-items: center;
  text-align: center; padding: 22px 8px 0; position: relative;
}
.chart ul { display: flex; padding-top: 22px; position: relative; }
/* The root descends from nothing, so it takes no drop above it. Every other
   <li> takes the same one whether it has siblings or not -- an only child used
   to be exempted, and sat exactly one drop above its cousins. */
.chart > li { padding-top: 0; }

/* The label reserves a fixed band on EVERY branch row of a chart that has one,
   and none at all in a chart that has none. It is drawn out of flow: in flow
   its own line box WAS the space it took, so a parent with two marriages pushed
   their children a band below the cousins beside them.

   This band is also what the staggered rails are spent out of. Each marriage
   descends on its own horizontal so that two of them cannot merge into one bus
   -- see drawDescent() -- and the step is measured from the room between the
   two rail clearances, which is this. So a chart with several marriages costs
   no more height than one with none: the space was already reserved.

   Nothing here has to agree with where a line is drawn any more. The connectors
   are measured from the cards after layout, so this decides the spacing and they
   follow it. */
.chart.slot-gtag ul.groups > li.group { padding-top: 44px; }

/* THE CONNECTORS ARE NOT DRAWN HERE ANY MORE.
   They were border-top and border-left on ::before/::after across four element
   types, positioned by custom properties measured in script, with separate cases
   for a first child, a last child, an only child and the branch level. A line's
   continuity was emergent -- it held only while every padding, pseudo-element
   height and injected offset agreed -- so changing a padding detached a line
   somewhere else. They are one SVG path per parent-to-child route now; see
   drawDescent() in chart.php. What is left here is the SPACE they are drawn in,
   which is all this file ever needed to own. */
.chart-lines {
  position: absolute; top: 0; left: 0; pointer-events: none; overflow: visible;
}
/* The same line as the cards. A 3:1 border with 1.45:1 connectors between them
   reads as two different drawings on one page; they are one drawing. */
.chart-lines path { fill: none; stroke: var(--card-line); stroke-width: 1; }
.couple { display: flex; align-items: center; justify-content: center; }

/* A child who did not descend from the couple above is drawn dashed. The
   chart's lines mean "descended from", so where that is not what happened the
   line has to say so rather than quietly claim otherwise.

   Only that child's OWN drop, never the rail above it: the rail is shared by
   everyone in the row, and dashing it would say "none of these children are
   biological". This used to take three rules, because the drop was drawn from a
   different edge for a middle child, a last child and an only child. It is one
   stroke-dasharray on one path now -- see drawDescent() in chart.php. */

/* ONE SIZE FOR EVERY CARD. A generation reads as a row when the eye is not
   also working out why one box is bigger than the one beside it -- so a card
   carrying nothing but a name is deliberately larger than it needs to be, and
   the saving is in the reading, not in the pixels.

   Fixed width, and every line below is a fixed slot the card reserves whether
   or not this person fills it. --cardw is the one number to change.

   The card is a row: the photograph on the left, the lines beside it in a
   column of their own, all ragged left. A chart is read by running the eye down
   a generation looking for a name, and centred text gives it no edge to run
   down -- every name starts somewhere else. Left-aligned, they all start in the
   same place.

   The picture column is always there and always filled -- a photograph where
   there is one, the person's initials where there is not. It is what gives the
   row something to start with: a name on its own, in a card with no dates under
   it, sits in a lot of white and reads as unfinished rather than as brief. So
   this column is NOT one of the droppable slots below; only the lines are.
   240px = 138px of text + 72px of picture + the 8px between them + the 20px of
   padding and the 2px of border. It is written that way -- as the sum -- rather
   than as the number, because the picture is no longer a constant: it is as
   tall as the content box and 3:4 of that wide, so a chart that reserves a
   birth-name line AND all four life lines has a taller box, a wider picture and
   a card 11px wider to hold it. The TEXT budget is the fixed thing, and that is
   the thing worth fixing: it is what decides whether a name is clipped.

   138px of text is where measuring put it. Every name and every life line in the 153-person import tree was measured
   with a Range -- the text's own width, not the box it is clipped into -- and
   the budget bought:

     94px   2 names clipped, and only the p50 life line fits
     118px  1 name,  p90 of the life lines
     138px  1 name,  p99 of the life lines      <- here
     154px  0 names, every life line

   The one name still clipped at 138px is a double-barrelled surname of 145px;
   the one line still clipped is 170px. Chasing the last of them costs another
   16px on every card in every generation, which is the trade this number is:
   a generation is laid out ACROSS the screen, so every pixel here is multiplied
   by the number of cousins in the widest row. 240px is about 4700px of extra
   scroll on that tree against 196px. Height stays cheaper than width, which is
   why the life lines went DOWN the card rather than along it.

   Everything still clipped at 138px is one click from being read in full, and
   the full name is in the card's title attribute meanwhile.

   The padding is 10px on every side, not 9px 10px: the picture is the tallest
   thing in the card, so its inset IS the padding, and an inset that is 10
   across and 9 down would show as a picture sitting a pixel proud of centre
   against a border it nearly touches. */
/* The one arithmetic in the file, so that no two rules have to agree about it:
   the picture is at least 96px tall and never shorter than the text beside it,
   3:4 of its own height wide, and the card is the text budget plus that picture
   plus the gaps. Every consequence follows -- the picture is always the tallest
   thing in the card, so its distance to the border IS the padding on all three
   sides, at every combination of slots, without a single special case. */
.chart {
  --pich:  max(96px, calc(34px + var(--birthline, 0px) + var(--lifelines) * 15px));
  --picw:  calc(var(--pich) * 0.75);
  --cardw: calc(138px + var(--picw) + 30px);   /* 30 = 8 gap + 20 padding + 2 border */
  --texth: calc(34px + var(--birthline, 0px) + (var(--lifelines) + var(--fieldlines)) * 15px);
}
.cnode {
  position: relative; display: flex; flex-direction: column; align-items: center;
  border: 1px solid var(--card-line); border-radius: var(--radius); background: var(--surface);
  padding: 10px; width: var(--cardw); box-sizing: border-box; color: inherit;
}
/* flex-start, not center. While the picture is the taller of the two the
   difference is nil -- the text column is exactly its own height either way --
   but a tree that shows several of its own fields on the card pushes the text
   past the picture, and centring would leave the picture floating in the middle
   of a box it no longer sets the height of. Pinned to the top it keeps its 10px
   to the card on two sides out of three, and the extra room falls below it
   where the text is. */
.cnode-main {
  display: flex; flex-direction: row; align-items: flex-start; gap: 8px;
  text-decoration: none; color: inherit; width: 100%; min-width: 0;
}
/* Only the anchor stretches: on the ghost card .cnode-main is an inert span
   that carries the same two columns and nothing to click. */
a.cnode-main::after { content: ''; position: absolute; inset: 0; border-radius: var(--radius); }
/* min-width:0 so a long unbroken word shrinks this column instead of pushing
   the card wider than --cardw. text-align because .chart li centres its text. */
/* No gap: every line's line-height already carries its own leading, so the 1px
   that used to sit between them bought nothing.

   The column is the name block plus however many life lines this CHART
   reserves, at 15px each. The name block is 34px, or 51px in a chart where
   anybody has a birth name that differs from their surname. So 34px on a tree
   of bare names, 111px where somebody has a birth name AND all four of
   born-when, born-where, died-when, died-where -- the only combination that
   grows past the 96px picture and makes the text, not the picture, set the
   card height. See .cnode-photo. */
.cnode-text {
  display: flex; flex-direction: column;
  flex: 1; min-width: 0; text-align: left;
}
.cnode:hover { border-color: var(--muted); }
/* A portrait, not a badge: 3:4, which is the shape people photograph each other
   in and the shape a scanned print already is. A circle cropped every one of
   them to the middle of a face and threw away the rest; object-fit: cover still
   crops, but to a frame the picture was likely composed for.

   72x96 in almost every chart, and sized against the BUILT-IN facts: the two
   name lines, the birth-name line, and the four life lines. max(96px, that) is
   111px at the very most, so in every tree that has not added fields of its own
   the picture is the tallest thing in the card -- which is what makes its
   distance to the border the padding itself: 10px above, 10px below, 10px to
   its left, all three the same BY CONSTRUCTION rather than by three rules
   happening to agree. It was a flat 72x96 until a birth-name line and four life
   lines together asked for 111px of text and left the picture floating 18.5px
   off a border it was 11px from at the side.

   The tree's OWN fields are deliberately not in that sum. A card is a picture
   beside some text, and a tree showing four of its own fields would otherwise
   drive the picture to 128px wide -- a third of the card, and the card another
   40px wider, multiplied by every cousin in the widest generation. So past the
   built-in facts the text grows and the picture does not: it keeps 10px to the
   top and the left, and the room the extra lines need opens below it.

   The 96px floor is what makes a chart of bare names stand exactly as tall as
   one with every date and place filled in, instead of collapsing to a shorter
   card whenever a tree happens to be thin. It is also what that costs: 118px
   for every card in every chart, where a two-line chart used to be 86px. The
   picture is the same picture everywhere and every card in one chart is the
   same size, which is the property the whole file is arranged around.

   Exactly 3:4, and the numbers come out whole in every case a chart can
   actually reach: the only fractional width is a chart carrying a birth name
   and all four life lines, at 83.25px. That is one length for the CSS zoom on
   #cz to round, in a file with a history of trouble there, and it is checked.

   2px, against the card's 6px. A radius is a corner drawn at a distance from
   the one outside it: matching 6px would make the picture look like it was
   trying and failing to be the card, and 0px reads as a crop mark against a
   rounded box. Centred vertically, so it is a column of its own rather than
   something hung off the top line. */
.cnode-photo {
  width: var(--picw); height: var(--pich); border-radius: 2px; object-fit: cover;
  flex: none;
}
/* Exactly two lines, always -- height, not min-height, so that every card is
   the same box. The two lines are no longer wherever the name happened to wrap:
   given names on the first, surname on the second. A generation is read by
   scanning surnames down it, and they now begin at the same point on every
   card instead of somewhere along a line of full names.

   One line each and an ellipsis rather than a wrap, because the budget is two
   lines and both are spoken for. The whole name is in the title and on the card
   the click opens. */
.cnode-name {
  font-size: 13.5px; font-weight: 600; line-height: 17px; letter-spacing: -0.005em;
  height: calc(34px + var(--birthline, 0px)); width: 100%; min-width: 0;
  display: flex; flex-direction: column;
}
.cnode-given, .cnode-sur {
  height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The name they were born under, in parentheses under the one they are filed
   under -- how a maiden name is written on a chart, and on a chart of one
   family the thing that says which side each person married in from.

   Lighter and a size down, because it is the answer to a second question: the
   line above is who this card is, this one is where they came from. Same 17px
   as the two lines above so the name block stays a whole number of lines and
   the arithmetic in .cnode-text holds.

   Reserved chart-wide or not at all, like the life lines: --birthline is 0
   unless somebody in THIS chart has a birth name that differs from their
   surname, which in most trees is nobody. */
.cnode-birth {
  height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 11.5px; font-weight: 400; color: var(--muted);
}
.chart.slot-birth { --birthline: 17px; }
.chart:not(.slot-birth) .cnode-birth { display: none; }
/* Born when, born where, died when, died where: a line each, down the card.

   They used to be two lines -- both dates run together with a middle dot, then
   the birthplace -- and a card is 118px of text wide. '*3. Juli 1901 · †12. Mai
   1970' is not a string that fits in 118px, so it arrived cut off partway
   through the second date, which is to say the death date was unreadable on
   every card that had an exact one. A line each gives all four the full width,
   and the death PLACE, which had nowhere to go before, a place to go.

   Fixed height on the box, not on the lines inside it: the block is as tall as
   the most lines any card in THIS chart uses (chart_slot_lines() counts them
   and writes lines-N on .chart), and a card with fewer simply leaves the bottom
   of its box empty. Nothing is emitted for a fact nobody recorded, so what is
   recorded packs to the top on its own -- that is the whole of "each moves up
   if its predecessor is not set", and it costs no rule at all.

   overflow: hidden on the box as well, so that a fifth line could never make
   one card taller than its neighbours while somebody is working on it. */
/* TWO counts, and the difference between them is the whole reason there are
   two: --lifelines is the four built-in life lines, which the picture is sized
   against; --fieldlines is the tree's own fields, which stack under them and
   deliberately do NOT grow it. See .cnode-photo. Written out rather than
   computed, because a custom property cannot be set from a class name any
   other way. */
.chart          { --lifelines: 0; --fieldlines: 0; --birthline: 0px; }
.chart.lines-1  { --lifelines: 1; }
.chart.lines-2  { --lifelines: 2; }
.chart.lines-3  { --lifelines: 3; }
.chart.lines-4  { --lifelines: 4; }
.chart.fields-1 { --fieldlines: 1; }
.chart.fields-2 { --fieldlines: 2; }
.chart.fields-3 { --fieldlines: 3; }
.chart.fields-4 { --fieldlines: 4; }
.cnode-life {
  display: flex; flex-direction: column;
  width: 100%; min-width: 0;
  height: calc((var(--lifelines) + var(--fieldlines)) * 15px); overflow: hidden;
}
/* One line, clipped rather than wrapped: the height above is a budget, and a
   line allowed to wrap would spend the next line's share of it. */
.cnode-lifeline {
  font-size: 12px; color: var(--muted); line-height: 15px; height: 15px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A place is subordinate to the date above it and reads as the answer to
   "where", so it is quieter and a size down -- which also keeps a long village
   name from being the widest thing on the card. */
.cnode-lifeline.is-place { font-size: 11px; color: var(--faint); }
/* A field the tree defined for itself. Same size as a place and the same
   quiet, because it is the same kind of thing: a detail under the facts every
   card carries, not one of them. No label in front of it -- see chart_card(). */
.cnode-lifeline.is-field { font-size: 11px; color: var(--faint); }
/* The mark column. Its own element, and a fixed width, so that '*' and the
   dagger -- which are nothing like the same width in any font -- still start
   their text at the same x, and so that a place written under its own date is
   indented to line up with it rather than with the mark. Empty on a
   continuation line: the event is marked once, where it starts. */
.cnode-mark {
  display: inline-block; width: 10px;
  color: var(--faint); font-variant-numeric: normal;
}
/* Initials, in the shape .avatar already established for the account picture,
   but NEVER in the accent: on a chart the accent means one thing only, "you are
   here", and two hundred accent-coloured circles would say it about everybody.
   Quiet enough to be skipped -- it repeats the name beside it and is
   aria-hidden -- and still 4.72:1 on the card, measured. place-items so the
   person with only a surname recorded gets their one letter centred too. */
.cnode-photo.mono {
  display: grid; place-items: center;
  /* --card-line, not --line-2: inside a spouse card the frame sits on a tinted
     fill rather than on white, and a border faint enough to work on one is
     invisible on the other. The same line as the card that holds it. */
  background: var(--bg); border: 1px solid var(--card-line);
  color: var(--muted); font-size: 22px; font-weight: 600;
  letter-spacing: 0.02em; user-select: none;
}
/* The only card left with nothing in the column is the "Partner unknown" ghost,
   which is the absence of a person and has no name to take letters from. Hidden
   rather than removed, so it still holds the column open and the ghost's text
   starts on the same left edge as every other card in its generation.

   'blank', not 'empty': .empty is the page-level "nothing here yet" panel, with
   a dashed border and 22px/16px of padding. The placeholder was picking all of
   that up and measuring 34x46 instead of 32x32 -- invisible only because it is
   also invisible. */
.cnode-photo.blank { visibility: hidden; }
/* The name of a field the tree defined for itself, in front of what it says.
   Quiet, because on a panel the label is the question and the value is the
   answer -- and a tree with eight of its own fields would otherwise read as
   eight headings. */
.cf-label { color: var(--faint); }

/* The research behind a card. A grid rather than a list because most of these
   are scans, and a scan is recognised by looking at it -- a filename tells you
   nothing about which page of the parish register you are about to open. The
   PDFs, which cannot be previewed, get the same tile with their type in it so
   the two read as one collection. */
.docs {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 10px; margin-top: 4px;
}
.doc {
  display: flex; flex-direction: column; gap: 3px;
  text-decoration: none; color: inherit; min-width: 0;
}
.doc img, .doc-kind {
  width: 100%; aspect-ratio: 3 / 4; object-fit: cover;
  border: 1px solid var(--card-line); border-radius: var(--radius); background: var(--bg);
}
.doc-kind {
  display: grid; place-items: center;
  font-size: 13px; font-weight: 600; letter-spacing: 0.04em; color: var(--muted);
}
.doc:hover img, .doc:hover .doc-kind { border-color: var(--accent); }
.doc-name {
  font-size: 12px; line-height: 15px;
  /* Two lines, then an ellipsis: a title long enough to say which register and
     which year is the useful kind, and it should not push the tile below it. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.doc-meta { font-size: 11px; color: var(--faint); }
/* On the edit form the same documents are a plain list: it is a place to take
   them away, not to look at them. */
.doclist { display: flex; flex-direction: column; gap: 6px; margin-bottom: 4px; }
.docrow  { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; font-size: 12px; }
.cnode.repeat { border-style: dashed; opacity: .75; }
.cnode-repeat { color: var(--accent-ink); }
/* The accent, once again meaning only "you are here". */
/* Exactly the border and padding of every other card, so it is exactly the same
   box -- the emphasis is a box-shadow ring, which is drawn outside the border
   box and costs no layout at all. A 2px border plus 1px less padding looked
   equivalent and was not: the chart zooms, and at 0.65 a 2px border rounds down
   to the same device pixel as a 1px one while the padding difference stays,
   leaving the starting card 2px shorter than everything around it. */
.cnode.is-root {
  border-color: var(--accent); background: var(--accent-soft);
  box-shadow: 0 0 0 1px var(--accent);
}
.cnode.is-root .cnode-name { color: var(--accent-ink); }
.cnode.is-root .cnode-lifeline,
.cnode.is-root .cnode-mark { color: #5f5b50; }   /* 6.05:1 on the accent tint */
/* A spouse married INTO the family: the connector meets a couple at its centre,
   so this is what tells you which card the line descended to. */
/* A spouse married INTO the family: the connector meets a couple at its centre,
   so this is what tells you which card the line descended to.

   It used to say that by having NO background, which meant it had no contrast
   with the canvas at all -- 1.00:1, the same colour -- and a 1.45:1 dashed
   border was the whole of it. It was a card you could not see. It has a fill of
   its own now, and the dash and the lighter name still carry what it means. */
.cnode.is-spouse { background: var(--card-alt); border-style: dashed; }
.cnode.is-spouse .cnode-name { font-weight: 400; color: var(--ink-2); }
/* The absence of a person, which is why it keeps the canvas: there is no card
   here, only the space one would occupy. The dashed border at 3:1 is what makes
   that legible as a deliberate gap rather than as a rendering fault. */
.cnode.ghost { background: transparent; border-style: dashed; color: var(--muted); }

.mlink {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  /* Roomier than it was: a full date next to "geschieden" is wider than either
     card it sits between, and at 46px the two cards were nearly touching. */
  min-width: 84px; padding: 0 12px; flex: none;
}
/* A connector, drawn in CSS rather than SVG because it lives between two cards
   in the flow -- so it takes the connector's colour, not a form border's. */
.mbar { width: 100%; border-top: 1px solid var(--card-line); }
.mtext { font-size: 10.5px; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Its own line under the date rather than run together with a separator. */
.mnote { font-size: 10px; color: var(--faint); white-space: nowrap; line-height: 1.3; }

.gtag {
  font-size: 10px; color: var(--muted); background: var(--bg);
  border: 1px solid var(--card-line); border-radius: 999px; padding: 2px 9px; white-space: nowrap;
  /* Out of flow and ON the line, which is drawn to the child the branch descends
     to rather than to the middle of the row. BOTH offsets are measured with the
     connectors, from the same cards, so the pill lands on its own line by
     construction instead of by two rules agreeing.

     --gtag-y matters now that each marriage has its own rail height: a pill
     pinned to a fixed 22px would sit on the first marriage's line and float
     beside every other one. The 22px is the fallback for the frame before the
     first measurement, and is what a single marriage measures to anyway. */
  position: absolute; top: var(--gtag-y, 22px); left: var(--gtag-x, 50%);
  transform: translateX(-50%);
  height: 22px; box-sizing: border-box; line-height: 16px;
  z-index: 1;
}
.collapse {
  flex: none; margin-left: 8px; cursor: pointer;
  font: inherit; font-size: 11px; font-weight: 600; line-height: 1;
  min-width: 24px; padding: 4px 6px;
  color: var(--muted); background: var(--surface);
  border: 1px solid var(--card-line); border-radius: 999px;
  font-variant-numeric: tabular-nums;
}
.collapse:hover { color: var(--ink); border-color: var(--muted); }
.chart li.collapsed > ul.groups { display: none; }
.chart li.collapsed > .couple > .collapse { color: var(--accent-ink); background: var(--accent-soft); border-color: var(--accent); }

/* A menu now, not a strip under the chart: one column, and it only exists while
   somebody is asking. */
.helpmenu { position: relative; flex: none; }
.helpmenu > summary { list-style: none; }
.helpmenu > summary::-webkit-details-marker { display: none; }
.helpmenu .menu {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 40;
  min-width: 268px; max-width: min(320px, calc(100vw - 24px)); padding: 12px 14px;
  max-height: min(60vh, 340px); overflow: auto;
  background: var(--surface); border: 1px solid var(--line-2);
  border-radius: var(--radius); box-shadow: 0 6px 20px rgba(38, 36, 31, 0.13);
}
/* Each entry is a LINE of text, not a flex row: a row makes every text node and
   every <kbd> its own item, so a sentence long enough to wrap -- which the
   German ones are -- gets squeezed into narrow columns instead of flowing. */
.chart-legend {
  display: flex; flex-direction: column; gap: 9px;
  color: var(--muted); font-size: 11.5px; line-height: 1.5; margin: 0;
}
.chart-legend span { display: block; }
.chart-legend kbd {
  display: inline-block; vertical-align: 1px;
  font: inherit; font-size: 10.5px; line-height: 1; color: var(--faint);
  border: 1px solid var(--line-2); border-radius: 4px; padding: 2px 5px; background: var(--bg);
}
.chart-legend i {
  width: 18px; height: 12px; border-radius: 3px; display: inline-block;
  vertical-align: -1px; margin-right: 3px;
  border: 1px solid var(--line-2); background: var(--surface);
}
.chart-legend i.sp { border-style: dashed; background: transparent; }
/* The other two swatches are card borders; this one is a LINE, so it is drawn
   as one rather than as a box that happens to be flat. */
.chart-legend i.ln {
  height: 0; border: 0; border-top: 1px dashed var(--muted);
  border-radius: 0; background: none; vertical-align: 4px;
}

/* --------------------------------------------------------------- flyover -- */

.flyover {
  border: 1px solid var(--line-2); border-radius: 10px; background: var(--bg);
  color: var(--ink); padding: 22px 20px 20px; max-width: 560px; width: calc(100vw - 24px);
  max-height: 88vh; overflow: auto; box-shadow: 0 18px 50px rgba(20, 18, 14, .3);
}
.flyover::backdrop { background: rgba(20, 18, 14, .42); }
.fly-close {
  position: absolute; top: 10px; right: 12px; cursor: pointer;
  font-size: 20px; line-height: 1; padding: 2px 8px;
  background: none; border: 0; color: var(--muted);
}
.fly-close:hover { color: var(--ink); }
.flyover h1.fly-title { font-size: 18px; margin: 0 0 4px; }
.flyover .subtitle { margin-bottom: 18px; }
.fly-rel {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line);
}
.fly-rel .btn { font-size: 12px; padding: 7px 11px; }
.fly-rel .rowlabel { width: auto; padding-top: 0; }

@media (max-width: 700px) {
  body.wide { --pagepad: 8px; padding-left: 8px; padding-right: 8px; }
  .chart-scroll { padding: 12px 6px; height: calc(100vh - 178px); }
  .rel { grid-template-columns: 1fr; }
  .rel > h2 { grid-column: 1; padding-top: 0; padding-bottom: 6px; }
  .rel > *:not(h2) { grid-column: 1; }
  .union-row { grid-template-columns: 1fr; }
  .rowlabel { padding-top: 12px; }
}

/* The create-tree disclosure: a button until asked for. */
.new-tree { margin-top: 20px; }
.new-tree > summary { list-style: none; display: inline-flex; width: auto; }
.new-tree > summary::-webkit-details-marker { display: none; }
.new-tree[open] > summary { margin-bottom: 16px; }
.new-tree .card-form { max-width: 380px; }

/* ---------------------------------------------------------------- search -- */

/* The search leads this row and is given the room to look like the main way in:
   on a tree of any size it is faster than panning, and it is the first thing
   somebody who has just been invited wants. */
.csearch { position: relative; flex: 1 1 320px; min-width: 200px; max-width: 460px; }
.csearch input {
  font: inherit; font-size: 14px; width: 100%; box-sizing: border-box;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--line-2); border-radius: var(--radius);
  padding: 10px 34px 10px 36px;
  box-shadow: 0 1px 2px rgba(38, 36, 31, 0.04);
}
.csearch input::placeholder { color: var(--muted); }
.csearch input:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
.csearch-icon {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  display: grid; place-items: center; color: var(--muted); pointer-events: none;
}
.csearch input:focus ~ .csearch-icon { color: var(--accent-ink); }
/* The hint retires as soon as the field is in use. */
.csearch-key {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  font: inherit; font-size: 11px; line-height: 1; color: var(--faint);
  border: 1px solid var(--line-2); border-radius: 4px; padding: 3px 6px;
  background: var(--bg); pointer-events: none;
}
.csearch input:focus ~ .csearch-key,
.csearch input:not(:placeholder-shown) ~ .csearch-key { display: none; }
@media (hover: none) { .csearch-key { display: none; } }

.cresults {
  position: absolute; z-index: 5; top: calc(100% + 5px); left: 0; right: 0;
  background: var(--surface); border: 1px solid var(--line-2); border-radius: var(--radius);
  box-shadow: 0 12px 30px rgba(20, 18, 14, .16);
  max-height: 320px; overflow: auto; padding: 4px;
}
.cresult {
  display: flex; align-items: baseline; gap: 9px; width: 100%;
  font: inherit; text-align: left; cursor: pointer;
  background: none; border: 0; border-radius: 4px; padding: 8px 9px;
  color: var(--ink);
}
.cresult:hover { background: var(--bg); }
.cresult-name { font-size: 13.5px; font-weight: 500; }
.cresult-life { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* The whole point of the index: somebody the current chart does not render. */
.cresult-away { margin-left: auto; font-size: 11px; color: var(--accent-ink); white-space: nowrap; }
.cresult-none { margin: 0; padding: 9px; font-size: 13px; color: var(--muted); }

/* Found by search — deliberately not the accent, which means "you are here". */
.cnode.found {
  outline: 2px solid var(--ink); outline-offset: 2px;
  animation: found-pulse 1.4s ease-out 1;
}
@keyframes found-pulse {
  0%   { outline-color: var(--ink); }
  60%  { outline-color: var(--ink); }
  100% { outline-color: transparent; }
}

/* The marriage line is the partnership, so it is where you edit it. */
.mlink.editable { cursor: pointer; border-radius: 4px; padding: 2px; margin: -2px; }
/* Neutral, not the accent: the accent means "you are here", and a hover state
   that borrows it would be the third meaning all over again. */
.mlink.editable:hover, .mlink.editable:focus-visible { background: var(--bg); outline: none; }
.mlink.editable:hover .mbar, .mlink.editable:focus-visible .mbar { border-top-color: var(--ink); }
.mpen {
  font-size: 10px; line-height: 1; color: var(--muted); opacity: 0;
  position: absolute; bottom: -13px; left: 50%; transform: translateX(-50%);
}
.mlink { position: relative; }
.mlink.editable:hover .mpen, .mlink.editable:focus-visible .mpen { opacity: 1; }
/* No hover on a touchscreen, so the pencil is always faintly there. */
@media (hover: none) {
  .mpen { opacity: .55; }
}

/* The route you climbed, so that "up" is not a series of invisible choices. */
.trail { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin: -8px 0 14px; }
.trail strong { font-weight: 600; color: var(--ink); }
.trail-sep { color: var(--faint); }
.trail-up { display: inline-flex; flex-wrap: wrap; gap: 8px; margin-left: 6px; }
.trail .up {
  font-size: 12px; text-decoration: none; white-space: nowrap;
  border: 1px solid var(--line-2); border-radius: 999px; padding: 3px 10px;
  background: var(--surface); color: var(--ink-2);
}
.trail .up:hover { border-color: var(--muted); color: var(--ink); }
