/*
 * Pedigree Editor — web.
 *
 * One stylesheet, no framework, no build step. Consistent with how the rest of this deploys:
 * copy the file and it works.
 *
 * The palette is the desktop app's own — the navy chrome and pale blue panels of MainForm —
 * so the two clients read as one product rather than two. Semantic colour (good / warning /
 * critical) is kept separate from the accent, because a COI above the breed median is a
 * different kind of statement from a link.
 */

:root {
  --paper: #eef1f7;
  --panel: #ffffff;
  --panel-2: #f6f8fc;
  --ink: #101c33;
  --ink-2: #37456a;
  --ink-3: #67739a;
  --rule: #d6dcec;
  --rule-2: #e6eaf4;
  --accent: #2c4f9e;
  --accent-soft: #dfe7f8;
  --accent-ink: #ffffff;

  --good: #1f6a49;
  --good-bg: #dff0e6;
  --warn: #8a6410;
  --warn-bg: #f6ecd4;
  --bad: #993633;
  --bad-bg: #f8e2e0;

  --male: #2c5fa8;
  --female: #8a3f6d;

  --shadow: 0 1px 2px rgba(16, 28, 51, .06), 0 8px 24px -18px rgba(16, 28, 51, .5);
  --radius: 5px;

  --font: "Segoe UI", system-ui, -apple-system, sans-serif;
  --mono: "Cascadia Mono", Consolas, ui-monospace, monospace;

  /*
   * How wide the application frame is allowed to get.
   *
   * This is a tool rather than a document: a five-generation pedigree is 32 boxes across and a
   * mate-finder table is eight columns, so a comfortable reading measure spends the widest part
   * of the screen on empty margins and makes the one view that needs room scroll sideways
   * instead.
   *
   * 2200px is wide enough for a five-generation chart to sit flat, and stops short of the point
   * where a table on an ultra-wide monitor turns into a row of numbers with a metre of gap
   * between the name and the figure that belongs to it.
   *
   * Running prose is protected separately by .reading, which stays near 68 characters. That is
   * the split that matters: the frame follows the window, the sentences do not.
   *
   * Set to 100% for edge-to-edge on any monitor.
   */
  --shell-max: 2200px;

  /*
   * The phone breakpoint.
   *
   * 640px, and one number rather than three. It used to be 720px for two masthead rules and
   * 900px for the theme buttons, which meant three different ideas about where "small" starts
   * and a band between them where the page was neither one thing nor the other.
   *
   * 640 rather than 768 because a portrait tablet is a screen the desktop layout serves well —
   * .grid-2 gives two columns, the tables fit, and the tab strip shows most of itself. Sending
   * an iPad the phone chrome would be a downgrade, not an improvement.
   *
   * CSS custom properties cannot be used in a media query, so the number itself is still
   * written out at each @media below. This note is the single place it is explained; if it
   * moves, it moves everywhere, and this comment moves with it.
   */

  /*
   * The notch, the home indicator, and the rounded corners.
   *
   * Zero on every device that has none of them, so these are safe to add unconditionally. They
   * matter to exactly two things — the bottom navigation bar and the sticky save bar — both of
   * which would otherwise sit underneath the home indicator on a modern iPhone.
   */
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);

  /* How tall the phone tab bar is, wherever something has to clear it. */
  --tabbar-h: 56px;

  /*
   * The narrowest a pedigree box may be drawn.
   *
   * A property rather than a literal because the phone needs a different answer: 160 is
   * comfortable, but eight generations of it is 1,322px of chart, and narrowing it is what
   * makes the full-site view usable on a handset instead of merely available.
   */
  --ped-node-min: 160px;
}

/*
 * Dark palette.
 *
 * Three states, not two, because "follow the operating system" is a real preference and the
 * commonest one. So:
 *
 *   no data-theme attribute   follow the OS      (the default)
 *   data-theme="light"        force light        (wins over a dark OS)
 *   data-theme="dark"         force dark         (wins over a light OS)
 *
 * The media query is guarded with :not([data-theme="light"]) so an explicit light choice beats
 * a dark OS; the attribute rule below repeats the palette so an explicit dark choice beats a
 * light OS. The two blocks are deliberately adjacent and MUST be kept in step — a value changed
 * in one and not the other shows up only for the half of people in that state.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #0c1220;
    --panel: #131b2c;
    --panel-2: #182136;
    --ink: #e4eaf7;
    --ink-2: #b3bfda;
    --ink-3: #8290b3;
    --rule: #26314c;
    --rule-2: #1d2740;
    --accent: #7fa5ee;
    --accent-soft: #1c2b4c;
    --accent-ink: #0c1220;
    --good: #6cc79a;  --good-bg: #12301f;
    --warn: #d8ac52;  --warn-bg: #332705;
    --bad:  #e79a95;  --bad-bg:  #361715;
    --male: #7fa5ee;  --female: #d98cba;
    --shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px -18px rgba(0,0,0,.9);
  }
}

:root[data-theme="dark"] {
  --paper: #0c1220;
  --panel: #131b2c;
  --panel-2: #182136;
  --ink: #e4eaf7;
  --ink-2: #b3bfda;
  --ink-3: #8290b3;
  --rule: #26314c;
  --rule-2: #1d2740;
  --accent: #7fa5ee;
  --accent-soft: #1c2b4c;
  --accent-ink: #0c1220;
  --good: #6cc79a;  --good-bg: #12301f;
  --warn: #d8ac52;  --warn-bg: #332705;
  --bad:  #e79a95;  --bad-bg:  #361715;
  --male: #7fa5ee;  --female: #d98cba;
  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px -18px rgba(0,0,0,.9);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { margin: 0; line-height: 1.25; text-wrap: balance; }
h1 { font-size: 1.6rem; }
h2 { font-size: 1.2rem; }
h3 { font-size: 1.02rem; }
p { margin: 0; }
a { color: var(--accent); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip {
  position: absolute; left: -999px; top: 0; z-index: 100;
  background: var(--accent); color: var(--accent-ink); padding: .6rem 1rem;
}
.skip:focus { left: 0; }

.muted { color: var(--ink-3); }
.mono { font-family: var(--mono); }
.tabular { font-variant-numeric: tabular-nums; }

/* ── Masthead ──────────────────────────────────────────────────────────── */

.masthead { background: var(--panel); border-bottom: 1px solid var(--rule); }

.masthead-bar {
  display: flex; align-items: center; gap: 1rem;
  padding: .7rem clamp(.75rem, 3vw, 1.75rem);
  max-width: var(--shell-max); margin: 0 auto;
}

.brand { display: flex; align-items: center; gap: .6rem; text-decoration: none; color: var(--ink); }
.brand-mark {
  display: grid; place-items: center; width: 30px; height: 30px; flex: none;
  background: var(--accent); color: var(--accent-ink);
  border-radius: 4px; font-weight: 700; font-size: .78rem; letter-spacing: .02em;
}
.brand-name { font-weight: 600; white-space: nowrap; }

.search { position: relative; flex: 1; max-width: 520px; margin-inline: auto; }
.search input {
  width: 100%; padding: .5rem .75rem; font: inherit;
  color: var(--ink); background: var(--panel-2);
  border: 1px solid var(--rule); border-radius: var(--radius);
}
.search input::placeholder { color: var(--ink-3); }

/* Wraps a form input so the panel hangs off the box itself, not off the label and hint too. */
.typeahead-wrap { position: relative; }

.typeahead {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 40;
  background: var(--panel); border: 1px solid var(--rule);
  border-radius: var(--radius); box-shadow: var(--shadow);
  max-height: 380px; overflow-y: auto;

  /* Reaching the end of the list must not start scrolling the page behind it. On a touchscreen
     that hand-off is the difference between picking a dog and losing your place. */
  overscroll-behavior: contain;
}
.typeahead a, .typeahead button {
  display: flex; justify-content: space-between; gap: 1rem; align-items: baseline;
  width: 100%; text-align: left; padding: .5rem .75rem;
  font: inherit; text-decoration: none; color: var(--ink);
  background: none; border: 0; border-bottom: 1px solid var(--rule-2);
  border-radius: 0; cursor: pointer;
}
.typeahead a:last-child, .typeahead button:last-child { border-bottom: 0; }
.typeahead a:hover, .typeahead a.is-highlighted,
.typeahead button:hover, .typeahead button.is-highlighted { background: var(--accent-soft); }
.typeahead .meta { color: var(--ink-3); font-size: .8rem; white-space: nowrap; }
.typeahead .empty { padding: .75rem; color: var(--ink-3); font-size: .875rem; }

.masthead-user { display: flex; align-items: center; gap: .75rem; white-space: nowrap; }
.who {
  font-size: .875rem; color: var(--ink-2); text-decoration: none;
  padding: .25rem .5rem; border-radius: var(--radius);
  border: 1px solid transparent; white-space: nowrap;
}
.who:hover { color: var(--ink); background: var(--panel-2); border-color: var(--rule); }

/*
 * The same link, reduced to initials.
 *
 * The name is too wide for a phone masthead, but hiding it outright — which is what this file
 * used to do below 720px — took away the only route to the account page, and with it profile,
 * badges, password, settings and signing out. There is no other link to /account anywhere in
 * the app, so on a handset the whole of it was unreachable.
 *
 * Hidden above the breakpoint, where .who says the name in full.
 */
.avatar {
  display: none; flex: none;
  width: 34px; height: 34px; border-radius: 50%;
  place-items: center; text-decoration: none;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid var(--rule);
  font-size: .78rem; font-weight: 700; letter-spacing: .02em;
}
.avatar:hover { filter: brightness(1.05); }

.link-button {
  background: none; border: 0; padding: 0; font: inherit;
  color: var(--accent); cursor: pointer; text-decoration: underline;
}
.inline { display: inline; }

.tabs {
  display: flex; gap: .25rem; overflow-x: auto;
  padding: 0 clamp(.75rem, 3vw, 1.75rem);
  max-width: var(--shell-max); margin: 0 auto;
}
.tabs a {
  padding: .5rem .8rem; text-decoration: none; color: var(--ink-2);
  border-bottom: 2px solid transparent; white-space: nowrap; font-size: .9rem;
}
.tabs a:hover { color: var(--ink); }
.tabs a.is-active { color: var(--ink); border-bottom-color: var(--accent); font-weight: 600; }
.tabs-action { margin-left: auto; color: var(--accent) !important; }

/* ── Theme control ─────────────────────────────────────────────────────── */

/*
 * Hidden unless JavaScript is running. The control cannot do anything without it, and a dead
 * set of buttons is worse than no buttons: the page still renders correctly in whichever theme
 * the operating system asked for.
 */
.no-js .theme { display: none; }

.theme {
  display: flex; gap: 1px; flex: none;
  background: var(--rule); border: 1px solid var(--rule);
  border-radius: var(--radius); overflow: hidden;
}
.theme button {
  border: 0; margin: 0; cursor: pointer; font: inherit;
  padding: .3rem .45rem; line-height: 1;
  background: var(--panel); color: var(--ink-3);
}
.theme button:hover { color: var(--ink); background: var(--panel-2); }

/* Pressed state carries the choice in weight and colour, not colour alone. */
.theme button[aria-pressed="true"] {
  background: var(--accent); color: var(--accent-ink); font-weight: 600;
}

/* ── Page frame ────────────────────────────────────────────────────────── */

main { max-width: var(--shell-max); margin: 0 auto; padding: clamp(1rem, 3vw, 1.75rem); }

.flashes { max-width: var(--shell-max); margin: 0 auto; padding: .75rem clamp(.75rem, 3vw, 1.75rem) 0; }
.flash {
  padding: .6rem .9rem; border-radius: var(--radius);
  border: 1px solid var(--rule); background: var(--panel); margin-bottom: .5rem;
}
.flash-good { background: var(--good-bg); border-color: var(--good); color: var(--good); }
.flash-bad  { background: var(--bad-bg);  border-color: var(--bad);  color: var(--bad); }
.flash-warn { background: var(--warn-bg); border-color: var(--warn); color: var(--warn); }

.page-head {
  display: flex; flex-wrap: wrap; gap: .75rem 1.25rem;
  align-items: baseline; justify-content: space-between; margin-bottom: 1.25rem;
}
.eyebrow {
  font-family: var(--mono); font-size: .68rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-3);
}

.card {
  background: var(--panel); border: 1px solid var(--rule);
  border-radius: var(--radius); box-shadow: var(--shadow); padding: 1.1rem 1.25rem;
}
.card + .card { margin-top: 1rem; }
.card-title { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: .75rem; }

.grid { display: grid; gap: 1rem; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.split-main { display: grid; gap: 1rem; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 1100px) { .split-main { grid-template-columns: 320px minmax(0, 1fr); } }

/* ── Buttons and forms ─────────────────────────────────────────────────── */

.btn {
  display: inline-block; padding: .5rem 1rem; border-radius: var(--radius);
  border: 1px solid var(--rule); background: var(--panel-2); color: var(--ink);
  font: inherit; text-decoration: none; cursor: pointer;
}
.btn:hover { background: var(--accent-soft); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn-primary:hover { filter: brightness(1.08); background: var(--accent); }
.btn-small { padding: .3rem .6rem; font-size: .85rem; }

/*
 * The favorite toggle.
 *
 * A button rather than an icon alone, and it keeps its label in both states: a lone star that
 * changes from hollow to filled tells you what happened only if you were watching it. The
 * colour carries the state as well, but never on its own — the glyph and the wording both
 * change too, so this reads the same to anyone who cannot tell the two shades apart.
 */
.star { display: inline-flex; align-items: center; gap: .4rem; }
.star .star-glyph { font-size: 1.05em; line-height: 1; color: var(--ink-3); }
.star:hover .star-glyph { color: var(--warn); }
.star.is-on { border-color: var(--warn); background: var(--warn-bg); color: var(--warn); }
.star.is-on:hover { background: var(--warn-bg); filter: brightness(0.97); }
.star.is-on .star-glyph { color: var(--warn); }

/* The full favorites list: a name, and the way to take it off the list. */
.fav-list { list-style: none; padding: 0; margin: 0; display: grid; }
.fav-list li {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding: .55rem 0; border-bottom: 1px solid var(--rule-2);
}
.fav-list li:last-child { border-bottom: 0; }
.fav-list .link-button { font-size: .82rem; color: var(--ink-3); }
.fav-list .link-button:hover { color: var(--bad); }

.field { display: grid; gap: .3rem; margin-bottom: .9rem; }
.field label { font-size: .82rem; color: var(--ink-2); font-weight: 600; }
.field input, .field select, .field textarea {
  padding: .5rem .65rem; font: inherit; color: var(--ink);
  background: var(--panel-2); border: 1px solid var(--rule); border-radius: var(--radius);
  width: 100%;
}
.field textarea { min-height: 6rem; resize: vertical; }
.field .hint { font-size: .8rem; color: var(--ink-3); }
.field-row { display: grid; gap: 0 1rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }

/* A tick box is not a text box: the .field rule above stretches an input across the whole
   card, which turns a checkbox into a wide grey slab with nothing in it. */
.field-check { display: flex; align-items: center; gap: .55rem; margin-bottom: .7rem; }
.field-check input[type="checkbox"] { flex: none; width: 1.05rem; height: 1.05rem; margin: 0; accent-color: var(--accent); }
.field-check label { font-size: .9rem; font-weight: 500; color: var(--ink); margin: 0; cursor: pointer; }

.toolbar { display: flex; flex-wrap: wrap; gap: .6rem; align-items: end; margin-bottom: 1rem; }
.toolbar .field { margin-bottom: 0; }

/* ── Chips and meters ──────────────────────────────────────────────────── */

.chip {
  display: inline-block; font-family: var(--mono); font-size: .66rem;
  letter-spacing: .06em; text-transform: uppercase; font-weight: 600;
  padding: .15rem .4rem; border-radius: 3px; background: var(--accent-soft); color: var(--accent);
  white-space: nowrap;
}
.chip-good  { background: var(--good-bg); color: var(--good); }
.chip-warn  { background: var(--warn-bg); color: var(--warn); }
.chip-bad   { background: var(--bad-bg);  color: var(--bad); }
.chip-quiet { background: var(--rule-2);  color: var(--ink-3); }

/*
 * Chips that can be taken away again: the titles on the edit page.
 *
 * Each chip is its own form, because removing one is its own request, so the wrapper does the
 * spacing and the form only has to hold the chip and its button on one line. The button takes
 * the same grey-turning-red treatment as the one on the favorites list — a removal should look
 * the same everywhere it appears.
 */
.chip-list { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }
.chip-list form { display: inline-flex; align-items: center; gap: .25rem; }
.chip-list .link-button { font-size: .95rem; line-height: 1; color: var(--ink-3); text-decoration: none; }
.chip-list .link-button:hover { color: var(--bad); }

.is-male   { color: var(--male); }
.is-female { color: var(--female); }

.stat { display: grid; gap: .15rem; }
.stat-label { font-family: var(--mono); font-size: .66rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); }
.stat-value { font-size: 1.9rem; font-variant-numeric: tabular-nums; line-height: 1.1; }
.stat-note { font-size: .82rem; color: var(--ink-3); }

.meter { height: 8px; border-radius: 4px; background: var(--rule-2); overflow: hidden; }
.meter > span { display: block; height: 100%; background: var(--accent); }
.meter-good > span { background: var(--good); }
.meter-warn > span { background: var(--warn); }
.meter-bad  > span { background: var(--bad); }

/* ── Badges ────────────────────────────────────────────────────────────── */

.badge-grid {
  display: grid; gap: .75rem;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}

.badge {
  border: 1px solid var(--rule); border-radius: var(--radius);
  padding: .75rem .85rem; background: var(--panel-2);
  display: flex; flex-direction: column; gap: .4rem;
}
.badge-top { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; }
.badge h3 { font-size: .95rem; }
.badge-desc { font-size: .85rem; color: var(--ink-2); }
.badge-note { font-size: .78rem; color: var(--ink-3); }

/* Earned reads as earned without relying on colour: it gains a border and full-strength ink. */
.badge.is-earned { border-color: var(--good); background: var(--good-bg); }
.badge.is-earned h3 { color: var(--good); }

.badge.is-locked { opacity: .78; }

/* The one the desktop counts locally. Not earned, not unearned — unknowable from here. */
.badge.is-unknown { border-style: dashed; opacity: .78; }

/* ── Tables ────────────────────────────────────────────────────────────── */

.tablewrap { overflow-x: auto; border: 1px solid var(--rule); border-radius: var(--radius); background: var(--panel); }
table { border-collapse: collapse; width: 100%; font-size: .875rem; }
th, td { text-align: left; padding: .5rem .7rem; border-bottom: 1px solid var(--rule-2); vertical-align: top; }
/*
 * No position:sticky here, though there was for a long time.
 *
 * `position: sticky; top: 0` sticks to the nearest scrolling ancestor, which is .tablewrap —
 * and .tablewrap only scrolls horizontally. Vertical scrolling happens on the page, so the
 * header never stuck to anything and never has. Making it work would mean giving .tablewrap a
 * max-height, turning every table in the app into a nested scroll region: a real change in
 * behaviour, on desktop, in service of something nobody has ever seen working.
 *
 * On a phone the .cards-sm layout below repeats the column name on every value, which is the
 * problem solved properly rather than approximately.
 */
thead th {
  font-family: var(--mono); font-size: .66rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-3); font-weight: 600; background: var(--panel-2);
  border-bottom: 1px solid var(--rule);
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--panel-2); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.is-best { background: var(--good-bg) !important; font-weight: 600; }

/* ── Pedigree chart ────────────────────────────────────────────────────── */

.pedigree { overflow-x: auto; padding-bottom: .5rem; }
.pedigree-grid { display: grid; gap: 6px; min-width: max-content; align-items: stretch; }

.ped-node {
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  border: 1px solid var(--rule); border-radius: 4px; background: var(--panel);
  padding: .4rem .55rem; text-decoration: none; color: var(--ink);
  min-width: var(--ped-node-min); position: relative;
}
.ped-node:hover { border-color: var(--accent); background: var(--accent-soft); }
.ped-node .n { font-weight: 600; font-size: .85rem; line-height: 1.25; }
.ped-node .m { font-size: .72rem; color: var(--ink-3); }
.ped-node.is-male   { border-left: 3px solid var(--male); }
.ped-node.is-female { border-left: 3px solid var(--female); }

/* An unrecorded parent is a gap in the record, not a dog. It reads as an invitation to fill
   it in rather than as a box that failed to load. */
.ped-empty {
  display: grid; place-items: center; min-width: var(--ped-node-min);
  border: 1px dashed var(--rule); border-radius: 4px;
  color: var(--ink-3); font-size: .78rem; background: transparent;
  text-decoration: none;
}
.ped-empty:hover { border-color: var(--accent); color: var(--accent); }

/* A dog appearing more than once in the same pedigree is the thing COI measures. Marking it
   makes the arithmetic visible rather than something the reader has to take on trust. */
.ped-node.is-repeat { box-shadow: inset 0 0 0 2px var(--warn); }
.ped-node .repeat-flag {
  position: absolute; top: -6px; right: -6px;
  background: var(--warn); color: var(--warn-bg);
  font-size: .6rem; font-weight: 700; padding: 0 .25rem; border-radius: 3px;
}

/* ── Photos ────────────────────────────────────────────────────────────── */

.photo-grid { display: grid; gap: .75rem; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
.photo-grid img { width: 100%; height: 180px; object-fit: cover; border-radius: var(--radius); border: 1px solid var(--rule); background: var(--panel-2); }

/*
 * The controls under each photograph.
 *
 * Each is its own form, because each is its own request, so the row does the spacing and a form
 * only has to sit inline within it. Muted until hovered: one of these rows sits under every
 * tile, and at full strength they would compete with the photographs they act on.
 */
.photo-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .55rem; margin-top: .45rem; }
.photo-actions form { display: inline-flex; }
.photo-actions .link-button { font-size: .78rem; color: var(--ink-3); text-decoration: none; }
.photo-actions .link-button:hover { color: var(--ink); text-decoration: underline; }
.photo-actions .link-button.is-turn { font-size: 1.05rem; line-height: 1; }
.photo-actions .link-button.is-danger:hover { color: var(--bad); }

/*
 * The cropper.
 *
 * The stage is sized by the picture inside it — inline-block around an image with a max height —
 * so the shade and the box can be positioned against the picture itself rather than against a
 * container that is wider than it. Anything else and the shade covers the letterboxing too, and
 * a rectangle dragged near the edge lands somewhere other than where it was drawn.
 */
.cropper-stage {
  position: relative; display: inline-block; line-height: 0;
  max-width: 100%; border-radius: var(--radius); overflow: hidden;
  background: var(--panel-2); border: 1px solid var(--rule);
}
.cropper-stage img {
  display: block; max-width: 100%; max-height: 70vh; width: auto; height: auto;
  -webkit-user-select: none; user-select: none; -webkit-user-drag: none;
}

/* Only once the script is running: with no script there is nothing to drag, and a crosshair
   over a picture that does not respond to one is a promise the page cannot keep. */
.cropper-stage.is-ready img { cursor: crosshair; }

/*
 * The chosen rectangle, and the dimming of everything outside it.
 *
 * One element does both: a box-shadow spread wider than the picture darkens everything around
 * this rectangle, and the stage clips it back to the picture's edges. Two elements — a shade and
 * a box — cannot do it without one covering the other. Nothing dims until a rectangle exists,
 * which leaves the photograph at full strength while it is still being chosen.
 */
.cropper-box {
  position: absolute; pointer-events: none;
  outline: 1px dashed #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .55), 0 0 0 100vmax rgba(0, 0, 0, .5);
}

.cropper-hint { font-size: .85rem; margin: .6rem 0 0; }
.cropper-fields {
  display: grid; gap: 0 1rem; margin-top: .9rem;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
}

/* ── Definition lists ──────────────────────────────────────────────────── */

.facts { display: grid; gap: .5rem 1rem; grid-template-columns: auto 1fr; margin: 0; font-size: .9rem; }
.facts dt { color: var(--ink-3); }
.facts dd { margin: 0; }

/* ── Prose blocks ──────────────────────────────────────────────────────── */

.reading { max-width: 68ch; }
.reading + .reading { margin-top: .6rem; }
.caveats { display: grid; gap: .5rem; margin-top: 1rem; }
.caveat {
  font-size: .85rem; color: var(--ink-2); padding-left: .8rem;
  border-left: 2px solid var(--warn); max-width: 72ch;
}

.empty-state { text-align: center; padding: 3rem 1rem; color: var(--ink-3); }
.empty-state h2 { color: var(--ink); margin-bottom: .5rem; }

.site-footer {
  max-width: var(--shell-max); margin: 2rem auto 0; padding: 1.25rem clamp(.75rem, 3vw, 1.75rem) 2.5rem;
  border-top: 1px solid var(--rule); color: var(--ink-3); font-size: .82rem;
  display: flex; flex-wrap: wrap; gap: .35rem 1.5rem; justify-content: space-between;
}

/* ── Sign-in ───────────────────────────────────────────────────────────── */

/*
 * 100dvh, not 100vh.
 *
 * On a mobile browser 100vh means the viewport with the URL bar hidden — a height the page does
 * not actually have when it first loads. The sign-in card was pushed below centre and the whole
 * page jumped as the bar collapsed. The dynamic unit is the one that tracks what is really on
 * screen.
 */
.signin-shell { min-height: 100dvh; display: grid; place-items: center; padding: 1.5rem; }
.signin { width: min(26rem, 100%); }
.signin-head { background: var(--accent); color: var(--accent-ink); padding: 1.5rem 1.5rem 1.25rem; border-radius: var(--radius) var(--radius) 0 0; }
.signin-head h1 { font-size: 1.5rem; }
.signin-head p { opacity: .85; font-size: .9rem; margin-top: .25rem; }
.signin-body { background: var(--panel); border: 1px solid var(--rule); border-top: 0; border-radius: 0 0 var(--radius) var(--radius); padding: 1.5rem; box-shadow: var(--shadow); }
.signin-error { background: var(--bad-bg); border: 1px solid var(--bad); color: var(--bad); padding: .75rem .9rem; border-radius: var(--radius); margin-bottom: 1rem; font-size: .9rem; }

/* ══════════════════════════════════════════════════════════════════════════
 * THE PHONE LAYER
 *
 * Everything from here down exists to make this read as an application on a handset. Two rules
 * govern all of it:
 *
 *   1. It is additive. No rule above this line was written to serve the phone, and none of it
 *      changes when these are switched on. The desktop layout is the one that was already here.
 *
 *   2. The person outranks the breakpoint. A media query is a guess about what somebody wants
 *      from the width of their window; it is a good guess, and it is still a guess. So the
 *      whole phone layer is scoped :root:not([data-view="full"]), and one link turns it off.
 *
 * The three-state pattern the theme control uses is deliberately NOT repeated here. Theme has
 * light / dark / follow-the-system because all three are real preferences. View has only two —
 * follow the breakpoint, or force the desktop — because forcing the phone layout onto a wide
 * window is a thing nobody has asked for, and supporting it would mean writing every rule below
 * a second time outside the media query, with the same drift hazard the theme comment warns
 * about. Narrowing the window already does it.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── The view switch ───────────────────────────────────────────────────── */

/*
 * Shown whenever the desktop layout has been forced, at any width.
 *
 * Not hidden on wide screens, and that is on purpose: in full view the page is rendered at a
 * fixed 1,100px viewport, so a `max-width` query cannot tell a phone from a monitor — the phone
 * reports 1,100 too. Since the link out only ever appears in the phone sheet, a cookie set at
 * all was set deliberately, and this strip is the only way to clear it.
 */
.viewbar { display: none; }

:root[data-view="full"] .viewbar {
  display: flex; flex-wrap: wrap; gap: .25rem 1rem;
  align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent);
  border-bottom: 1px solid var(--rule);
  padding: calc(.4rem + var(--safe-t)) .75rem .4rem;
  font-size: .85rem; text-align: center;
}
.viewbar a { color: var(--accent); font-weight: 600; }

/* ── The bottom tab bar ────────────────────────────────────────────────── */

/*
 * Hidden by default and revealed only on a phone, so the desktop never renders it at all.
 *
 * Five destinations rather than nine. The four that did not make it — Breeders, Countries,
 * Compare, Simulate — plus the theme control and the account live in the sheet behind "More".
 * Compare and Simulate are wide tables and multi-field forms: desktop work, and a poor use of
 * one of five slots a thumb can reach.
 */
.tabbar { display: none; }

.tabbar-item {
  display: grid; gap: 2px; justify-items: center; align-content: center;
  padding: .35rem .15rem; min-height: 48px;
  text-decoration: none; color: var(--ink-3);
  font-size: .62rem; letter-spacing: .01em; text-align: center;
  background: none; border: 0; font-family: inherit; cursor: pointer;
}
.tabbar-item .g { font-size: 1.15rem; line-height: 1; }

/* State in weight and colour together, never colour alone — as with .star and .theme. */
.tabbar-item.is-active { color: var(--accent); font-weight: 700; }

/*
 * The sheet.
 *
 * <details> rather than <dialog>, because a dialog cannot be opened without a script and every
 * page in this app works with scripting off. This one gets Escape, a toggle and a shown/hidden
 * state from the browser for nothing.
 */
.more-sheet { position: static; }
.more-sheet > summary {
  list-style: none; cursor: pointer;
  display: grid; gap: 2px; justify-items: center; align-content: center;
  padding: .35rem .15rem; min-height: 48px;
  color: var(--ink-3); font-size: .62rem; text-align: center;
}
.more-sheet > summary::-webkit-details-marker { display: none; }
.more-sheet > summary .g { font-size: 1.15rem; line-height: 1; }
.more-sheet[open] > summary,
.more-sheet > summary.is-active { color: var(--accent); font-weight: 700; }

.sheet-panel {
  position: fixed; left: 0; right: 0;
  bottom: calc(var(--tabbar-h) + var(--safe-b));
  z-index: 60; max-height: 70dvh; overflow-y: auto; overscroll-behavior: contain;
  background: var(--panel); border-top: 1px solid var(--rule);
  border-radius: 12px 12px 0 0; box-shadow: 0 -8px 28px -14px rgba(16, 28, 51, .55);
  padding: .5rem .5rem .75rem;
  display: grid; gap: 1px;
}
.sheet-panel a,
.sheet-panel button {
  display: flex; align-items: center; gap: .6rem;
  min-height: 46px; padding: .5rem .75rem;
  color: var(--ink); text-decoration: none; text-align: left;
  background: none; border: 0; border-radius: var(--radius);
  font: inherit; cursor: pointer; width: 100%;
}
.sheet-panel a:hover, .sheet-panel button:hover { background: var(--panel-2); }
.sheet-panel a.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.sheet-panel form { margin: 0; }
.sheet-panel .sheet-rule { height: 1px; background: var(--rule-2); margin: .35rem .25rem; }
.sheet-panel .sheet-label {
  font-family: var(--mono); font-size: .62rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3); padding: .5rem .75rem .25rem;
}
.sheet-panel .theme { margin: 0 .75rem .35rem; width: max-content; }
.sheet-panel .theme button { min-height: 40px; min-width: 46px; }

/* The link out to the desktop layout. Marked as leaving, because it is. */
.sheet-panel .sheet-full { color: var(--accent); font-weight: 600; }

/* ── Add a dog, on a phone ─────────────────────────────────────────────── */

/*
 * The one action worth a permanent button. It sits above the bar rather than in it: the bar is
 * for places, and this is a thing you do.
 */
.fab { display: none; }

/* ── Filters that fold away ────────────────────────────────────────────── */

/*
 * The same .toolbar, wrapped so a phone can put it away.
 *
 * Above the breakpoint the summary is hidden and the contents are forced open, so the toolbar
 * looks and behaves exactly as it always has. Two rules are needed for that, not one: browsers
 * are moving the closed state from the children to a ::details-content pseudo-element, and a
 * selector one of them does not recognise invalidates the whole block it sits in — hence the
 * separate rule below rather than a longer selector list.
 */
.filters > summary { display: none; }
.filters > .toolbar { display: flex; }

.filters::details-content { content-visibility: visible; block-size: auto; }

/* ── Save, within reach ────────────────────────────────────────────────── */

/*
 * A plain block in the flow at desktop width — this is where the button already was.
 */
.save-bar { display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; }

/* ── The pedigree, focused ─────────────────────────────────────────────── */

/*
 * The phone answer to a chart that is 1,322px wide at eight generations.
 *
 * Both layouts are rendered on every request and CSS shows one. That is deliberate: the server
 * cannot know the width of the window, the rows are already loaded either way, and it means the
 * full-site switch above needs no special case here at all — it is the same two rules.
 *
 * Hidden by default; the phone block at the end reveals it.
 */
.pedigree-focus { display: none; }

.ped-trail {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .2rem .1rem;
  font-size: .78rem; color: var(--ink-3); margin-bottom: .9rem;
  padding-bottom: .6rem; border-bottom: 1px solid var(--rule-2);
}
.ped-trail a { color: var(--ink-3); text-decoration: none; }
.ped-trail a:hover { color: var(--accent); text-decoration: underline; }
.ped-trail .sep { color: var(--ink-3); padding: 0 .15rem; }
.ped-trail .here { color: var(--ink); font-weight: 600; }

.ped-gen {
  font-family: var(--mono); font-size: .62rem; letter-spacing: .11em; text-transform: uppercase;
  color: var(--ink-3); margin: 1.1rem 0 .45rem;
}

.ped-stack { display: grid; gap: .5rem; }

.ped-card {
  display: grid; gap: .15rem;
  padding: .65rem .8rem; min-height: 56px; align-content: center;
  border: 1px solid var(--rule); border-radius: var(--radius);
  background: var(--panel); color: var(--ink); text-decoration: none;
  position: relative;
}
.ped-card:hover { border-color: var(--accent); background: var(--accent-soft); }
.ped-card .n { font-weight: 600; font-size: .95rem; line-height: 1.3; }
.ped-card .m { font-size: .78rem; color: var(--ink-3); }
.ped-card.is-male { border-left: 3px solid var(--male); }
.ped-card.is-female { border-left: 3px solid var(--female); }

/* The dog the view is rooted on. Not a link — you are already here. */
.ped-card.is-focus {
  background: var(--panel-2); border-color: var(--rule);
  box-shadow: inset 0 0 0 1px var(--rule-2);
}
.ped-card.is-focus .n { font-size: 1.05rem; }

/* The chevron says this one re-roots the view rather than leaving the page. */
.ped-card .go { position: absolute; right: .7rem; top: 50%; transform: translateY(-50%); color: var(--ink-3); }
.ped-card:hover .go { color: var(--accent); }
.ped-card .n, .ped-card .m { padding-right: 1rem; }

/* Same meaning as the chart's is-repeat, said in words because there is room for words here. */
.ped-card.is-repeat { box-shadow: inset 0 0 0 2px var(--warn); }

.ped-card.is-empty {
  border-style: dashed; color: var(--ink-3); background: transparent;
  justify-items: start; font-size: .85rem;
}
.ped-card.is-empty:hover { border-color: var(--accent); color: var(--accent); }

/* An ancestor at the edge of what was fetched: it opens its own page rather than re-rooting. */
.ped-card.is-leaving .go { transform: translateY(-50%) rotate(-45deg); }

/* ══════════════════════════════════════════════════════════════════════════
 * Below 640px, unless the desktop layout has been asked for by name.
 * ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 640px) {

  /* ── Reach ───────────────────────────────────────────────────────────── */

  /*
   * 16px is not a preference, it is the threshold Safari on iOS checks before deciding to zoom
   * the page. Under it, tapping any field on the dog edit form leaves the page magnified and
   * scrolled sideways, with no way back short of pinching.
   */
  :root:not([data-view="full"]) .field input,
  :root:not([data-view="full"]) .field select,
  :root:not([data-view="full"]) .field textarea,
  :root:not([data-view="full"]) .search input,
  :root:not([data-view="full"]) input[type="text"],
  :root:not([data-view="full"]) input[type="search"],
  :root:not([data-view="full"]) input[type="number"],
  :root:not([data-view="full"]) input[type="date"],
  :root:not([data-view="full"]) input[type="password"],
  :root:not([data-view="full"]) select,
  :root:not([data-view="full"]) textarea { font-size: 16px; }

  /* A 44px floor, in the one place the file used to do the opposite. */
  :root:not([data-view="full"]) .btn,
  :root:not([data-view="full"]) .theme button,
  :root:not([data-view="full"]) .tabs a {
    min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
  }
  :root:not([data-view="full"]) .btn-small { min-height: 44px; padding: .3rem .75rem; }
  :root:not([data-view="full"]) .theme button { min-width: 44px; padding: .3rem .5rem; }
  :root:not([data-view="full"]) .fav-list .link-button,
  :root:not([data-view="full"]) .chip-list .link-button {
    min-height: 44px; display: inline-flex; align-items: center; padding: 0 .4rem;
  }

  /* ── Masthead ────────────────────────────────────────────────────────── */

  :root:not([data-view="full"]) .brand-name { display: none; }
  :root:not([data-view="full"]) .who { display: none; }
  :root:not([data-view="full"]) .avatar { display: grid; }

  /* The theme control moves into the sheet, where the targets can be full size. */
  :root:not([data-view="full"]) .masthead-bar > .masthead-user > .theme { display: none; }

  :root:not([data-view="full"]) .masthead-bar { gap: .5rem; padding: .5rem .75rem; }
  :root:not([data-view="full"]) .search { margin-inline: 0; }

  /* ── Navigation ──────────────────────────────────────────────────────── */

  /*
   * The masthead strip only.
   *
   * .tabs is used twice: the primary navigation in the masthead, which the bottom bar replaces,
   * and the dog page's own section nav — Pedigree, Dog information, Photos, Offspring — which
   * nothing replaces. Hiding both, as this rule first did, made three of the four tabs on the
   * busiest page in the app unreachable on a phone. Same shape of bug as the account link.
   */
  :root:not([data-view="full"]) .masthead .tabs { display: none; }

  /*
   * A page's own tabs stay, and wrap rather than scroll.
   *
   * "Dog information" alone is most of a 390px row, so as a single line these four would be a
   * horizontal scroller with no affordance — which is finding three of this very audit. Wrapping
   * to two rows shows all of them at once and costs a few pixels of height.
   */
  :root:not([data-view="full"]) main .tabs {
    flex-wrap: wrap;
    overflow-x: visible;
    gap: 0 .1rem;
    padding-inline: 0;
  }
  :root:not([data-view="full"]) main .tabs a { font-size: .85rem; padding-inline: .55rem; }

  /* Pushed to its own row rather than squeezed against the last tab by margin-left:auto. */
  :root:not([data-view="full"]) main .tabs .tabs-action { margin-left: 0; }

  :root:not([data-view="full"]) .tabbar {
    display: grid; grid-template-columns: repeat(5, 1fr);
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
    background: var(--panel); border-top: 1px solid var(--rule);
    padding-bottom: var(--safe-b);
    padding-left: var(--safe-l); padding-right: var(--safe-r);
  }

  /* Room for the bar, so the footer and the last row of a table are not underneath it. */
  :root:not([data-view="full"]) body {
    padding-bottom: calc(var(--tabbar-h) + var(--safe-b));
  }

  :root:not([data-view="full"]) .fab {
    display: grid; place-items: center;
    position: fixed; z-index: 45;
    right: calc(1rem + var(--safe-r));
    bottom: calc(var(--tabbar-h) + var(--safe-b) + 1rem);
    width: 52px; height: 52px; border-radius: 50%;
    background: var(--accent); color: var(--accent-ink);
    border: 0; text-decoration: none;
    font-size: 1.6rem; line-height: 1;
    box-shadow: 0 6px 18px -6px rgba(16, 28, 51, .6);
  }
  :root:not([data-view="full"]) .fab:hover { filter: brightness(1.08); }

  /* ── Page frame ──────────────────────────────────────────────────────── */

  :root:not([data-view="full"]) main { padding: .9rem .75rem; }
  :root:not([data-view="full"]) .card { padding: .9rem .85rem; }
  :root:not([data-view="full"]) .page-head { gap: .6rem; margin-bottom: .9rem; }
  :root:not([data-view="full"]) h1 { font-size: 1.35rem; }

  /* ── The pedigree ────────────────────────────────────────────────────── */

  :root:not([data-view="full"]) .pedigree { display: none; }
  :root:not([data-view="full"]) .pedigree-focus { display: block; }

  /* Narrower boxes, for the grid the full-site view still shows. */
  :root:not([data-view="full"]) { --ped-node-min: 120px; }

  /* ── Tables ──────────────────────────────────────────────────────────── */

  /*
   * A table of one record per row becomes a stack of cards; a matrix does not, because its
   * meaning IS the alignment of rows against columns. Two techniques, opted into per page.
   */
  :root:not([data-view="full"]) .cards-sm { border: 0; background: none; overflow-x: visible; }
  :root:not([data-view="full"]) .cards-sm table { min-width: 0; }

  /* Off-screen rather than display:none — a screen reader still announces the column names. */
  :root:not([data-view="full"]) .cards-sm thead {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap;
  }

  :root:not([data-view="full"]) .cards-sm table,
  :root:not([data-view="full"]) .cards-sm tbody,
  :root:not([data-view="full"]) .cards-sm tr,
  :root:not([data-view="full"]) .cards-sm td { display: block; }

  :root:not([data-view="full"]) .cards-sm tbody tr {
    border: 1px solid var(--rule); border-radius: var(--radius);
    background: var(--panel); padding: .7rem .85rem; margin-bottom: .6rem;
  }
  :root:not([data-view="full"]) .cards-sm tbody tr:hover { background: var(--panel); }
  :root:not([data-view="full"]) .cards-sm tbody tr:last-child { margin-bottom: 0; }

  :root:not([data-view="full"]) .cards-sm td {
    border-bottom: 0; padding: .2rem 0;
    display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: .75rem; align-items: baseline;
  }
  :root:not([data-view="full"]) .cards-sm td::before {
    content: attr(data-label);
    font-family: var(--mono); font-size: .6rem; letter-spacing: .08em; text-transform: uppercase;
    color: var(--ink-3);
  }
  :root:not([data-view="full"]) .cards-sm td.num { text-align: left; }

  /* A cell with nothing to label — an actions column — spans instead of leaving a gap. */
  :root:not([data-view="full"]) .cards-sm td:not([data-label]),
  :root:not([data-view="full"]) .cards-sm td[data-label=""] { display: block; }
  :root:not([data-view="full"]) .cards-sm td[data-label=""]::before { content: none; }

  /* The first cell is the name. It is the heading of the card, not a labelled field. */
  :root:not([data-view="full"]) .cards-sm tbody td:first-child {
    display: block; font-size: 1rem; font-weight: 600;
    padding: 0 0 .4rem; margin-bottom: .4rem;
    border-bottom: 1px solid var(--rule-2);
  }
  :root:not([data-view="full"]) .cards-sm tbody td:first-child::before { content: none; }

  /*
   * Except where the first cell is a form field rather than a name.
   *
   * The puppies table is a row of inputs, and a text box promoted to a card heading loses the
   * label that says what it is for — which is the one thing a field cannot do without. These
   * keep the label and the two-column layout all the way down.
   */
  :root:not([data-view="full"]) .cards-form tbody td:first-child {
    display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: .75rem;
    font-size: inherit; font-weight: inherit;
    padding: .2rem 0; margin-bottom: 0; border-bottom: 0;
  }
  :root:not([data-view="full"]) .cards-form tbody td:first-child::before { content: attr(data-label); }
  :root:not([data-view="full"]) .cards-form td input,
  :root:not([data-view="full"]) .cards-form td select { width: 100%; }

  /*
   * Everything else keeps its columns and scrolls, with the row label pinned so the numbers
   * always have something to belong to, and an edge shadow so an off-screen column is visible
   * as a possibility rather than a secret.
   */
  :root:not([data-view="full"]) .tablewrap:not(.cards-sm) {
    background-color: var(--panel);
    background-image:
      linear-gradient(to left, var(--panel), rgba(16, 28, 51, 0)),
      radial-gradient(farthest-side at 100% 50%, rgba(16, 28, 51, .18), rgba(16, 28, 51, 0));
    background-position: right center, right center;
    background-size: 30px 100%, 12px 100%;
    background-repeat: no-repeat;
    background-attachment: local, scroll;
  }

  :root:not([data-view="full"]) .sticky-first th:first-child,
  :root:not([data-view="full"]) .sticky-first td:first-child {
    position: sticky; left: 0; z-index: 1;
    background: var(--panel); border-right: 1px solid var(--rule);
  }
  :root:not([data-view="full"]) .sticky-first thead th:first-child {
    background: var(--panel-2); z-index: 2;
  }
  :root:not([data-view="full"]) .sticky-first tbody tr:hover td:first-child { background: var(--panel-2); }

  /* ── Forms ───────────────────────────────────────────────────────────── */

  :root:not([data-view="full"]) .filters > summary {
    display: flex; align-items: center; gap: .4rem;
    min-height: 44px; padding: .4rem .75rem; margin-bottom: 1rem;
    border: 1px solid var(--rule); border-radius: var(--radius);
    background: var(--panel-2); color: var(--ink-2);
    font-size: .88rem; font-weight: 600; cursor: pointer;
    list-style: none; width: max-content;
  }
  :root:not([data-view="full"]) .filters > summary::-webkit-details-marker { display: none; }
  :root:not([data-view="full"]) .filters > summary::after { content: "\25BE"; color: var(--ink-3); }
  :root:not([data-view="full"]) .filters[open] > summary::after { content: "\25B4"; }
  :root:not([data-view="full"]) .filters[open] > summary { margin-bottom: .6rem; }
  :root:not([data-view="full"]) .filters > .toolbar { display: flex; }

  /*
   * Save, within reach of the field that was changed.
   *
   * The dog form is 335 lines and the litter form 284: on a desktop the button is a glance
   * away, on a phone it was several screens down past the titles, registrations and sources.
   */
  :root:not([data-view="full"]) .save-bar {
    position: sticky; z-index: 20;
    bottom: calc(var(--tabbar-h) + var(--safe-b));
    margin: 1rem -.75rem 0; padding: .7rem .75rem;
    background: var(--panel); border-top: 1px solid var(--rule);
    box-shadow: 0 -6px 20px -14px rgba(16, 28, 51, .5);
  }
  :root:not([data-view="full"]) .save-bar .btn { flex: 1 1 auto; }

  /* ── Photos ──────────────────────────────────────────────────────────── */

  :root:not([data-view="full"]) .photo-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  :root:not([data-view="full"]) .photo-grid img { height: 130px; }

  /*
   * The controls stay a row of small links under the photograph.
   *
   * This started as a disclosure — one "Manage" control per tile opening full-width 44px rows —
   * on the reasoning that three link-sized targets a few pixels apart is how a photograph gets
   * deleted when a quarter turn was meant. In practice that traded a real problem for a worse
   * one: the closed control still occupied a row on every tile, and opened it was taller than
   * the photograph it belonged to, so a screen of six pictures became a screen of menus.
   *
   * So the targets grow instead of moving. Each control gets a 30px box and real space around
   * it — comfortably past the 24px WCAG 2.5.8 minimum, short of the 44px that caused this —
   * and the two turn arrows become squares rather than glyphs on a text baseline, which is
   * what made them the hardest things on the page to hit.
   */
  :root:not([data-view="full"]) .photo-actions {
    gap: .1rem .45rem; margin-top: .3rem; font-size: .78rem;
  }
  :root:not([data-view="full"]) .photo-actions .link-button {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 30px; padding: 0 .25rem;
  }
  :root:not([data-view="full"]) .photo-actions .link-button.is-turn {
    min-width: 30px; font-size: 1.15rem; padding: 0;
  }

  /* ── Odds and ends ───────────────────────────────────────────────────── */

  /* Most of the screen once the keyboard is up, which left the field itself invisible. */
  :root:not([data-view="full"]) .typeahead { max-height: min(380px, 45dvh); }

  :root:not([data-view="full"]) .stat-value { font-size: 1.5rem; }
  :root:not([data-view="full"]) .site-footer { padding-bottom: 1.5rem; }
}

@media print {
  .masthead, .tabs, .tabbar, .viewbar, .fab, .site-footer, .toolbar, .flashes { display: none !important; }
  body { background: #fff; padding-bottom: 0 !important; }
  .card { box-shadow: none; border-color: #ccc; }
  .pedigree { overflow: visible; }

  /* The chart, not the walk-through — paper has the room the phone did not. */
  .pedigree-focus, .save-bar { display: none !important; }
  .pedigree { display: block !important; }
}

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