/* The family portal shell.

   Deliberately larger type and larger tap targets than the office apps. The
   audience is not staff on a laptop: it is thirty adults across three
   households on phones, and the two people the app most exists for are 76 and
   66. 16px base, 44px minimum touch target, high contrast.

   Hand-written rather than a Tailwind build so the values are editable without
   a rebuild step, and so the CSP can stay `script-src 'self'` with no CDN. */

:root {
  --ink: #1c1b18;
  --ink-soft: #57544d;
  /* ⚠️ MEASURED, NOT PICKED. The old #8b877e was 3.43:1 on --paper and
     3.58:1 on --card -- below the 4.5:1 WCAG AA floor at every size it is used
     at, and it is the colour of .muted, .hint, .row-sub, .card-title, .empty,
     .side-mini and .side-sub: roughly 150 places across 18 templates, all of
     them 9-14px, none of them large-text size. On an app whose two most
     important readers are 76 and 66 that is the single widest-reaching defect
     in the stylesheet.

     #67645e measures 5.64:1 on --paper and 5.90:1 on --card, and --ink-soft is
     7.23:1 on paper -- comfortably past AA, and still plainly lighter than
     --ink-soft, which is the hierarchy this token exists to draw.

     ⚠️ Those three figures were WRONG when this comment was first written
     (4.59 / 4.79 / 7.55). They belonged to an earlier candidate colour and were
     never recomputed for the one that shipped -- so a comment headed MEASURED
     carried numbers nobody had measured, which is worse than no comment at all.
     The test CANNOT catch that: tests/test_visual_contract.py recomputes from
     the tokens by design, so it passes whatever prose sits here. Recompute by
     hand if you ever change the value. */
  --ink-faint: #67645e;
  --line: #e2e2dd;
  --paper: #fafaf8;
  --card: #ffffff;
  --accent: #4f46e5;
  --accent-soft: #eef2ff;
  --good: #15803d;
  --good-soft: #f0fdf4;
  --bad: #b91c1c;
  --bad-soft: #fef2f2;
  --warm: #b45309;
  --radius: 10px;
  --shadow: 0 1px 3px rgba(0, 0, 0, .04);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font: 16px/1.55 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

.skip {
  position: absolute; left: -9999px;
  background: var(--card); padding: .75rem 1rem; z-index: 100;
}
.skip:focus { left: .5rem; top: .5rem; }

/* -- layout ------------------------------------------------------------- */
.layout { display: flex; min-height: 100vh; }

.sidebar {
  width: 232px; flex: 0 0 232px;
  background: linear-gradient(#fff, var(--paper));
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
}
.side-head { padding: 1.1rem 1rem .75rem; }
.side-brand { display: flex; gap: .6rem; align-items: center; color: inherit; }
.side-brand:hover { text-decoration: none; }
.side-mark {
  width: 30px; height: 30px; border-radius: 8px; flex: 0 0 30px;
  background: var(--ink); color: #fff;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 700; letter-spacing: .04em;
}
.side-title { display: block; font-size: 14px; font-weight: 700; letter-spacing: .02em; }
.side-sub {
  display: block; font-size: 9px; font-weight: 600;
  letter-spacing: .16em; color: var(--ink-faint); margin-top: 1px;
}

.side-nav { padding: .5rem .6rem; display: flex; flex-direction: column; gap: 2px; flex: 1; }
.side-link {
  display: block; padding: .62rem .7rem; border-radius: 8px;
  color: var(--ink-soft); font-size: 14px; font-weight: 500;
  min-height: 44px; line-height: 1.5;
}
.side-link:hover { background: #f2f1ec; color: var(--ink); text-decoration: none; }
.side-link.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.side-section {
  padding: .9rem .7rem .3rem; font-size: 10px; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint);
}

.side-foot { padding: .8rem 1rem 1rem; border-top: 1px solid var(--line); }
.side-who { font-size: 13px; font-weight: 600; margin-bottom: .35rem; }
.side-mini { display: block; font-size: 12px; color: var(--ink-faint); padding: .25rem 0; }
.badge-staff {
  display: inline-block; margin-left: .35rem; padding: 1px 6px;
  border-radius: 999px; background: #f2f1ec; color: var(--ink-faint);
  font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
}

/* ⚠️ `margin: 0 auto` IS LOAD-BEARING, not tidiness. A flex item with
   `flex: 1` and a `max-width` grows to the cap and then stops, and the leftover
   space all falls on ONE side -- so on any screen wider than about 1330px the
   whole app sat hard against the sidebar with a hand-width of blank paper down
   the right. Auto margins are what absorb that free space evenly. */
.main { flex: 1; padding: 1.6rem 1.8rem 4rem; max-width: 1100px; margin: 0 auto; width: 100%; }

/* -- headings ----------------------------------------------------------- */
h1 { font-size: 26px; margin: 0 0 .2rem; letter-spacing: -.01em; }
h2 { font-size: 19px; margin: 2rem 0 .6rem; }
h3 { font-size: 16px; margin: 1.2rem 0 .4rem; }
.lede { color: var(--ink-soft); margin: 0 0 1.4rem; font-size: 15px; }
.muted { color: var(--ink-faint); }
.small { font-size: 13px; }

/* -- cards -------------------------------------------------------------- */
.card {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 1.1rem 1.2rem; margin-bottom: 1rem;
}
/* ⚠️ The margin is the full shorthand, not just `margin-bottom`, because this
   class is worn by an <h2> as well as by a <div>: a card title that is a real
   heading (so it can be navigated to) would otherwise inherit h2's 2rem
   margin-top and sit adrift inside its own card. A <div> has no default margin,
   so nothing that wore this before looks any different. */
.card-title { font-size: 12px; font-weight: 700; letter-spacing: .08em;
              text-transform: uppercase; color: var(--ink-faint);
              margin: 0 0 .7rem; }
.grid { display: grid; gap: 1rem; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
/* ⚠️ NOT --ink-faint AND NOT 14px. With the database empty this rule is the
   ENTIRE visible content of Photos, Get-togethers and Announcements -- so the
   one thing a first visitor reads was the app's smallest, palest text. It is
   body-sized and body-coloured now; the dashed border is what says "nothing
   here yet", and it does not need the type to whisper as well. */
.empty {
  color: var(--ink-soft); font-size: 16px; line-height: 1.55;
  padding: 1.5rem 1.2rem;
  border: 1px dashed var(--line); border-radius: var(--radius); text-align: center;
}
.empty .btn-row { justify-content: center; }

/* -- buttons ------------------------------------------------------------ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  min-height: 44px; padding: .6rem 1.05rem; border-radius: 8px;
  border: 1px solid var(--line); background: var(--card); color: var(--ink);
  font-size: 15px; font-weight: 600; cursor: pointer; text-decoration: none;
}
.btn:hover { background: #f7f6f2; text-decoration: none; }
/* ⚠️ THE WHOLE APP HAD NO :active AND NO :focus-visible -- two focus rules in
   the file, both on form fields, and none anywhere on .btn (101 uses), .tile,
   .album-card, .side-link or .tabs. A phone has no hover, so on the device the
   posters actually use there was NO acknowledgement that a tap had landed: you
   press Send and nothing changes until the page navigates. That is what makes
   somebody press it twice. And with no visible focus ring the app was
   unusable by keyboard. */
.btn:active, .tile:active, .album-card:active,
.react-b:active, .side-link:active { transform: translateY(1px); }
.btn:focus-visible, .tile:focus-visible, .album-card:focus-visible,
.side-link:focus-visible, .tabs a:focus-visible, .react-b:focus-visible,
.linkish:focus-visible, summary:focus-visible, a:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px;
}
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: #4338ca; }
.btn-danger { color: var(--bad); border-color: #f0c9c9; }
.btn-sm { min-height: 36px; padding: .35rem .7rem; font-size: 13px; }
.btn-row { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .9rem; }

/* -- forms -------------------------------------------------------------- */
label { display: block; font-size: 13px; font-weight: 600; margin: .9rem 0 .3rem; }
/* ⚠️ input[type=file] IS IN THIS LIST DELIBERATELY. It was the one control
   the whole app depends on -- nothing gets into this site except through it --
   and left to the user-agent default it rendered 21px tall on a phone, half the
   floor this file declares for itself. */
input[type=text], input[type=email], input[type=password], input[type=date],
input[type=datetime-local], input[type=number], input[type=search],
input[type=file], select, textarea {
  width: 100%; min-height: 44px; padding: .55rem .7rem;
  border: 1px solid var(--line); border-radius: 8px;
  font: inherit; background: #fff; color: var(--ink);
}
textarea { min-height: 120px; resize: vertical; }
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);
}
input[type=file] { padding: .45rem .6rem; }
input[type=file]::file-selector-button {
  font: inherit; font-weight: 600; min-height: 34px;
  margin-right: .7rem; padding: .35rem .8rem;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--card); color: var(--ink); cursor: pointer;
}

/* A checkbox and its words as ONE 44px target. The bare control is 13x13px in
   every browser, inside a 21px label -- and one of them is the confirmation
   that deletes a get-together. */
.check {
  display: flex; align-items: center; gap: .6rem;
  min-height: 44px; font-size: 15px; font-weight: 500;
}
.check input[type=checkbox] { width: 22px; height: 22px; flex: 0 0 22px; margin: 0; }

.hint { font-size: 13px; color: var(--ink-faint); margin-top: .25rem; }
.field-row { display: flex; gap: .8rem; flex-wrap: wrap; }
.field-row > * { flex: 1 1 160px; }

/* -- flashes ------------------------------------------------------------ */
.flashes { margin-bottom: 1rem; }
.flash {
  padding: .7rem .9rem; border-radius: 8px; font-size: 14px;
  border: 1px solid; margin-bottom: .5rem;
}
.flash-ok { background: var(--good-soft); border-color: #bbf7d0; color: var(--good); }
.flash-error { background: var(--bad-soft); border-color: #fecaca; color: var(--bad); }
/* Something the reader still has to do. Amber rather than red: a Shabbos
   deferral is expected and correct, and alarming somebody about the app
   observing Shabbos would be its own kind of wrong. */
.flash-warn { background: #fffbeb; border-color: #fde68a; color: var(--warm); }

/* -- photo grid --------------------------------------------------------- */
.tiles { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.tile {
  display: block; aspect-ratio: 1 / 1; border-radius: 8px; overflow: hidden;
  background: #efeee9; border: 1px solid var(--line); position: relative;
}
.tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tile-cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: linear-gradient(transparent, rgba(0, 0, 0, .62));
  color: #fff; font-size: 12px; padding: 1.4rem .5rem .4rem;
}
.album-card { display: block; color: inherit; }
.album-card:hover { text-decoration: none; }
.album-card .tile { aspect-ratio: 4 / 3; }
/* Clamped: .tiles is a grid whose row height is its tallest item, and album
   titles are maxlength 256 -- so one long name silently inflates every card
   beside it. The full text stays in a title attribute. */
.album-name {
  font-weight: 600; font-size: 15px; margin-top: .45rem;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}

/* -- lists -------------------------------------------------------------- */
.rows { border: 1px solid var(--line); border-radius: var(--radius);
        background: var(--card); overflow: hidden; }
.row {
  display: flex; gap: .9rem; align-items: center;
  padding: .8rem 1rem; border-bottom: 1px solid var(--line);
}
.row:last-child { border-bottom: 0; }
.row-main { flex: 1; min-width: 0; }
.row-title { font-weight: 600; }
.row-sub { font-size: 13px; color: var(--ink-faint); }
.avatar {
  width: 40px; height: 40px; border-radius: 999px; flex: 0 0 40px;
  background: #efeee9; object-fit: cover;
  display: grid; place-items: center; font-size: 13px; font-weight: 700;
  color: var(--ink-faint);
}

/* -- pills -------------------------------------------------------------- */
.pill {
  display: inline-block; padding: 2px 9px; border-radius: 999px;
  font-size: 12px; font-weight: 600; background: #f2f1ec; color: var(--ink-soft);
}
.pill-yes { background: var(--good-soft); color: var(--good); }
.pill-no { background: var(--bad-soft); color: var(--bad); }
.pill-maybe { background: #fffbeb; color: var(--warm); }

/* -- switcher ----------------------------------------------------------- */
.switch { position: relative; margin: 0 .6rem .4rem; }
.switch-cur {
  display: flex; align-items: center; justify-content: space-between;
  padding: .5rem .7rem; border: 1px solid var(--line); border-radius: 8px;
  font-size: 12px; color: var(--ink-soft); background: #fff; cursor: default;
}
.switch:hover .switch-menu { display: block; }
.switch-menu {
  display: none; position: absolute; left: 0; right: 0; top: 100%;
  background: #fff; border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .08); padding: 4px; z-index: 40;
  max-height: 60vh; overflow-y: auto;
}
.switch-item { display: block; padding: .38rem .55rem; border-radius: 6px;
               font-size: 12px; color: var(--ink-soft); }
.switch-item:hover { background: #f5f4ef; text-decoration: none; }

/* -- sign-in ------------------------------------------------------------ */
.anon { min-height: 100vh; display: grid; place-items: center; padding: 1.5rem; }
.anon-card {
  width: 100%; max-width: 400px; background: var(--card);
  border: 1px solid var(--line); border-radius: 14px;
  box-shadow: var(--shadow); padding: 2rem 1.75rem;
}
.anon-card h1 { text-align: center; font-size: 22px; }
.anon-card .lede { text-align: center; }

/* -- directory / tree switch -------------------------------------------- */
.tabs {
  display: flex; gap: .4rem; margin: -.4rem 0 1rem;
  border-bottom: 1px solid var(--line);
}
.tabs > * {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: .55rem .85rem; font-size: 14px; font-weight: 600;
  color: var(--ink-faint); border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.tabs a:hover { color: var(--ink); text-decoration: none; }
.tab-on { color: var(--ink); border-bottom-color: var(--accent); }

/* -- the family tree ----------------------------------------------------- */
/* Nesting is drawn with a border on the nested list plus a short stub per row,
   rather than with box-drawing characters: the stub scales with the row and a
   character does not, and a screen reader reads a border as nothing at all
   where it reads U+2514 out loud on every line. */
.tree-card { padding: .75rem 1rem; overflow-x: auto; }
.tree-list { list-style: none; margin: 0; padding: 0; }
.tree-list .tree-list {
  margin-left: 1.15rem; padding-left: 1.15rem;
  border-left: 1px solid var(--line);
}
.tree-item { position: relative; }
.tree-list .tree-list > .tree-item::before {
  content: ''; position: absolute; left: -1.15rem; top: 1.3rem;
  width: .85rem; height: 1px; background: var(--line);
}
.tree-line {
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  padding: .3rem 0; min-height: 44px;   /* the app's tap-target floor */
}
.tree-person {
  display: inline-flex; align-items: center; gap: .5rem;
  color: var(--ink); max-width: 100%;
}
/* 32px rather than the directory's 40px: a tree row is indented up to four
   levels deep on a 375px screen, and the face is a recognition cue here where
   in the directory it is the row. */
.tree-face { width: 32px; height: 32px; flex: 0 0 32px; font-size: 11px; }
.tree-name { font-weight: 600; }
.tree-amp { color: var(--ink-faint); font-size: 13px; }
.tree-private { color: var(--ink-faint); }
.tree-private .tree-name { font-weight: 500; font-style: italic; }
.tree-count { flex: 0 0 auto; }

/* -- the family tree, drawn as a chart ------------------------------------
   Generations in rows, top to bottom, the way a tree is drawn by hand. An
   alternate view of the SAME structure the indented list walks; that one stays
   the default because it is the one that reads on a phone.

   Sized for this app's readers rather than for density: the type here is the
   body size, the faces are the directory's 40px, and nothing shrinks below
   what the two people this app most exists for can read. That is also why the
   smallest zoom is 70% and not the 40% a denser tree would allow -- a whole
   family on one screen is worth nothing if nobody can read it.

   No script runs on this page (`script-src 'self'`), so folding and zoom are
   the checkbox/radio pattern and work with scripting off. */
.chart-bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem;
  margin: 0 0 .75rem;
}
.chart-hint { color: var(--ink-faint); font-size: 13px; }
.nowrap { white-space: nowrap; }

/* the segmented Chart / List switch, shared with the indented view */
.seg {
  display: inline-flex; border: 1px solid var(--line);
  border-radius: 8px; overflow: hidden; background: var(--card);
}
.seg > a, .seg > label {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px;                    /* the app's tap-target floor */
  padding: 0 .85rem; font-size: 15px; font-weight: 600;
  color: var(--ink-soft); cursor: pointer; text-decoration: none;
}
.seg > * + * { border-left: 1px solid var(--line); }
.seg > a:hover, .seg > label:hover { background: var(--paper); color: var(--ink); text-decoration: none; }
.seg .seg-on { background: var(--accent); color: #fff; }

/* The fold checkbox is never seen, only the label that drives it.
   NOT `display: none`: that takes it off the keyboard too, and it is the only
   way to open a branch without script. It stays focusable, and the label it
   drives carries the focus ring. */
.chart-fold {
  position: absolute; width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
}

/* ⚠️ Anything that CANNOT work without static/js/tree.js is hidden until that
   script confirms it is running, so the page never shows a control that does
   nothing when tapped. Opening one generation at a time needs no script and is
   deliberately not in here. Do not reveal a `.js-only` control "for tidiness". */
.js-only { display: none; }
.tree-js .js-only { display: inline-flex; }

.chart-zoom { align-items: center; gap: .4rem; }
.chart-actions { gap: .4rem; }
.chart-zbtn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 40px; min-height: 40px; padding: 0;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--card); color: var(--ink);
  font-size: 19px; font-weight: 700; line-height: 1; cursor: pointer;
}
.chart-zbtn:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.chart-zrange { width: 150px; height: 40px; cursor: pointer; accent-color: var(--accent); }
/* A 13px thumb is the browser default and is under the mark for a fingertip. */
.chart-zrange::-webkit-slider-thumb { width: 22px; height: 22px; }
.chart-zrange::-moz-range-thumb { width: 22px; height: 22px; border: 0; }
.chart-zval {
  min-width: 3.4em; font-size: 14px; font-weight: 600; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* The tree is not a card. It is the page: boxing it inside the reading column
   left blank paper down both sides of a drawing that was already scrolling
   sideways. `main-wide` (templates/base.html) lifts the 1100px cap; this keeps
   a rule top and bottom so the scroll region still has edges. */
/* ⚠️ `width: auto` IS THE LOAD-BEARING HALF, not `max-width: none`.
   `.main` sets `width: 100%`, and for a FLEX ITEM that percentage resolves
   against the whole flex container -- the sidebar's 232px included -- not
   against the space left over beside it. With the 1100px cap in place that was
   invisible on most screens; lifting the cap made `.main` exactly one sidebar
   wider than the window, so the page grew a horizontal scrollbar and the right
   hand end of the tree sat permanently off-screen where no amount of scrolling
   the tree itself could reach it. `flex: 1` already sizes this correctly; the
   percentage just has to stop overriding it.

   ⚠️ `min-width: 0` is the OTHER half, and dropping it is worse than the bug it
   replaces. A flex item's automatic minimum size is its CONTENT's minimum, so
   `.main` refuses to shrink below the full width of the tree -- the inner
   `overflow-x: auto` never engages, and instead of the tree scrolling inside a
   fixed frame the whole PAGE scrolls, sidebar and all. Both declarations are
   needed; either one alone leaves a different half of the family unreachable. */
.main-wide { max-width: none; width: auto; min-width: 0; }
.chart-card-wrap {
  padding: .75rem 0 .5rem;
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}

/* -- one person, without leaving the tree --------------------------------
   Clicking a name opens their household here rather than replacing the page,
   because finding your way back to the same branch of a sixty-three person tree
   is the whole cost of navigating away. The card is a real link and still
   navigates if the script is not running. */
.chart-dialog {
  width: min(640px, calc(100vw - 2rem)); max-height: 82vh;
  padding: 0; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); color: var(--ink); overflow: hidden;
}
.chart-dialog::backdrop { background: rgba(28, 27, 24, .45); }
.chart-dialog-head {
  display: flex; align-items: center; gap: .6rem;
  padding: .8rem 1rem; border-bottom: 1px solid var(--line);
  position: sticky; top: 0; background: var(--card);
}
.chart-dialog-title { font-size: 17px; font-weight: 700; flex: 1; margin: 0; }
.chart-dialog-body { padding: 1rem 1.1rem 1.3rem; overflow-y: auto; max-height: calc(82vh - 4rem); }
.chart-dialog-body .tabs, .chart-dialog-body h1 { display: none; }
.chart-dialog-close {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; padding: 0;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--card); color: var(--ink-soft);
  font-size: 20px; line-height: 1; cursor: pointer;
}
.chart-dialog-close:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
.chart-dialog-wait { padding: 1.6rem 1.1rem; color: var(--ink-soft); }
/* The starting size, and the only one available with scripting off -- so it
   is a size that can still be READ, not the smallest one the slider allows. */
/* max-content so the scroller measures the tree's REAL width; auto margins
   centre it while it still fits and resolve to zero once it does not, which is
   the half of the scroll fix that `safe center` does not cover. */
.chart-root { zoom: .7; width: max-content; margin: 0 auto; }
.chart-scroll {
  --chart-line: #cfcfc8;               /* the connector ink: --line is too faint at arm's length */
  overflow-x: auto; padding-bottom: .35rem;
}
/* `zoom` reflows the box model, so the scrollbar still describes the content --
   `transform: scale()` would lie about how wide the tree is. */

/* Each generation is a flex row; the connectors are elbows drawn on the item.
   Nothing is positioned absolutely, so a branch of any width reflows on its
   own and the depth of the family is never written down. */
/* ⚠️ `safe center`, NOT `center`. A centred flex container that OVERFLOWS pushes
   the overflow out of BOTH sides, and whatever goes past the start edge cannot
   be scrolled back to -- the scrollbar does not reach it. On this tree that put
   the entire left-hand side of the family permanently out of view. `safe` drops
   back to flex-start the moment the content stops fitting, which is exactly what
   is wanted: centred while it fits, reachable when it does not. */
.chart-list { display: flex; justify-content: safe center; list-style: none; margin: 0; padding: 0; }
.chart-list .chart-list { padding-top: 22px; position: relative; }
.chart-item {
  position: relative; display: flex; flex-direction: column;
  align-items: center; padding: 22px .3rem 0;
}
/* the elbow: half the sibling bar, plus the drop into this card */
.chart-item::before, .chart-item::after {
  content: ''; position: absolute; top: 0; right: 50%;
  width: 50%; height: 22px; border-top: 2px solid var(--chart-line);
}
.chart-item::after { right: auto; left: 50%; border-left: 2px solid var(--chart-line); }
/* Trim the outer ends and round the corners. On the LAST child the drop comes
   from ::before's right border, which is why clearing every border on ::after
   does not lose the line. */
.chart-item:first-child::before, .chart-item:last-child::after { border: 0 none; }
.chart-item:last-child::before { border-right: 2px solid var(--chart-line); border-radius: 0 7px 0 0; }
.chart-item:first-child::after { border-radius: 7px 0 0 0; }
/* an only child needs no bar, just the drop */
.chart-item:only-child::before { display: none; }
.chart-item:only-child::after {
  left: 50%; right: auto; width: 0; border: 0 none;
  border-left: 2px solid var(--chart-line); border-radius: 0;
}
/* a root has nothing above it */
.chart-root > .chart-item { padding-top: 0; }
.chart-root > .chart-item::before, .chart-root > .chart-item::after { display: none; }
/* the stem from a card down to its children's bar */
.chart-list .chart-list::before {
  content: ''; position: absolute; top: 0; left: 50%; width: 0; height: 22px;
  border-left: 2px solid var(--chart-line);
}
/* Unchecked is CLOSED. That inversion is what makes the tree open one
   generation at a time without any bookkeeping: opening a line reveals
   its children, who are themselves closed. */
li.chart-item > .chart-fold:not(:checked) ~ .chart-list { display: none; }

/* -- one household, one card --------------------------------------------- */
.chart-card {
  position: relative; display: flex; flex-direction: column; gap: .3rem;
  /* A min-height so a one-line name and a wrapped two-line one still leave
     their generation reading as a row, which is the whole point of the drawing. */
  min-width: 190px; max-width: 240px; min-height: 96px;
  padding: .6rem .7rem .95rem;
  background: var(--card); border: 1px solid var(--line);
  border-left: 5px solid var(--chart-gen, var(--ink-faint));
  border-radius: 10px;
}
.chart-faces { display: flex; gap: .3rem; }
.chart-face { width: 40px; height: 40px; flex: 0 0 40px; font-size: 13px; }
.chart-names { font-size: 15px; font-weight: 600; line-height: 1.3; }
.chart-person { color: var(--ink); }
.chart-amp { color: var(--ink-faint); font-weight: 500; }
.chart-card-private { border-left-style: dotted; }
.chart-private { color: var(--ink-faint); font-weight: 500; font-style: italic; }

/* ⚠️ IN FLOW, not floating over the card. These were absolutely positioned on
   the card's bottom edge, which drove the pill straight through the second line
   of any name that wrapped -- and in this family most of them wrap, so it was
   covering the very information the card exists to show. A row of their own
   costs a few pixels of height and cannot obscure anything. */
.chart-controls {
  display: flex; align-items: center; justify-content: center;
  gap: .3rem; margin-top: auto; padding-top: .4rem;
}
/* The child count doubles as the open control, so it is a control and is sized
   like one. Shorter than 44px under a mouse for the same reason .btn-sm is;
   the phone block below raises it. */
/* ⚠️ `display` is set on .chart-count ONLY, never on the pair. .chart-all is
   `.js-only`, and `.js-only { display: none }` is a single class -- so a
   `display` here would sit later in the file at equal specificity and win,
   putting a dead "all" button on the page for anybody without JavaScript. It
   did exactly that once. .chart-all takes its display from the .js-only rules
   and from nowhere else; test_chart_all_takes_its_display_only_from_js_only
   fails if that changes. */
.chart-count, .chart-all {
  align-items: center; justify-content: center;
  min-height: 32px; padding: 0 .6rem; white-space: nowrap;
  background: var(--paper); border: 1px solid var(--line); border-radius: 999px;
  font-size: 13px; font-weight: 600; color: var(--ink-soft); cursor: pointer;
}
.chart-count { display: inline-flex; }
.chart-all { font-family: inherit; }
.chart-count:hover, .chart-all:hover {
  background: var(--accent-soft); color: var(--accent); border-color: var(--accent);
}
.chart-fold:checked ~ .chart-card .chart-count { background: var(--accent); border-color: var(--accent); color: #fff; }
.chart-fold:focus-visible ~ .chart-card .chart-count { outline: 2px solid var(--accent); outline-offset: 2px; }
.chart-all:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* -- generation colour ----------------------------------------------------
   Read off Person.generation, not off nesting depth: somebody scoped to their
   own branch sits at the top of the drawing and is still Gen 4. Every value is
   dark enough to carry white text and to pass AA as ink on --card, so the same
   colour can mark the card edge and fill a face. */
.chart-g1 { --chart-gen: #6d4c1f; }
.chart-g2 { --chart-gen: #a03d2f; }
.chart-g3 { --chart-gen: #3f6b47; }
.chart-g4 { --chart-gen: #2f6273; }
.chart-g5 { --chart-gen: #4f46e5; }
.chart-g6 { --chart-gen: #8a3d6b; }
.chart-g0 { --chart-gen: var(--ink-faint); }
.chart-card span.chart-face { background: var(--chart-gen); color: #fff; }

/* -- saying something back ----------------------------------------------- */
/* Reactions and replies. The half of the app that is not a broadcast.

   ⚠️ `.react-b` styles a bare <button>, so it declares everything -- font,
   background, border. The app's `.btn` is a different, taller control and this
   deliberately does not build on it: a row of four 44px-tall buttons under
   every photograph in a sixty-photograph album would be a wall of chrome with
   the pictures somewhere behind it. The tap target is met by padding plus the
   label rather than by a min-height, and is measured below. */
.react { display: flex; flex-wrap: wrap; gap: .35rem; margin: .5rem 0 0; }
.react-b {
  display: inline-flex; align-items: center; gap: .3rem;
  min-height: 36px; padding: .3rem .6rem;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--card); color: var(--ink-soft);
  font: inherit; font-size: 13px; font-weight: 600;
  cursor: pointer; line-height: 1;
}
.react-b:hover { background: #f7f6f2; color: var(--ink); }
.react-b:disabled { opacity: .55; cursor: default; }
/* What the family said, shown to somebody who cannot add to it -- office staff
   on the platform cookie, who have no account on this site. A span, not a
   button: a control that refuses on submit is worse than no control. */
.react-read { display: flex; flex-wrap: wrap; gap: .35rem; margin: .5rem 0 0; }
.react-read .react-b { cursor: default; }
/* The pressed state is warm rather than the app's indigo: this is the one place
   in the family's software that should feel like a person, not a control. It is
   also NOT colour alone -- the border weight and the background both change, so
   it is still legible to a reader who cannot separate the hues. */
.react-b.on {
  background: #fff1f2; border-color: #fecdd3; color: #9f1239;
}
.react-e { font-size: 15px; line-height: 1; }
.react-n { font-variant-numeric: tabular-nums; color: var(--ink-faint); }
.react-b.on .react-n { color: #9f1239; }

.talk { margin-top: .9rem; border-top: 1px solid var(--line); padding-top: .8rem; }
.talk-list { list-style: none; margin: 0 0 .8rem; padding: 0;
             display: flex; flex-direction: column; gap: .75rem; }
.talk-item { display: flex; gap: .6rem; align-items: flex-start; }
.talk-body { flex: 1; min-width: 0; }
.talk-who { font-size: 13px; font-weight: 600; }
.talk-when { font-weight: 500; color: var(--ink-faint); margin-left: .35rem; }
/* ⚠️ `pre-wrap` keeps the newlines a relative typed. `overflow-wrap: anywhere`
   is what stops a pasted URL pushing the whole card off the side of a phone --
   the same pair the announcement body carries, for the same reason. */
.talk-text {
  margin: .15rem 0 0; font-size: 15px; line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.talk-del { flex: 0 0 auto; }
.talk-form { display: flex; gap: .5rem; align-items: flex-start; }
/* (0,1,0) beats the bare `textarea { min-height: 120px }` at (0,0,1). A reply
   box the height of an essay tells somebody to write an essay. */
.talk-input { min-height: 44px; height: 44px; resize: vertical; font-size: 15px; }
.talk-form .btn { flex: 0 0 auto; }

/* A button that reads as a link. Used for destructive minor actions where a
   full button would out-weigh the thing it removes. */
.linkish {
  background: none; border: 0; padding: .25rem .3rem; margin: 0;
  font: inherit; font-size: 12px; color: var(--ink-faint);
  cursor: pointer; text-decoration: underline;
}
.linkish:hover { color: var(--bad); }

.avatar-sm { width: 30px; height: 30px; flex: 0 0 30px; font-size: 11px; }

/* Visually hidden, still read aloud. For a real <label> on a control whose
   purpose is obvious to the eye -- a placeholder is not a label, and it
   disappears the moment anybody starts typing. */
.vh {
  position: absolute !important; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* -- the birthday year --------------------------------------------------- */
/* A month card is drawn even when it is empty: on a calendar an empty February
   is information, and dropping it would reflow the grid every time somebody is
   added to the roster. */
.month { margin-bottom: 0; }
.month-now { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent-soft); }
.bday-list { list-style: none; margin: 0; padding: 0;
             display: flex; flex-direction: column; gap: .1rem; }
.bday { display: flex; gap: .6rem; align-items: baseline;
        padding: .32rem 0; font-size: 15px; }
/* Fixed width and tabular figures so the names line up down the column -- a
   ragged left edge is what makes a date list unscannable. */
.bday-day {
  flex: 0 0 1.6rem; text-align: right;
  font-variant-numeric: tabular-nums; font-weight: 700;
  font-size: 13px; color: var(--ink-faint);
}
/* Today's birthday, in the "coming up" list. Not colour alone -- the pill beside
   it already reads "Today" in words. */
.row-today { background: var(--accent-soft); }

/* -- the first-run welcome ----------------------------------------------- */
/* Warmer than a plain .card, because it is the first thing anybody sees on an
   app with nothing in it and its whole job is to read as an invitation rather
   than as an error. */
.welcome { background: linear-gradient(180deg, #fffdf8, var(--card)); }
.welcome-lede { font-size: 16px; line-height: 1.6; margin: 0 0 .6rem; max-width: 46ch; }

/* -- one photograph ------------------------------------------------------ */
/* The picture is the page. Everything is sized so that on a phone the image
   occupies the screen and the walk controls are under the thumb. */
.photo-stage {
  background: #171614; border-radius: var(--radius);
  display: grid; place-items: center; overflow: hidden;
  margin-bottom: .8rem;
}
/* `max-height: 78vh` so a portrait photograph -- which is most of what a phone
   takes -- still leaves the Previous/Next row on screen without scrolling.
   Without it a tall picture pushes the only navigation off the bottom. */
/* ⚠️ `height: auto` is what lets the width/height ATTRIBUTES on the element
   reserve the right aspect ratio without also fixing the rendered size -- the
   attributes give the browser the ratio, these rules give it the box. Dropping
   `height: auto` would render every photograph at its full pixel height. */
.photo-full { display: block; max-width: 100%; max-height: 78vh; height: auto; }
.photo-missing { color: #d9d6cf; font-size: 15px; padding: 3rem 1.2rem; text-align: center; }

.photo-walk {
  display: flex; align-items: center; justify-content: space-between;
  gap: .6rem; margin-bottom: 1rem;
}
.photo-count { font-size: 13px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
/* Rendered at both ends even where there is nowhere to go, so the two controls
   never move between photographs. A control that shifts position depending on
   where you are in the album has to be hunted for on every tap. */
.btn.is-off { opacity: .38; cursor: default; }

.photo-caption {
  font-size: 17px; line-height: 1.55; margin: 0 0 1rem;
  white-space: pre-wrap; overflow-wrap: anywhere;
}

.chips { display: flex; flex-wrap: wrap; gap: .45rem; }
.chip {
  display: inline-flex; align-items: center; gap: .45rem;
  min-height: 44px; padding: .3rem .75rem .3rem .3rem;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--card); color: var(--ink); font-size: 14px; font-weight: 600;
}
.chip:hover { background: #f7f6f2; text-decoration: none; }

/* -- previewing an email ------------------------------------------------ */
/* Sandboxed: the digest carries titles and names relatives typed, and an
   iframe with no allow-scripts and no allow-same-origin cannot reach this
   page's DOM, cookies or origin whatever ends up inside it. */
.mail-preview {
  width: 100%; height: 78vh; border: 1px solid var(--line);
  border-radius: var(--radius); background: #fff; display: block;
}
.chip-on { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }

/* -- phones ------------------------------------------------------------- */
@media (max-width: 780px) {
  .layout { flex-direction: column; }
  .sidebar {
    width: 100%; flex: none; height: auto; position: static;
    border-right: 0; border-bottom: 1px solid var(--line);
  }
  /* ⚠️ WRAP, DO NOT SCROLL. As a horizontal scroller this strip overflowed by
     61px at 390px and 91px at 360px, with no fade, no arrow and no shadow --
     so "Family" was 0px wide on the two commonest phone widths and an admin's
     "Settings & people" was entirely off-screen, on the one navigation the app
     has. Two rows of chips cost ~50px of height and cannot hide a destination. */
  .side-nav { flex-direction: row; flex-wrap: wrap; padding: .4rem .5rem; }
  .side-link { white-space: nowrap; }
  .side-section { display: none; }
  .side-foot { display: flex; gap: 1rem; align-items: center; padding: .6rem 1rem; }
  .side-who { margin: 0; flex: 1; }
  .main { padding: 1.1rem 1rem 3rem; }
  .tiles { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
  .grid-2 { grid-template-columns: 1fr; }
  /* More of the screen for the picture, and the walk stays reachable. */
  .photo-full { max-height: 68vh; }
  .photo-walk .btn { flex: 1; }
  .photo-count { order: 3; width: 100%; text-align: center; }
  /* ⚠️ `.vh`, NOT `display: none`. The emoji beside it is aria-hidden, so
     hiding the word outright leaves the button's accessible name as whatever
     the count says -- "Mazel tov" becomes the announced name "1" the moment
     anybody taps it, and "" before that. Visually hidden keeps the name and
     still gives the glyphs the width. */
  .react-l {
    position: absolute !important; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
  }
  /* ⚠️ The 44px floor this stylesheet declares at the top is about TOUCH, so
     it is enforced here rather than everywhere. `.btn-sm` is 36px and `.react-b`
     35px, which is comfortable under a mouse and under the mark for the thumb
     of a mother holding a baby -- and the two people this app exists for are 76
     and 66. On a laptop the compact size is kept, because a row of full-height
     buttons under every card is the reason .btn-sm exists at all. */
  .btn-sm { min-height: 44px; }
  .react-b { padding: .5rem .7rem; min-height: 44px; }
  /* Same bargain as `.btn-sm`: the fold control sits under every card on the
     chart, so it is compact under a mouse and full size under a thumb. */
  .chart-count, .chart-all { min-height: 44px; padding: 0 .8rem; }
  /* The one destructive control on every reply. Deliberately quiet -- a full
     button would out-weigh the line of text it removes -- but quiet is not the
     same as hard to hit, and 12px type in .25rem of padding renders ~35px. */
  .linkish { padding: .5rem .4rem; min-height: 44px; }
  .talk-form { flex-direction: column; }
  .talk-form .btn { align-self: flex-start; }
  /* Tighter nesting: four levels at 1.15rem costs 74px of a 375px screen. */
  .tree-list .tree-list { margin-left: .7rem; padding-left: .8rem; }
  .tree-list .tree-list > .tree-item::before { left: -.8rem; width: .55rem; }
  .tree-card { padding: .6rem .7rem; }
}
