/* ==========================================================================
   Family tree — visual layer
   ========================================================================== */

@property --s  { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --rx { syntax: '<angle>';  inherits: false; initial-value: 0deg; }
@property --ry { syntax: '<angle>';  inherits: false; initial-value: 0deg; }
@property --sweep { syntax: '<angle>'; inherits: false; initial-value: 0deg; }

:root {
  color-scheme: dark;

  --bg-0: #05060f;
  --bg-1: #0a0d1e;
  --ink: #eef1ff;
  --ink-dim: #9aa3c7;
  --ink-faint: #6b74a0;

  --brand: #7c5cff;
  --brand-2: #22d3ee;
  --accent: #ff5d8f;
  --male: #4cc4ff;
  --female: #ff7ab8;
  --neutral: #a78bfa;

  --glass: rgb(255 255 255 / .055);
  --glass-strong: rgb(255 255 255 / .09);
  --stroke: rgb(255 255 255 / .12);
  --stroke-soft: rgb(255 255 255 / .07);

  /* Cards carry no backdrop-filter (too many of them), so they need enough
     body of their own to stay legible over the grid and aurora. */
  --card: rgb(23 27 56 / .82);
  --card-strong: rgb(34 39 76 / .88);

  --edge-from: rgb(124 92 255 / .55);
  --edge-to: rgb(34 211 238 / .5);
  --edge-dim: rgb(255 255 255 / .07);

  --shadow: 0 18px 50px -12px rgb(0 0 0 / .75);
  --radius: 18px;
  --ease: cubic-bezier(.2, .8, .2, 1);

  --font: 'Inter var', 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}

:root[data-theme='light'] {
  color-scheme: light;
  --bg-0: #f4f5fb;
  --bg-1: #e9ebfa;
  --ink: #15173a;
  --ink-dim: #545a86;
  --ink-faint: #8a90b5;
  --glass: rgb(255 255 255 / .72);
  --glass-strong: rgb(255 255 255 / .9);
  --card: rgb(255 255 255 / .86);
  --card-strong: rgb(255 255 255 / .95);
  --stroke: rgb(20 22 60 / .1);
  --stroke-soft: rgb(20 22 60 / .06);
  --edge-from: rgb(96 62 240 / .55);
  --edge-to: rgb(6 160 190 / .5);
  --edge-dim: rgb(20 22 60 / .1);
  --shadow: 0 16px 40px -14px rgb(30 34 90 / .35);
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: var(--bg-0);
  color: var(--ink);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
}

button { font: inherit; color: inherit; cursor: pointer; }

/* ---------- background ---------------------------------------------------- */

.bg { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }

.bg__aurora {
  position: absolute; inset: -30%;
  background:
    radial-gradient(38% 44% at 18% 22%, color-mix(in oklab, var(--brand) 55%, transparent), transparent 70%),
    radial-gradient(34% 40% at 82% 18%, color-mix(in oklab, var(--brand-2) 45%, transparent), transparent 70%),
    radial-gradient(46% 46% at 60% 88%, color-mix(in oklab, var(--accent) 38%, transparent), transparent 72%),
    linear-gradient(180deg, var(--bg-1), var(--bg-0));
  filter: blur(40px) saturate(130%);
  opacity: .5;
  animation: drift 42s var(--ease) infinite alternate;
  /* Explicit so every engine keeps the blurred result on its own layer and the
     42s drift is a pure composite. Without it, re-running blur(40px) over a
     160%-of-viewport box is a per-frame cost. One layer, not eighty. */
  will-change: transform;
}
:root[data-theme='light'] .bg__aurora { opacity: .38; }

@keyframes drift {
  0%   { transform: translate3d(-2%, -1%, 0) scale(1.02) rotate(0deg); }
  50%  { transform: translate3d(3%, 2%, 0) scale(1.08) rotate(3deg); }
  100% { transform: translate3d(-1%, 3%, 0) scale(1.04) rotate(-2deg); }
}

.bg__grid {
  position: absolute; inset: 0;
  background-image:
    linear-gradient(var(--stroke-soft) 1px, transparent 1px),
    linear-gradient(90deg, var(--stroke-soft) 1px, transparent 1px);
  background-size: 64px 64px;
  mask-image: radial-gradient(70% 70% at 50% 45%, #000 20%, transparent 100%);
  opacity: .6;
}

.bg__dust { position: absolute; inset: 0; width: 100%; height: 100%; }

.bg__spotlight {
  position: absolute; inset: 0;
  background: radial-gradient(420px 420px at var(--mx, 50%) var(--my, 40%),
    color-mix(in oklab, var(--brand) 22%, transparent), transparent 65%);
  opacity: .5;
  transition: opacity .4s;
}

/* ---------- shell --------------------------------------------------------- */

.app {
  position: relative; z-index: 1;
  height: 100%;
  display: grid;
  grid-template-rows: auto 1fr;
  /* minmax(0, 1fr), not the implicit auto column: an auto column is sized from
     its items' content, so one long title in family.json stretched the column
     (and with it the whole header) past the viewport, no matter how much the
     flex items inside were allowed to shrink. */
  grid-template-columns: minmax(0, 1fr);
  opacity: 0;
  transition: opacity .6s var(--ease);
}
.app.is-ready { opacity: 1; }

.topbar {
  display: flex; align-items: center; gap: 22px;
  min-width: 0; /* grid items default to min-width: auto, which resists shrinking */
  padding: 14px 20px;
  background: linear-gradient(180deg, color-mix(in oklab, var(--bg-0) 78%, transparent), transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--stroke-soft);
}

.brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
/* The title/subtitle wrapper is itself a flex item; without this its default
   min-width: auto keeps it at full text width and the h1 ellipsis never fires. */
.brand > div { min-width: 0; }
.brand__mark {
  display: grid; place-items: center;
  flex: none; /* or the flex row squashes it out of square when space runs short */
  width: 40px; height: 40px; border-radius: 13px;
  background: linear-gradient(140deg, var(--brand), var(--brand-2));
  box-shadow: 0 8px 22px -8px var(--brand);
  color: #fff;
}
.brand__mark svg { width: 22px; height: 22px; }
.brand h1 {
  margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -.02em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.brand__sub { margin: 1px 0 0; font-size: 12px; color: var(--ink-faint); }

.search {
  position: relative;
  display: flex; align-items: center; gap: 8px;
  flex: 1 1 260px; max-width: 420px;
  padding: 9px 14px;
  border-radius: 999px;
  background: var(--glass);
  border: 1px solid var(--stroke);
  backdrop-filter: blur(12px);
  transition: border-color .25s, box-shadow .25s, background .25s;
}
.search:focus-within {
  border-color: color-mix(in oklab, var(--brand) 60%, transparent);
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--brand) 18%, transparent);
}
.search__icon { width: 16px; height: 16px; color: var(--ink-faint); flex: none; }
.search input {
  flex: 1; min-width: 0;
  background: none; border: 0; outline: none;
  color: var(--ink); font-size: 14px;
}
.search input::placeholder { color: var(--ink-faint); }
.search input::-webkit-search-cancel-button { filter: invert(.6); }
.search__count { font-size: 11px; color: var(--ink-faint); white-space: nowrap; }

.stats { display: flex; gap: 16px; margin-left: auto; font-size: 12px; color: var(--ink-faint); }
.stats b { color: var(--ink); font-weight: 620; }
@media (max-width: 860px) { .stats, .brand__sub { display: none; } }

/* Phones: brand and search can't share a row — at 375px the search field ran
   ~58px off the right edge, and the shell clips overflow so it was simply gone.
   Let it wrap onto its own full-width row instead. */
@media (max-width: 640px) {
  .topbar { flex-wrap: wrap; gap: 10px 14px; padding: 10px 14px; }
  .brand { flex: 1 1 auto; }
  .brand h1 { font-size: 16px; }
  .search {
    order: 2;
    flex: 1 0 100%;
    max-width: none;
    padding: 8px 12px;
  }
  /* 16px is the threshold below which iOS Safari zooms the page on focus. */
  .search input { font-size: 16px; }
}

.hint {
  position: fixed; left: 50%; bottom: 14px; transform: translateX(-50%);
  margin: 0; font-size: 11.5px; color: var(--ink-faint);
  background: var(--glass); border: 1px solid var(--stroke-soft);
  padding: 6px 14px; border-radius: 999px; backdrop-filter: blur(10px);
  pointer-events: none; white-space: nowrap;
}
.hint kbd {
  font: inherit; padding: 1px 5px; border-radius: 5px;
  background: var(--glass-strong); border: 1px solid var(--stroke);
}
@media (max-width: 720px) { .hint { display: none; } }

/* ---------- viewport / stage ---------------------------------------------- */

.viewport {
  position: relative;
  overflow: hidden;
  cursor: grab;
  touch-action: none;
}
.viewport.is-grabbing { cursor: grabbing; }

.stage {
  position: absolute; top: 0; left: 0;
  transform-origin: 0 0;
  will-change: transform;
}

.links { position: absolute; top: 0; left: 0; overflow: visible; pointer-events: none; }
.cards { position: relative; perspective: 1200px; }

/* ---------- generation bands ---------------------------------------------- */

.gen-band {
  position: absolute; left: 0; top: var(--y); width: var(--w);
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--stroke) 12%, var(--stroke) 88%, transparent);
  pointer-events: none;
}
.gen-band__label {
  position: absolute; left: 26px; top: -8px;
  font-size: 10.5px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--ink-faint); opacity: .6;
  padding: 0 12px 0 0;
  background: linear-gradient(90deg, var(--bg-0) 70%, transparent);
}

/* ---------- edges ---------------------------------------------------------- */

.edge {
  fill: none;
  stroke: url(#edge-gradient);
  stroke-width: 2;
  stroke-linecap: round;
  /* Keep lines a constant width on screen. Without this they inherit the
     stage's zoom: at fit-to-screen on a wide tree the scale drops to ~0.18 and
     a 2px dashed line becomes a 0.36px stroke with 1.3px dashes — invisible. */
  vector-effect: non-scaling-stroke;
  transition: stroke .4s, opacity .4s, stroke-width .3s;
}
.edge--descent {
  stroke-dasharray: 7 9;
  /* No animation at rest, on purpose: stroke-dashoffset can't be composited, so
     animating every descent line repaints the whole (tree-sized, ~7000px wide)
     SVG layer every frame even when the page is idle. The flow now runs only on
     a highlighted lineage — a handful of paths, while the user is looking. */
}
.edge--marriage { stroke: color-mix(in oklab, var(--accent) 70%, transparent); stroke-width: 2.4; }
.edge--marriage.is-divorced { stroke-dasharray: 4 6; opacity: .55; }
.knot {
  fill: color-mix(in oklab, var(--accent) 85%, white);
  filter: url(#edge-glow);
  /* Same idea as non-scaling-stroke, but radius has to be divided by hand. */
  r: calc(4px / var(--zoom, 1));
  transition: opacity .4s;
}

@keyframes flow { to { stroke-dashoffset: -32; } }

.links.has-lineage .edge:not(.in-lineage),
.links.has-lineage .knot:not(.in-lineage) { stroke: var(--edge-dim); opacity: .18; }
/* Emphasis by width and colour, never by filter: filterUnits defaults to
   objectBoundingBox, which is degenerate for a straight vertical link (only
   child under their parents) or a horizontal marriage link — and the browser
   drops such an element entirely rather than skipping the effect. */
.links.has-lineage .edge--descent.in-lineage {
  stroke: color-mix(in oklab, var(--brand-2) 62%, white);
  stroke-width: 3;
  animation: flow 1.6s linear infinite;
}
.links.has-lineage .edge--marriage.in-lineage {
  stroke: color-mix(in oklab, var(--accent) 80%, white);
  stroke-width: 3;
}

/* ---------- cards ---------------------------------------------------------- */

.card {
  position: absolute; top: 0; left: 0;
  width: var(--w); height: var(--h);
  transform:
    translate3d(var(--x), var(--y), 0)
    rotateX(var(--rx)) rotateY(var(--ry)) scale(var(--s));
  /* The tilt gets its depth from `perspective` on .cards, which applies to this
     element's own rotation. No descendant is 3D-transformed, so preserve-3d here
     would only force ~80 separate 3D rendering contexts the compositor then has
     to sort and cannot squash together. Deliberately flat.
     `contain` stops at layout+style: paint containment would clip the pseudo
     elements that draw past the border box — the "you" badge (top: -9px) and
     the 12px glow around the ::after gender bar. */
  contain: layout style;
  border-radius: var(--radius);
  background:
    linear-gradient(150deg, var(--card-strong), var(--card));
  border: 1px solid var(--stroke);
  box-shadow: var(--shadow);
  /* Deliberately no backdrop-filter here. Cards sit inside .stage, which is
     transformed on every pan/zoom frame, so a per-card backdrop blur means
     re-blurring ~80 backdrops per frame while dragging. The gradient below is
     opaque enough to read as glass; only the selected card gets the real blur. */
  cursor: pointer;
  outline: none;
  animation: pop .75s var(--ease) backwards;
  animation-delay: calc(var(--i) * 26ms);
  transition: --s .35s var(--ease), --rx .25s, --ry .25s,
              border-color .3s, box-shadow .35s, opacity .4s, filter .4s;
}

@keyframes pop {
  from { opacity: 0; --s: .82; filter: blur(8px); }
  to   { opacity: 1; --s: 1;   filter: blur(0); }
}

.card:hover, .card:focus-visible { --s: 1.05; z-index: 5; border-color: color-mix(in oklab, var(--brand) 55%, var(--stroke)); }
.card:focus-visible { box-shadow: var(--shadow), 0 0 0 3px color-mix(in oklab, var(--brand-2) 60%, transparent); }

/* rotating conic border on hover / selection */
.card__aura {
  position: absolute; inset: -1px; border-radius: inherit; padding: 1px;
  background: conic-gradient(from var(--sweep),
    transparent 0deg, var(--brand) 60deg, var(--brand-2) 130deg, transparent 200deg);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  opacity: 0;
  transition: opacity .35s;
}
.card:hover .card__aura, .card.is-selected .card__aura {
  opacity: 1;
  animation: sweep 3.4s linear infinite;
}
@keyframes sweep { to { --sweep: 360deg; } }

.card__body {
  position: relative; z-index: 1;
  display: flex; align-items: center; gap: 12px;
  height: 100%; padding: 14px;
}
.card__text { min-width: 0; }
.card__name {
  margin: 0; font-size: 14.5px; font-weight: 620; letter-spacing: -.015em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.card__dates { margin: 3px 0 0; font-size: 12px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.card__role {
  margin: 4px 0 0; font-size: 11px; color: var(--ink-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* glare that follows the pointer */
.card__spark {
  position: absolute; inset: 0; border-radius: inherit;
  background: radial-gradient(200px 140px at var(--gx, 50%) var(--gy, 0%),
    rgb(255 255 255 / .14), transparent 60%);
  opacity: 0; transition: opacity .3s; pointer-events: none;
}
.card:hover .card__spark { opacity: 1; }

.avatar {
  position: relative; flex: none;
  display: grid; place-items: center;
  width: 54px; height: 54px; border-radius: 15px;
  font-weight: 650; font-size: 17px; color: #fff;
  background: linear-gradient(140deg, hsl(var(--h1, 260) 85% 62%), hsl(var(--h2, 300) 85% 55%));
  box-shadow: 0 8px 20px -10px hsl(var(--h1, 260) 85% 50%), inset 0 1px 0 rgb(255 255 255 / .3);
  overflow: hidden;
}
.avatar__img { width: 100%; height: 100%; object-fit: cover; }
.avatar--lg { width: 76px; height: 76px; border-radius: 22px; font-size: 24px; }

.card--m  { --ring: var(--male); }
.card--f  { --ring: var(--female); }
.card--x  { --ring: var(--neutral); }
.card::after {
  content: ''; position: absolute; left: 14px; bottom: 0; width: 54px; height: 2px;
  border-radius: 2px; background: var(--ring); opacity: .75;
  box-shadow: 0 0 12px var(--ring);
}

.card.is-deceased .avatar { filter: saturate(.35); }

/* Unknown ancestors: present only to keep a sibling group together. */
.card.is-placeholder {
  background: none;
  border-style: dashed;
  border-color: var(--stroke);
  box-shadow: none;
  backdrop-filter: none;
  opacity: .55;
}
.card.is-placeholder .avatar {
  background: none;
  border: 1px dashed var(--stroke);
  box-shadow: none;
  color: var(--ink-faint);
}
.card.is-placeholder::after { display: none; }
.card.is-placeholder .card__name { color: var(--ink-dim); font-weight: 500; }

.card.is-self { border-color: color-mix(in oklab, var(--brand-2) 60%, var(--stroke)); }
.card.is-self::before {
  content: 'you'; position: absolute; top: -9px; right: 12px; z-index: 2;
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px; color: #04121a;
  background: linear-gradient(90deg, var(--brand-2), #9df);
}

.card.is-selected { --s: 1.06; z-index: 6; backdrop-filter: blur(16px) saturate(130%); }

/* dimming modes */
.cards.has-lineage .card:not(.in-lineage) { opacity: .22; filter: grayscale(.7) blur(1px); }
.cards.has-search  .card:not(.is-match)   { opacity: .2; filter: grayscale(.8); }
.cards.has-search  .card.is-match         { --s: 1.06; }
.cards.has-search  .card.is-match .card__aura { opacity: 1; animation: sweep 3.4s linear infinite; }

/* ---------- toolbar -------------------------------------------------------- */

.toolbar {
  position: fixed; right: 20px; bottom: 20px; z-index: 20;
  display: flex; align-items: center; gap: 6px;
  padding: 7px; border-radius: 999px;
  background: var(--glass); border: 1px solid var(--stroke);
  backdrop-filter: blur(16px); box-shadow: var(--shadow);
}
.toolbar button {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: 50%;
  background: transparent; border: 0; color: var(--ink-dim);
  font-size: 18px; line-height: 1;
  transition: background .25s, color .25s, transform .2s var(--ease);
}
.toolbar button:hover { background: var(--glass-strong); color: var(--ink); transform: translateY(-2px); }
.toolbar button svg { width: 18px; height: 18px; }
.toolbar button.is-on { color: var(--brand-2); background: color-mix(in oklab, var(--brand-2) 16%, transparent); }
.toolbar__sep { width: 1px; height: 22px; background: var(--stroke); }

/* ---------- detail panel --------------------------------------------------- */

.panel {
  position: fixed; top: 76px; right: 16px; bottom: 16px; z-index: 15;
  width: min(360px, calc(100vw - 32px));
  padding: 22px;
  border-radius: 26px;
  background: color-mix(in oklab, var(--bg-1) 82%, transparent);
  border: 1px solid var(--stroke);
  backdrop-filter: blur(22px) saturate(140%);
  box-shadow: var(--shadow);
  overflow-y: auto; overscroll-behavior: contain;
  transform: translateX(calc(100% + 24px));
  opacity: 0; visibility: hidden;
  transition: transform .5s var(--ease), opacity .35s, visibility .5s;
}
.panel.is-open { transform: none; opacity: 1; visibility: visible; }
.panel::-webkit-scrollbar { width: 8px; }
.panel::-webkit-scrollbar-thumb { background: var(--stroke); border-radius: 4px; }

.panel__close {
  position: absolute; top: 14px; right: 14px;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--glass); border: 1px solid var(--stroke-soft);
  color: var(--ink-dim); font-size: 18px; line-height: 1;
}
.panel__close:hover { color: var(--ink); background: var(--glass-strong); }

.panel__hero { display: flex; gap: 14px; align-items: center; margin-bottom: 18px; padding-right: 34px; }
.panel__name { margin: 0; font-size: 20px; font-weight: 660; letter-spacing: -.02em; }
.panel__life { margin: 4px 0 0; font-size: 13px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.panel__bio {
  margin: 16px 0; padding: 12px 14px; font-size: 13px; line-height: 1.55; color: var(--ink-dim);
  border-left: 2px solid color-mix(in oklab, var(--brand) 70%, transparent);
  background: var(--glass); border-radius: 0 12px 12px 0;
}

.tags { display: flex; gap: 6px; margin-top: 8px; }
.tag {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 999px;
  background: color-mix(in oklab, var(--brand) 22%, transparent);
  color: color-mix(in oklab, var(--brand) 30%, var(--ink));
}

.facts { display: grid; gap: 1px; margin: 0 0 4px; border-radius: 14px; overflow: hidden; background: var(--stroke-soft); }
.facts > div { display: flex; justify-content: space-between; gap: 12px; padding: 9px 13px; background: var(--glass); }
.facts dt { font-size: 12px; color: var(--ink-faint); }
.facts dd { margin: 0; font-size: 12.5px; text-align: right; }

.rel { margin-top: 18px; }
.rel h4 {
  margin: 0 0 8px; font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-faint); font-weight: 600;
}
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 11px; border-radius: 999px; font-size: 12.5px;
  background: var(--glass); border: 1px solid var(--stroke-soft);
  transition: transform .2s var(--ease), border-color .25s, background .25s;
}
.chip:hover { transform: translateY(-2px); background: var(--glass-strong); border-color: color-mix(in oklab, var(--brand) 50%, transparent); }
.chip__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--neutral); }
.chip__dot--m { background: var(--male); }
.chip__dot--f { background: var(--female); }
.chip__years { font-size: 11px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }

@media (max-width: 720px) {
  .panel { top: auto; left: 8px; right: 8px; bottom: 8px; width: auto; max-height: 62vh; border-radius: 22px; }
  .panel { transform: translateY(calc(100% + 24px)); }
}

/* ---------- splash / misc --------------------------------------------------- */

.splash {
  position: fixed; inset: 0; z-index: 40;
  display: grid; place-content: center; justify-items: center; gap: 22px;
  background: var(--bg-0);
  transition: opacity .5s var(--ease), visibility .5s;
}
.splash.is-hidden { opacity: 0; visibility: hidden; pointer-events: none; }
/* `visibility: hidden` alone doesn't stop the animation — the conic gradient
   keeps being repainted for the whole session behind an invisible layer. */
.splash.is-hidden .splash__ring { animation: none; }
.splash__ring {
  width: 54px; height: 54px; border-radius: 50%;
  background: conic-gradient(from 0deg, transparent, var(--brand), var(--brand-2), transparent);
  mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 0);
  animation: spin 1.1s linear infinite;
}
@keyframes spin { to { transform: rotate(1turn); } }
.splash__msg { max-width: 460px; text-align: center; font-size: 14px; color: var(--ink-dim); line-height: 1.6; }
.splash__msg strong { display: block; color: var(--ink); margin-bottom: 8px; font-size: 16px; }
.splash__msg pre, .splash__msg code {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 12.5px;
  background: var(--glass); border: 1px solid var(--stroke-soft);
  padding: 2px 6px; border-radius: 6px; color: var(--brand-2);
}
.splash__msg pre { padding: 10px 14px; margin-top: 12px; }

.ripple {
  position: fixed; z-index: 30; width: 12px; height: 12px; margin: -6px 0 0 -6px;
  border-radius: 50%; border: 2px solid var(--brand-2);
  pointer-events: none; animation: ripple .7s var(--ease) forwards;
}
@keyframes ripple {
  to { transform: scale(9); opacity: 0; border-width: .5px; }
}

body.is-dropping::after {
  content: 'Drop family.json to load';
  position: fixed; inset: 12px; z-index: 50;
  display: grid; place-content: center;
  border: 2px dashed var(--brand); border-radius: 26px;
  background: color-mix(in oklab, var(--bg-0) 72%, transparent);
  color: var(--ink); font-size: 16px; backdrop-filter: blur(6px);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}
