/* ══════════════════════════ TREE NODES & LINES ════════════════════════════
   Restyled to match Mockup/nafhan_family_tree_ULTIMATE_9.html's glass-card
   look. IMPORTANT: a node's on-screen POSITION (its `transform`) is set
   per-frame by JS (tree-view.js's setTransform/animateTo), not by CSS — an
   inline style always wins over a stylesheet rule for the same property, so
   nothing here may set `transform` directly or the JS positioning breaks.
   The mockup's hover/focus "lift" is instead built from the standalone
   `translate`/`scale` CSS properties, which compose independently of
   `transform` (spec-defined render order: translate → rotate → scale →
   transform) — that's what lets this file add the mockup's lift-on-hover
   purely in CSS without ever touching the JS that owns positioning.        */

.node {
  position: absolute;
  top: 0; left: 0;
  width: var(--node-w);
  min-height: var(--node-h);
  padding: 24px 14px 14px;
  border-radius: 24px;
  /* Fully opaque — no translucency anywhere on a card. Cards sit over a
     patterned canvas with connector lines running behind them, so any
     see-through at all let those lines ghost through the artwork. This
     also drops the per-card backdrop-filter, which was the single most
     expensive thing in the tree to paint (one live blur per visible card,
     re-rasterised on every pan frame). */
  background: var(--surface);
  border: 1.5px solid var(--line-soft);
  box-shadow: var(--sh-2), var(--node-inset);
  text-align: center;
  cursor: pointer;
  translate: 0 0;
  scale: 1;
  transition: opacity var(--t-move) var(--ease-out),
              border-color var(--t-fast),
              box-shadow var(--t-base) var(--ease-out),
              background var(--t-fast),
              translate var(--t-base) var(--ease-out),
              scale var(--t-base) var(--ease-out);
  will-change: transform, translate, scale;
  user-select: none;
  -webkit-user-select: none;
  z-index: var(--z-node);
}
/* Hover only for devices that actually have hover (a mouse) — on touch,
   many mobile browsers treat the first tap on an element with :hover CSS as
   "simulate hover" rather than a real click, so the tap that should select
   the card just previews it instead, and a SECOND tap is needed to actually
   select it. Scoping hover to real pointers means a touch tap goes straight
   to the node's click handler, first time, every time. */
@media (hover: hover) and (pointer: fine) {
  /* No translate/scale here any more. The card sits at the exact point
     tree-view.js's layout math put it, with a connector line drawn to meet
     its edge exactly — lifting the card -6px on hover (as the mockup does)
     moved the card away from that fixed line endpoint without moving the
     line, so the stub connecting it to its parent/child visibly detached
     from the card's edge on every hover. The photo still zooms slightly
     (self-contained inside the card, nothing external anchors to it), and
     the deepened shadow + brighter border still read as "hovered" without
     repositioning the card at all. */
  /* border-color left out of the general .node:hover rule below on
     purpose — a hover pseudo-class beats a plain rank class at equal
     selector-count specificity regardless of source order, so it would
     otherwise flatten every ranked card's colour back to neutral grey the
     moment you hovered it. Rank colour just gets a little brighter instead
     via the shadow; the border colour itself never changes on hover. */
  .node:hover {
    box-shadow: var(--sh-3), var(--node-inset);
    z-index: var(--z-node-selected);
  }
  .node:not([class*="node--role-"]):hover { border-color: var(--line-strong); }
  .node:hover .node__photo { transform: scale(1.04); }
}

/* entering / leaving the 2-degree window (opacity only — transform is JS) */
.node--leaving { opacity: 0; pointer-events: none; }

/* ── Photo ───────────────────────────────────────────────────────────────── */
.node__photo {
  width: var(--node-photo);
  height: var(--node-photo);
  margin: 0 auto 10px;
  border-radius: 50%;
  border: 3px solid var(--bg-elevated);
  /* Matches the mockup's fallback avatar exactly: a flat wash of the
     page's own background colour (near-white in light mode, near-black in
     dark) with fixed slate-400 initials — reads as a quiet placeholder
     that belongs on the canvas, not a painted badge. The earlier version
     used a dark gradient with forced-white text, which looked fine in
     dark mode but out of place — a dark circle sitting on a light card —
     in light mode. */
  background: var(--bg);
  background-size: cover;
  background-position: center;
  display: grid;
  place-items: center;
  font-size: calc(var(--node-photo) * .34);
  font-weight: 700;
  color: #94a3b8;
  overflow: hidden;
  box-shadow: var(--sh-1);
  transition: transform var(--t-base) var(--ease-out), box-shadow var(--t-fast);
}
.node__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* Belt-and-braces alongside the draggable="false" attribute in
     tree-view.js — some browsers only honour one or the other. Both stop
     the native ghost-image drag from hijacking a pan that starts on a
     photo. */
  -webkit-user-drag: none;
  user-select: none;
}
/* ── In memory ─────────────────────────────────────────────────────────────
   A quieter, dignified stone tone — color-mix pulls a bit of "stone" into
   whatever --surface the current theme already defines, so this reads
   correctly in both dark and light instead of the fixed hex it used to be
   (which only ever looked right in dark mode).                            */
/* Same dark card as everyone else — the memorial reads through the
   greyscale photo, the serif italic name and the stone badge, not by
   washing the whole card grey (which just made it look disabled). */
/* Colours from the user's palette file — its dark-mode version painted the
   card near-pure-black (#050505), which read as a different app dropped
   onto the page; kept the "Silver Inlay" ring/glow/badge treatment that
   made it feel special, dropped just the background override so the card
   stays the same dark surface every other card uses. Light mode is the
   file's own values as-is, background included — that one wasn't the
   complaint. */
.node--deceased {
  border-color: #78716c;
  background: #fafaf9;
  box-shadow: inset 0 0 0 1px #e7e5e4;
}
.node--deceased .node__photo {
  border-color: #78716c;
  background: #f5f5f4;
  filter: grayscale(1) contrast(.95) brightness(1.02);
}
.node--deceased .node__name {
  font-family: Georgia, 'Times New Roman', serif;
  font-style: italic;
  font-weight: 800;
  color: #44403c;
}
.node--deceased .node__meta-pill { color: #a8a29e; }
.node--deceased .node__rel { color: #a8a29e; font-size: 10px; }

:root[data-theme="dark"] .node--deceased {
  border-color: #334155;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px #1e293b;
}
:root[data-theme="dark"] .node--deceased .node__photo {
  border-color: #e2e8f0;
  background: #0f172a;
  box-shadow: 0 0 15px rgba(226,232,240,.15);
}
:root[data-theme="dark"] .node--deceased .node__name {
  color: #f8fafc;
  letter-spacing: .025em;
}
:root[data-theme="dark"] .node--deceased .node__meta-pill,
:root[data-theme="dark"] .node--deceased .node__rel { color: #94a3b8; }

/* ── Text ────────────────────────────────────────────────────────────────── */
.node__name {
  font-family: var(--font-display);
  font-size: var(--fs-base);
  font-weight: 800;
  line-height: 1.22;
  letter-spacing: var(--tracking-display);
  overflow-wrap: anywhere;
}
.node__nick {
  margin-top: 1px;
  font-size: var(--fs-xs);
  color: var(--text-dim);
  font-style: italic;
  overflow-wrap: anywhere;
}
/* The birth–death line reads as a small tinted pill in the mockup, not
   plain text — same treatment the ghost status line below borrows too. */
/* Block-level rows, NOT inline — as inline boxes the date pill and the
   relation line beneath it flowed onto the same row instead of stacking.
   The inner pill element shrink-wraps to its own text. */
.node__meta { margin-top: 8px; display: flex; justify-content: center; }
.node__meta-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 10px;
  border-radius: var(--r-md);
  background: var(--surface-3);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
/* Always amber with a small link glyph, on every rank — in the mockup this
   line is about the RELATIONSHIP to you, not the person's rank, so it
   keeps one consistent colour rather than picking up the rank tint. */
.node__rel {
  margin-top: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  font-size: var(--fs-xs);
  color: var(--gold);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
}
.node__rel svg { width: 11px; height: 11px; flex: none; }

/* ── Centre node ─────────────────────────────────────────────────────────── */
/* The mockup does NOT give the focused card a solid amber border — that's
   reserved for the Guardian's own rank border (.node--role-admin below).
   A centred Seedling or Trusted Leaf stays its normal quiet outline; what
   marks it as focused is purely the lift — a layered "physically raised
   off the canvas" shadow plus a hairline 1px amber ring, not a thick
   painted-on border. Doubling up an amber border AND this glow on top of
   the Guardian's own amber border was what read as a dull, muddy orange. */
.node--center {
  background: var(--surface);
  box-shadow:
    0 6px 12px -4px rgba(15,23,42,.10),
    0 18px 28px -10px rgba(15,23,42,.18),
    0 44px 70px -22px rgba(15,23,42,.34),
    0 0 0 1px rgba(245,158,11,.16);
  cursor: default;
  z-index: var(--z-node-selected);
  translate: 0 -14px;
  animation: focusSettle var(--t-base) var(--ease-out) both;
}
:root[data-theme="dark"] .node--center {
  box-shadow:
    0 6px 12px -4px rgba(0,0,0,.45),
    0 18px 28px -10px rgba(0,0,0,.55),
    0 44px 70px -22px rgba(0,0,0,.75),
    0 0 0 1px rgba(245,158,11,.30);
}
@keyframes focusSettle {
  0%   { translate: 0 -2px; }
  60%  { translate: 0 -18px; }
  100% { translate: 0 -14px; }
}
@media (hover: hover) and (pointer: fine) {
  .node--center:hover { translate: 0 -18px; scale: 1.02; }
}
.node--center .node__photo {
  width: calc(var(--node-photo) * 1.12);
  height: calc(var(--node-photo) * 1.12);
  box-shadow: 0 0 0 4px rgba(245,158,11,.14);
  animation: haloPulse 3.4s var(--ease-soft) infinite;
}
@keyframes haloPulse {
  0%, 100% { box-shadow: 0 0 0 4px rgba(245,158,11,.10); }
  50%      { box-shadow: 0 0 0 9px rgba(245,158,11,.05); }
}
/* Slightly larger, but NOT uppercased — the mockup's focused card shows
   the name exactly as written, and shouting it in caps was both harder to
   read and inconsistent with every other card. */
.node--center .node__name { font-size: var(--fs-md); }

/* The mockup's small circular "pin" badge, shown only on the centred card —
   a real element (added in tree-view.js's refreshNode), not a pseudo-
   element, since it carries the crosshair icon from the shared sprite. */
.node__focus-pin {
  display: none;
  position: absolute;
  bottom: -13px; left: 50%;
  translate: -50% 0;
  width: 26px; height: 26px;
  border-radius: 999px;
  /* Fixed amber-500, not the theme-darkened --gold — the mockup's pin is
     the same #f59e0b whether the card behind it is Guardian or not, and
     the darkened light-mode --gold made it look washed out/brownish. */
  background: var(--accent-amber);
  color: var(--text-on-gold);
  align-items: center;
  justify-content: center;
  z-index: 45;
  box-shadow: 0 8px 18px -5px rgba(245,158,11,.75);
  border: 2px solid var(--bg-elevated);
}
.node--center .node__focus-pin { display: flex; }
.node__focus-pin svg { width: 13px; height: 13px; }

/* highlight pulse after a deep link / notification jump */
.node--flash { animation: flash 2s var(--ease-soft) 2; }
@keyframes flash {
  0%, 100% { box-shadow: var(--glow-gold), var(--sh-3); }
  50%      { box-shadow: 0 0 0 12px rgba(245,158,11,.28), var(--sh-4); }
}

/* ── Role accents ────────────────────────────────────────────────────────────
   Colour-per-rank taken directly from the user's own hand-picked palette
   file (gemini-code-1787148691232.html) — every rank gets a genuinely
   different hue now (Seedling sky, Rooted Sapling emerald, Trusted Leaf
   amethyst, Branch Keeper fuchsia, Tree Guardian amber), and light/dark
   each use that file's own shade rather than one fixed hex doing double
   duty — light leans a step darker/more saturated for contrast on white,
   dark leans a step lighter so it doesn't sink into the near-black card.
   Rank NAMES are unchanged; only colour. */
/* :not(.node--ghost):not(.node--deceased):not(.node--fallen) everywhere
   below — a status overlay must always outrank plain rank colour (a Ghost
   or an In Memory card doesn't stop being one because the person also
   happens to be rank Seedling), and without it these rules would win that
   fight anyway: adding the :root[data-theme="dark"] wrapper for per-theme
   colour raised their specificity to 3 simple selectors, comfortably above
   .node--ghost/.node--deceased/.node--fallen's 1, so a card that was BOTH
   (e.g. a ghost who is also rank member) silently lost its status colour
   back to the rank colour the instant dark mode's extra wrapper was added.
   Found by testing this in the browser, not by inspection. */
.node--role-member:not(.node--ghost):not(.node--deceased):not(.node--fallen),
.node--role-rooted:not(.node--ghost):not(.node--deceased):not(.node--fallen),
.node--role-verified:not(.node--ghost):not(.node--deceased):not(.node--fallen),
.node--role-subadmin:not(.node--ghost):not(.node--deceased):not(.node--fallen) {
  border-width: 1.5px;
}
.node--role-member:not(.node--ghost):not(.node--deceased):not(.node--fallen)    { border-color: #0ea5e9; }
.node--role-rooted:not(.node--ghost):not(.node--deceased):not(.node--fallen)    { border-color: #10b981; }
.node--role-verified:not(.node--ghost):not(.node--deceased):not(.node--fallen)  { border-color: #8b5cf6; }
.node--role-subadmin:not(.node--ghost):not(.node--deceased):not(.node--fallen)  { border-color: #c026d3; }

.node--role-member:not(.node--ghost):not(.node--deceased):not(.node--fallen)    .node__photo { border-color: #0ea5e9; }
.node--role-rooted:not(.node--ghost):not(.node--deceased):not(.node--fallen)    .node__photo { border-color: #10b981; }
.node--role-verified:not(.node--ghost):not(.node--deceased):not(.node--fallen)  .node__photo { border-color: #8b5cf6; }
.node--role-subadmin:not(.node--ghost):not(.node--deceased):not(.node--fallen)  .node__photo { border-color: #c026d3; }

:root[data-theme="dark"] .node--role-member:not(.node--ghost):not(.node--deceased):not(.node--fallen)    { border-color: #38bdf8; }
:root[data-theme="dark"] .node--role-rooted:not(.node--ghost):not(.node--deceased):not(.node--fallen)    { border-color: #34d399; }
:root[data-theme="dark"] .node--role-verified:not(.node--ghost):not(.node--deceased):not(.node--fallen)  { border-color: #a78bfa; }
:root[data-theme="dark"] .node--role-subadmin:not(.node--ghost):not(.node--deceased):not(.node--fallen)  { border-color: #d946ef; }

:root[data-theme="dark"] .node--role-member:not(.node--ghost):not(.node--deceased):not(.node--fallen)    .node__photo { border-color: #38bdf8; box-shadow: 0 0 10px rgba(56,189,248,.3); }
:root[data-theme="dark"] .node--role-rooted:not(.node--ghost):not(.node--deceased):not(.node--fallen)    .node__photo { border-color: #34d399; box-shadow: 0 0 10px rgba(52,211,153,.3); }
:root[data-theme="dark"] .node--role-verified:not(.node--ghost):not(.node--deceased):not(.node--fallen)  .node__photo { border-color: #a78bfa; box-shadow: 0 0 10px rgba(167,139,250,.3); }
:root[data-theme="dark"] .node--role-subadmin:not(.node--ghost):not(.node--deceased):not(.node--fallen)  .node__photo { border-color: #d946ef; box-shadow: 0 0 15px rgba(217,70,239,.3); }

/* ── 👑 Tree Guardian ──────────────────────────────────────────────────────
   The thickest, solid-amber border in the rank ramp above, a slow sheen
   sweeping across the card, and a warm halo — a flat color rather than a
   gradient border, since a gradient border reads differently (and
   previously, badly) on a white card vs. a dark one, and the sheen + halo
   already carry the "special" feeling on their own. */
.node--role-admin {
  /* Matches the mockup's .card-guardian exactly: a flat 2px amber-500
     outline (not the theme-darkened --gold, and not the old 3.5px), plus
     a soft amber-tinted lift instead of a wash. */
  border-width: 2px;
  border-color: var(--accent-amber);
  box-shadow: 0 12px 35px -10px rgba(245,158,11,.25), inset 0 0 0 1px rgba(245,158,11,.15);
}
:root[data-theme="dark"] .node--role-admin:not(.node--ghost):not(.node--deceased):not(.node--fallen) {
  /* amber-400, not amber-500 — the user's palette file lightens every
     rank's dark-mode shade a step so it doesn't sink into the near-black
     card; Guardian gets the same treatment as everyone else here. Same
     :not() reasoning as the role-accent block above — this 3-selector
     wrapper would otherwise outrank a Guardian-ranked ghost/deceased/
     fallen card's own status colour. */
  border-color: #fbbf24;
  box-shadow: 0 12px 35px -10px rgba(251,191,36,.25), inset 0 0 0 1px rgba(251,191,36,.15);
}

/* Centred AND Guardian — border stays the Guardian's own flat amber; the
   shadow is .node--center's layered "raised off the canvas" stack (needs
   re-declaring here since .node--role-admin's box-shadow rule comes later
   in this file and would otherwise win the cascade at equal specificity
   and flatten the lift back down to Guardian's plain resting shadow). */
.node--center.node--role-admin {
  border-width: 2px;
  border-color: var(--accent-amber);
  box-shadow:
    0 6px 12px -4px rgba(15,23,42,.10),
    0 18px 28px -10px rgba(15,23,42,.18),
    0 44px 70px -22px rgba(15,23,42,.34),
    0 0 0 1px rgba(245,158,11,.16);
}
:root[data-theme="dark"] .node--center.node--role-admin {
  border-color: #fbbf24;
  box-shadow:
    0 6px 12px -4px rgba(0,0,0,.45),
    0 18px 28px -10px rgba(0,0,0,.55),
    0 44px 70px -22px rgba(0,0,0,.75),
    0 0 0 1px rgba(251,191,36,.30);
}

/* The sheen sweep is a DARK-MODE flourish only. Over a white card its warm
   highlight simply tints the surface cream, which is exactly the "dirty"
   look it was meant to feel special against. */
:root[data-theme="dark"] .node--role-admin::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(115deg,
    transparent 32%,
    rgba(255,255,255,.08) 44%,
    rgba(255, 232, 168, .22) 50%,
    rgba(255,255,255,.08) 56%,
    transparent 68%);
  background-size: 260% 100%;
  background-position: 210% 0;
  animation: guardianSheen 5.5s var(--ease-soft) infinite;
}
@keyframes guardianSheen {
  0%        { background-position: 210% 0; }
  55%, 100% { background-position: -70% 0; }
}

.node--role-admin:not(.node--ghost):not(.node--deceased):not(.node--fallen) .node__photo { border-color: var(--accent-amber); }
:root[data-theme="dark"] .node--role-admin:not(.node--ghost):not(.node--deceased):not(.node--fallen) .node__photo { border-color: #fbbf24; box-shadow: 0 0 15px rgba(251,191,36,.4); }

/* The Guardian's name stays the ordinary text colour — in the mockup every
   card's name reads the same, with the crown badge and amber border doing
   the "this is the Guardian" work. */

.node--role-admin:hover { box-shadow: 0 18px 40px -12px rgba(245,158,11,.34), var(--sh-3); }

/* Guardian styling must not fight the "in memory" or "pending" treatments. */
.node--role-admin.node--deceased,
.node--role-admin.node--ghost { animation: none; }
.node--role-admin.node--deceased::after,
.node--role-admin.node--ghost::after { display: none; }

/* Deceased and pending styling always wins over the role accent. */
.node--deceased .node__photo,
.node--ghost .node__photo { box-shadow: none; }

/* ── Birthday 🎂 ─────────────────────────────────────────────────────────────
   Only shown when the exact day is on record and set to "full" visibility
   (Store.isBirthdayToday) — a rose pulse, a floating cake badge, and a
   gradient name, on their actual birthday only. */
/* !important here is deliberate (matches the mockup's own .birthday-active
   rule): a birthday can land on a Guardian, a Ghost, or a Fallen Leaf card
   alike, and it has to win over whichever theme's border/animation would
   otherwise come later in this file and tie on specificity. */
.node--birthday {
  border-color: #f472b6 !important;
  animation: birthdayPulse 2.4s var(--ease-soft) infinite !important;
}
@keyframes birthdayPulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(244,114,182,.16), var(--sh-2); }
  50%      { box-shadow: 0 0 0 9px rgba(244,114,182,.05), 0 0 22px rgba(245,158,11,.28), var(--sh-2); }
}
.node--birthday .node__photo {
  border-color: #f472b6 !important;
  box-shadow: 0 0 0 3px rgba(244,114,182,.2) !important;
}
.node--birthday .node__name { color: #db2777; }
.node__birthday-badge {
  position: absolute;
  top: -16px; right: -10px;
  width: 34px; height: 34px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: linear-gradient(135deg, #ec4899, #f43f5e);
  color: #fff;
  border: 2px solid var(--surface);
  box-shadow: 0 4px 10px rgba(236,72,153,.5);
  animation: floatCake 2.5s ease-in-out infinite;
  pointer-events: none;
  z-index: 3;
}
.node__birthday-badge svg { width: 16px; height: 16px; }
@keyframes floatCake {
  0%, 100% { transform: translateY(0) rotate(-10deg); }
  50%      { transform: translateY(-6px) rotate(10deg); }
}

/* ── Ghost node ──────────────────────────────────────────────────────────────
   The mockup treats pending/ghost as AMBER (not red) — dashed border, a
   faint diagonal-stripe overlay, no overall fade. --ghost is amber now (see
   tokens.css), so most of this falls out of the token change alone; the
   diagonal stripe is the one new bit of CSS, added as a ::before so no JS
   changes were needed for it. */
.node--ghost {
  border-style: dashed;
  border-width: 2px;
  border-color: var(--ghost);
  background: var(--surface);
  box-shadow: 0 10px 35px -22px rgba(234,88,12,.45);
}
:root[data-theme="dark"] .node--ghost { box-shadow: 0 10px 35px -22px rgba(251,146,60,.45); }
.node--ghost::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: repeating-linear-gradient(45deg, rgba(234,88,12,.05) 0 9px, transparent 9px 18px);
}
:root[data-theme="dark"] .node--ghost::before {
  background: repeating-linear-gradient(45deg, rgba(251,146,60,.05) 0 9px, transparent 9px 18px);
}
.node--ghost .node__photo { border-color: var(--ghost); border-style: dashed; color: var(--ghost); }
.node--ghost:hover { border-color: var(--ghost); }

/* "Awaiting approval" flag along the bottom edge — the mockup keeps the
   person's real rank badge on top and puts the pending status here
   instead, because the record IS complete, it just isn't public yet. */
.node__ghost-flag {
  position: absolute;
  bottom: -10px; left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
  z-index: 25;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  background: var(--ghost);
  color: #ffffff;
  font-size: 8px;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  box-shadow: 0 6px 16px -6px rgba(234,88,12,.8);
}
:root[data-theme="dark"] .node__ghost-flag { box-shadow: 0 6px 16px -6px rgba(251,146,60,.8); }
.node__ghost-flag svg { width: 9px; height: 9px; }
/* It would collide with the action bar, so it steps aside while that's open. */
.node--active .node__ghost-flag { display: none; }

.node__addedby {
  margin-top: 4px;
  font-size: 9.5px;
  font-weight: 700;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: .04em;
}

/* ── 🍂 Fallen Leaf ────────────────────────────────────────────────────────
   Someone who married in, then divorced and left the family — still shown
   for the historical/children record, but visibly a different status from
   any live role. A muted, faded, dashed-border theme; never applies to a
   ghost or a memorial card, which both already outrank it. */
/* Colours straight from the user's palette file — plain card background
   (the file doesn't tint it at all, just the border/text/ring), lighter
   slate in light mode, deeper slate in dark, and the name/relation shades
   swap weight between the two themes to stay legible on each. */
.node--fallen {
  border-style: dashed;
  border-color: #94a3b8;
}
.node--fallen:hover, .node--fallen.node--center { border-color: #94a3b8; }
.node--fallen .node__name { color: #64748b; }
.node--fallen .node__rel  { color: #94a3b8; }
.node--fallen .node__photo { border-color: #cbd5e1; border-style: dashed; filter: grayscale(.6); }
.node__role-pill--fallen { background: #f1f5f9; color: #64748b; }

:root[data-theme="dark"] .node--fallen,
:root[data-theme="dark"] .node--fallen:hover,
:root[data-theme="dark"] .node--fallen.node--center { border-color: #64748b; }
:root[data-theme="dark"] .node--fallen .node__name { color: #94a3b8; }
:root[data-theme="dark"] .node--fallen .node__rel  { color: #64748b; }
:root[data-theme="dark"] .node--fallen .node__photo { border-color: #475569; }
:root[data-theme="dark"] .node__role-pill--fallen { background: color-mix(in srgb, #94a3b8 22%, var(--bg-elevated)); color: #94a3b8; }

/* confirm animation: ghost → public */
.node--confirming { animation: ghostConfirm 900ms var(--ease-out) both; }
@keyframes ghostConfirm {
  0%   { opacity: .72; box-shadow: 0 0 0 0 rgba(16,185,129,.55); }
  55%  { opacity: 1;   box-shadow: 0 0 0 26px rgba(16,185,129,0); }
  100% { opacity: 1;   box-shadow: var(--sh-2); }
}

/* ── Pause decorative loops while actively panning ────────────────────────────
   .stage.is-panning is set by both the desktop drag-pan and the mobile touch
   pan (gestures.js). None of these box-shadow/background-position animations
   can be watched mid-drag anyway, and they're exactly the kind of continuous
   paint work that turns into stutter and missed taps on a phone GPU while a
   finger is actively moving the tree. */
.stage.is-panning .node--center .node__photo,
.stage.is-panning .node--role-admin::after,
.stage.is-panning .node--role-admin .node__photo,
.stage.is-panning .node--birthday,
.stage.is-panning .node__birthday-badge {
  animation: none !important;
}

/* ── Role pill (straddles the top border) ───────────────────────────────────
   One centred pill sitting half on, half off the card's top edge —
   TREE GUARDIAN, BRANCH KEEPER, TRUSTED LEAF, ROOTED SAPLING, SEEDLING, or
   IN MEMORY. Distinct from the small corner "Pending" badge a ghost member
   gets, which is about the tree record, not a role. Soft-tinted pill
   (colour-per-rank + a low-alpha same-hue background), matching the app's
   own established chip/toast pattern elsewhere — the border borrows
   whatever text colour the variant already set via currentColor, so each
   variant only needs to declare background + color, nothing else. */
.node__role-pill {
  position: absolute;
  top: -12px; left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Outlined, not filled — the mockup's badge is a SOLID dark chip with a
     coloured border and coloured text, which is both opaque (nothing shows
     through it) and far lighter visually than a saturated filled pill.
     `currentColor` drives the border, so each variant below only has to
     declare its colour. */
  /* A pale wash of the rank's own colour over the card surface — opaque,
     but tinted rather than stark white/black, which is what the mockup's
     badges do in both themes. */
  background: color-mix(in srgb, currentColor 14%, var(--bg-elevated));
  border: 1px solid color-mix(in srgb, currentColor 40%, transparent);
  box-shadow: var(--sh-1);
  z-index: 2;   /* local to the card — just above its own photo/name, not the app-wide scale */
  pointer-events: none;
}
.node__role-pill svg { width: 11px; height: 11px; flex: none; }

/* Hardcoded per rank/theme rather than the generic currentColor wash above
   — the user's palette file specifies its own exact badge background per
   rank (a light pastel tint in light mode, a low-alpha tint of the
   brighter dark-mode shade in dark), not just "14% of whatever the border
   colour is", so each variant sets background/border itself now. */
.node__role-pill--member   { color: #0ea5e9; background: #f0f9ff; border-color: rgba(14,165,233,.3); }
.node__role-pill--rooted   { color: #10b981; background: #ecfdf5; border-color: rgba(16,185,129,.3); }
.node__role-pill--verified { color: #8b5cf6; background: #f5f3ff; border-color: rgba(139,92,246,.3); }
.node__role-pill--subadmin { color: #c026d3; background: #fdf4ff; border-color: rgba(192,38,211,.3); }
.node__role-pill--admin    { color: #f59e0b; background: #fffbeb; border-color: rgba(245,158,11,.3); }

/* Solid, opaque chips — not a low-alpha rgba wash over the card. The
   palette file's own dark-mode badges were a translucent 10-15% tint,
   which over this app's near-black card read as faded/washed-out rather
   than as a colour chip; color-mix against --bg-elevated keeps the same
   tint but as a fully opaque solid, matching light mode's own solid
   pastel chips instead of looking lower-contrast than them. */
:root[data-theme="dark"] .node__role-pill--member   { color: #38bdf8; background: color-mix(in srgb, #38bdf8 22%, var(--bg-elevated)); border-color: rgba(56,189,248,.4); }
:root[data-theme="dark"] .node__role-pill--rooted   { color: #34d399; background: color-mix(in srgb, #34d399 22%, var(--bg-elevated)); border-color: rgba(52,211,153,.4); }
:root[data-theme="dark"] .node__role-pill--verified { color: #a78bfa; background: color-mix(in srgb, #a78bfa 22%, var(--bg-elevated)); border-color: rgba(167,139,250,.4); }
:root[data-theme="dark"] .node__role-pill--subadmin { color: #d946ef; background: color-mix(in srgb, #d946ef 22%, var(--bg-elevated)); border-color: rgba(217,70,239,.4); }
:root[data-theme="dark"] .node__role-pill--admin    { color: #fbbf24; background: color-mix(in srgb, #fbbf24 26%, var(--bg-elevated)); border-color: rgba(251,191,36,.4); }

/* Solid inverted fill, not the pale-wash pattern the other badges use —
   matches the palette file's own "In Loving Memory" badge exactly. */
.node__role-pill--memory { background: #78716c; color: #ffffff; border-color: transparent; }
:root[data-theme="dark"] .node__role-pill--memory { background: #e2e8f0; color: #050505; font-weight: 800; }
.node__role-pill--ghost { color: #ea580c; background: #fff7ed; border-color: rgba(234,88,12,.3); }
:root[data-theme="dark"] .node__role-pill--ghost { color: #fb923c; background: color-mix(in srgb, #fb923c 22%, var(--bg-elevated)); border-color: rgba(251,146,60,.4); }

/* Membership number, quiet and monospace in the bottom-right corner —
   matching the mockup's idBadge. Absolutely positioned so it never joins
   the centred text flow. */
.node__idbadge {
  position: absolute;
  bottom: 8px; right: 10px;
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, monospace;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: .04em;
  color: var(--text-dim);
  pointer-events: none;
}

/* hidden-connection counters — the whole reason these exist is to tell you
   "there's more here, off-screen"; they don't get a second chance to be
   noticed if a viewer's depth setting is dialled down from the default
   whole-tree view, so being reliably visible whenever they're present
   matters more than most badges on the card. */
.node__hidden {
  position: absolute;
  display: flex;
  gap: 4px;
  pointer-events: none;
  /* .node__role-pill (every single card has one, no exceptions) sits
     opaque at z-index: 2 dead-centre on the top edge. --up used to sit
     directly on top of it at the same centred position — same real
     estate, and the always-present role pill always won, so the "+N ↑"
     counter was never actually visible on any card, ever, whenever
     someone had hidden ancestors. Above the role pill's z-index now, in
     case a future layout still lets them overlap. */
  z-index: 3;
}
.node__hidden--up   { top: -11px;  right: 6px; }
.node__hidden--down { bottom: -11px; left: 50%; transform: translateX(-50%); }
.node__hidden--side { right: -12px; top: 50%; transform: translateY(-50%); flex-direction: column; }
.hidden-pill {
  padding: 1px 7px;
  border-radius: var(--r-pill);
  background: var(--bg-elevated);
  border: 1px solid var(--line-strong);
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 700;
  white-space: nowrap;
  box-shadow: var(--sh-1);
}

/* ── Quick actions: first tap reveals, second tap opens full details ─────────
   Ported from Mockup/nafhan_family_tree_ULTIMATE_5.html. A floating bar
   below the active card offering "View Details" and "Locate", so a tap on a
   card never yanks the viewport on its own — see the tap model in
   tree-view.js's buildNode. Absolutely positioned (not in flow) so it never
   affects the collapsed card height JS measures for row spacing; the
   overhang lands in the row-gap whitespace. */
.node__actions {
  display: none;
  position: absolute;
  bottom: -52px; left: 50%;
  transform: translateX(-50%);
  gap: 6px;
  z-index: 60;
  white-space: nowrap;
  padding: 5px;
  border-radius: 16px;
  background: var(--bg-elevated);
  border: 1px solid var(--line-soft);
  box-shadow: var(--sh-3);
  animation: actionsIn .18s var(--ease-out);
}
.node--active .node__actions { display: flex; }
@keyframes actionsIn {
  from { opacity: 0; transform: translateX(-50%) translateY(-6px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

.node__act {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 7px 12px;
  border-radius: 12px;
  border: 0;
  cursor: pointer;
  font-size: 11px;
  font-weight: 800;
  background: var(--surface-2);
  color: var(--text-muted);
  transition: background var(--t-fast), color var(--t-fast);
}
/* The details button is the primary of the pair — same theme-inverted
   colour as every other primary action in the app. */
.node__act:first-child { background: var(--btn-primary-bg); color: var(--btn-primary-text); }
.node__act:hover { background: var(--gold); color: var(--text-on-gold); }
.node__act svg { width: 13px; height: 13px; }

/* The active card lifts slightly and sits above its neighbours. */
.node--active {
  z-index: 60;
  translate: 0 -6px;
  box-shadow: var(--sh-3), var(--node-inset);
}
.node--center.node--active { translate: 0 -14px; }
/* These would collide with the action bar, so they step aside while it's open. */
.node--active .node__focus-pin { display: none; }

@media (max-width: 720px) {
  .node__actions { bottom: -48px; }
  .node__act { padding: 8px 11px; }
}

/* ── Connection lines ─────────────────────────────────────────────────────────
   Geometry/drawing is untouched (see core/layout.js and tree-view.js's
   drawConnectors/animateTo) — only colours and dash-patterns are restyled
   here, matched to the mockup's edge vocabulary: plain grey lineage lines,
   a solid amber marriage line, a red-dashed line for a divorced marriage,
   and an amber-dashed line for a still-pending (ghost) link. */
.link { fill: none; transition: stroke var(--t-fast), stroke-width var(--t-fast), opacity var(--t-move); z-index: var(--z-tree-line); }
.link--parent  { stroke: var(--line-strong); stroke-width: 2; z-index: var(--z-tree-line); }
/* Fixed amber-500, not the theme-shifted --gold — a stroke colour reads as
   dull/washed the moment it's the darkened light-mode text shade instead
   of a flat, saturated line. */
.link--spouse  { stroke: var(--accent-amber); stroke-width: 2.5; z-index: var(--z-marriage-line); }
.link--sibling { stroke: var(--line-strong); stroke-width: 2; stroke-dasharray: 6 6; opacity: .8; z-index: var(--z-tree-line); }
.link--ghost   { stroke: var(--accent-amber); stroke-width: 2; stroke-dasharray: 5 5; opacity: .8; z-index: var(--z-ghost-line); }
/* A former marriage: the amber line switches to a muted red, wide dash —
   still there (they were married), just visibly ended. */
.link--spouse.link--divorced { stroke: var(--danger); stroke-dasharray: 3 7; opacity: .65; }
.link:hover    { stroke-width: 4; }
.link-hit      { stroke: transparent; stroke-width: 14; fill: none; pointer-events: stroke; cursor: help; }

/* Junction dot where a child joins its parents' line — the visual proof of
   which cards on a crowded row are actually the children. */
.link-joint        { fill: var(--line-strong); stroke: none; }
.link-joint--ghost { fill: var(--accent-amber); opacity: .8; }

.link-label {
  fill: var(--text-dim);
  font-size: 10px;
  font-weight: 700;
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--bg);
  stroke-width: 4px;
  stroke-linejoin: round;
  opacity: 0;
  transition: opacity var(--t-fast);
}
.linkgroup:hover .link-label { opacity: 1; }

/* ── Generation guides ───────────────────────────────────────────────────── */
.gen-guide { stroke: var(--line-soft); stroke-width: 1; stroke-dasharray: 2 8; opacity: .5; }
.gen-label {
  fill: var(--text-dim);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  opacity: .55;
}
