/* Real skill tree (explicit direction — not a card grid dressed up with
   the word "arbre"): a root node, a central trunk, 3 levels of 2 branch
   cards each, all physically connected by CSS-only connectors. No canvas,
   no graph library, no hardcoded per-card vertical coordinates — every
   connector attaches at `top: 50%` of its OWN card box (a pseudo-element
   positioned relative to that card), so it tracks that card's real height
   automatically regardless of how much content its sibling has. */

:root {
  --tree-gap-x: 20px;
  --tree-center-w: 64px;
  --tree-stub: 52px; /* = gap + half of center column, the distance from a
                         card's own edge to the trunk's centerline */
  --tree-vgap: 26px;
}

.skills-tree-section {
  margin-top: 56px;
}

.skill-tree {
  margin-top: 28px;
}

/* ---- Root node ---- */
.skill-tree__root {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: fit-content;
  max-width: 220px;
  margin: 0 auto;
  padding: 16px 24px;
  text-align: center;
  border-radius: 16px;
  background:
    linear-gradient(rgba(28, 24, 40, 0.9), rgba(28, 24, 40, 0.9)) padding-box,
    linear-gradient(135deg, #3f7bf8, #b86bf8, #f86bb8) border-box;
  border: 1.5px solid transparent;
}

.skill-tree__root::before {
  content: "";
  position: absolute;
  inset: -24px;
  z-index: -1;
  border-radius: inherit;
  background: radial-gradient(60% 60% at 50% 40%, rgba(139, 92, 246, 0.3), transparent 74%);
  filter: blur(20px);
  opacity: 0.6;
  animation: skill-tree-root-pulse 7s ease-in-out infinite;
  pointer-events: none;
}

@keyframes skill-tree-root-pulse {
  0%,
  100% {
    opacity: 0.45;
  }
  50% {
    opacity: 0.75;
  }
}

@media (prefers-reduced-motion: reduce) {
  .skill-tree__root::before {
    animation: none;
    opacity: 0.6;
  }
}

.skill-tree__root-icon {
  display: flex;
  color: #cdd8ff;
}

.skill-tree__root-icon svg {
  width: 22px;
  height: 22px;
}

.skill-tree__root-title {
  font-family: var(--font-satoshi);
  font-weight: 700;
  font-size: 15px;
  color: #fff;
}

.skill-tree__root-subtitle {
  font-family: var(--font-satoshi);
  font-size: 11.5px;
  letter-spacing: 0.03em;
  color: var(--li-text-secondary);
}

/* Root -> level 1 connector. A fixed-length structural stem (not a
   per-card coordinate — this just bridges two always-adjacent elements),
   flush with the first level's own trunk segment below it. */
.skill-tree__stem {
  width: 2px;
  height: 30px;
  margin: 0 auto;
  background: linear-gradient(180deg, rgba(184, 107, 248, 0.7), rgba(63, 123, 248, 0.35));
}

/* ---- Levels ---- */
.skill-tree__level {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--tree-center-w) minmax(0, 1fr);
  align-items: center;
  gap: var(--tree-gap-x);
}

.skill-tree__level + .skill-tree__level {
  margin-top: var(--tree-vgap);
}

/* .skill-tree__branch-wrap only exists to hold the mobile trunk
   connector as a real sibling *before* the branch (skill-tree.njk) — a
   box's own background always paints before its own descendants, so a
   connector living inside the branch itself could never render behind
   that branch's own card surface, no matter its z-index. `display:
   contents` here makes the wrapper invisible to the desktop grid, so
   .skill-tree__level's three columns still see exactly the same items
   (branch, gutter, branch) as if this wrapper didn't exist — the
   connector itself is display: none by default (see below), so
   `contents` never leaves it behind as a stray fourth/sixth grid item. */
.skill-tree__branch-wrap {
  display: contents;
}

.skill-tree__connector {
  display: none;
}

.skill-tree__gutter {
  position: relative;
  align-self: stretch;
}

.skill-tree__gutter::before {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(-1 * var(--tree-vgap) / 2);
  bottom: calc(-1 * var(--tree-vgap) / 2);
  width: 2px;
  transform: translateX(-50%);
  background: linear-gradient(180deg, rgba(184, 107, 248, 0.5), rgba(63, 123, 248, 0.35));
}

/* :first-child/:last-child never actually matched here (real bug found
   and fixed, same root cause as the mobile trunk fix further down) —
   .skill-tree__level is never literally the first/last child of its own
   parent, .skill-tree__root and .skill-tree__stem come before it, so
   these two trims silently never applied; the first/last level's own
   gutter line extended slightly too far into non-existent space above/
   below. .skill-tree__level--first/--last (skill-tree.njk) name them
   unambiguously instead of guessing at DOM position. */
.skill-tree__level--first .skill-tree__gutter::before {
  top: 0;
}

.skill-tree__level--last .skill-tree__gutter::before {
  bottom: 0;
}

/* ---- Branch cards ---- */
.skill-tree__branch {
  position: relative;
  isolation: isolate;
  padding: 22px 22px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 42%),
    var(--li-card-bg);
  backdrop-filter: var(--li-card-blur);
  -webkit-backdrop-filter: var(--li-card-blur);
  border: 1px solid var(--li-border);
  border-radius: 16px;
  box-shadow: var(--li-shadow);
  /* transform/border-color only — no box-shadow or background in this
     list (real fix found and fixed: neither property is GPU-composited,
     so animating them forces a repaint every frame, which is exactly
     what read as janky/"pas fluide" hovering over several cards).
     border-color is a cheap, universally-used exception across this
     site's own cards. */
  transition: transform 0.25s ease, border-color 0.25s ease;
}

.skill-tree__branch--left {
  --branch-glow: rgba(63, 123, 248, 0.55);
}

.skill-tree__branch--right {
  --branch-glow: rgba(184, 107, 248, 0.5);
}

/* The card's colored glow, done the same static-halo way as every other
   card on this page (skills-layout.css's .cert-card::before): opacity 0
   at rest, opacity 1 on hover — pure opacity is the one transition
   that's always GPU-composited regardless of what's under it (unlike
   animating box-shadow's blur radius directly, which forces the browser
   to recompute and repaint the shadow every frame).
   A real element (.skill-tree__branch-glow, skill-tree.njk), not a
   ::before — both pseudo-element slots on this card are already spoken
   for: ::before is the mobile trunk line and (at >=768px) the joint dot,
   ::after is the connector stub. */
.skill-tree__branch-glow {
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: radial-gradient(60% 55% at 50% 0%, var(--branch-glow, rgba(139, 92, 246, 0.45)), transparent 72%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}

/* Both selectors needed — .skill-tree.is-visible .skill-tree__branch (the
   entrance-reveal rule further down) is 3 classes deep and would
   otherwise always win over a plain .skill-tree__branch:hover (2) once
   the tree has revealed, silently killing the hover lift permanently.
   The .is-visible-scoped version here matches that specificity so hover
   keeps working after the reveal plays (the plain version still covers
   hover during/before the reveal transition itself). */
.skill-tree__branch:hover,
.skill-tree.is-visible .skill-tree__branch:hover {
  transform: translateY(-3px);
  border-color: var(--li-border-hover);
  /* box-shadow deliberately left untouched here (real bug found and
     fixed, the actual remaining cause of the measured jank — up to
     346ms long tasks, confirmed via a CDP performance trace): the base
     rule's own box-shadow already changed on hover too (--li-shadow ->
     --li-shadow-hover, a bigger blur radius), on top of the colored-glow
     fix above. That's a SECOND non-composited box-shadow repaint, tiny
     on its own but combined with 6 backdrop-filter: blur(24px) cards all
     eligible to hover in the same view, it was slow enough to feel like
     real lag. transform + border-color + the glow's opacity is now the
     complete list of what hover changes here — nothing left that forces
     a shadow recompute. */
}

.skill-tree__branch:hover .skill-tree__branch-glow {
  opacity: 0.9;
}

@media (prefers-reduced-motion: reduce) {
  .skill-tree__branch,
  .skill-tree__branch-glow {
    transition: none;
  }

  .skill-tree__branch:hover,
  .skill-tree.is-visible .skill-tree__branch:hover {
    transform: none;
  }
}

/* Connector stub + joint dot — a pseudo-element pair per card, each
   anchored at `top: 50%` of THAT card's own box, so it always lands on
   the card's real vertical center no matter how tall its sibling is.
   Desktop/tablet only (>=768px) — below that the mobile timeline block
   further down redefines plain .skill-tree__branch::before/::after for
   its own left-edge connector, which these higher-specificity --left/
   --right selectors would otherwise always win over regardless of media
   query source order. */
@media (min-width: 768px) {
  /* Dim at rest, full brightness on hover — real fix found and fixed:
     this used to declare a `background` transition that nothing ever
     actually triggered (no hover rule changed its background), while
     its `opacity` transition was equally inert (already 1 by default,
     hover set it to 1 again) — the stub never visibly responded to
     hover at all despite the declared transitions. Giving it a real
     resting/hover opacity difference makes the transition mean
     something, and opacity is the cheap, composited property to
     animate here anyway. */
  .skill-tree__branch--left::after,
  .skill-tree__branch--right::after {
    content: "";
    position: absolute;
    top: 50%;
    width: var(--tree-stub);
    height: 1.5px;
    background: linear-gradient(90deg, rgba(255, 255, 255, 0.05), var(--branch-glow, rgba(139, 92, 246, 0.5)));
    opacity: 0.45;
    transition: opacity 0.25s ease;
  }

  .skill-tree__branch--left::after {
    right: calc(-1 * var(--tree-stub));
    transform: translateY(-50%);
  }

  .skill-tree__branch--right::after {
    left: calc(-1 * var(--tree-stub));
    transform: translateY(-50%) scaleX(-1);
    background: linear-gradient(90deg, rgba(255, 255, 255, 0.05), var(--branch-glow, rgba(139, 92, 246, 0.5)));
  }

  /* The joint dot's ring+glow is a fixed box-shadow now — not animated
     (real fix found and fixed: transitioning a box-shadow's blur radius
     forces the browser to repaint it every frame, non-composited, the
     other concrete cause of the hover feeling janky). Hover instead
     scales the dot itself up slightly, a `transform` change, always
     GPU-composited regardless of what's under it. */
  .skill-tree__branch--left::before,
  .skill-tree__branch--right::before {
    content: "";
    position: absolute;
    top: 50%;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--branch-glow, #b86bf8);
    box-shadow: 0 0 0 3px rgba(20, 16, 30, 0.9), 0 0 12px 2px var(--branch-glow, rgba(139, 92, 246, 0.7));
    transition: transform 0.25s ease;
  }

  .skill-tree__branch--left::before {
    right: calc(-1 * var(--tree-stub) - 3.5px);
    transform: translateY(-50%);
  }

  .skill-tree__branch--right::before {
    left: calc(-1 * var(--tree-stub) - 3.5px);
    transform: translateY(-50%);
  }

  .skill-tree__branch:hover::after {
    opacity: 1;
  }

  .skill-tree__branch:hover::before {
    transform: translateY(-50%) scale(1.4);
  }

  @media (prefers-reduced-motion: reduce) {
    .skill-tree__branch--left::after,
    .skill-tree__branch--right::after,
    .skill-tree__branch--left::before,
    .skill-tree__branch--right::before {
      transition: none;
    }
  }
}

.skill-tree__branch-head {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* A real colored badge now (explicit direction: "beaucoup mieux
   travaillé" — the icon used to just float bare next to the title,
   reading as an afterthought). Tinted from the same --branch-glow each
   branch already carries, so the left/right color pairing established
   by the connectors and card halo reads through here too. */
.skill-tree__branch-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--branch-glow, #8b5cf6) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--branch-glow, #8b5cf6) 34%, transparent);
  color: #d7e0ff;
}

.skill-tree__branch-icon svg {
  width: 17px;
  height: 17px;
}

.skill-tree__branch-title {
  margin: 0;
  font-family: var(--font-satoshi);
  font-weight: 700;
  font-size: 14.5px;
  color: #fff;
}

/* ---- Chips: every technology gets IDENTICAL styling now (explicit
   direction, reversing the earlier two-tier "structuring vs
   complementary" pass — that unexplained visual hierarchy read as some
   skills being more important with no legend to justify it). Same
   background, border, color, opacity, radius and weight for all of
   them; order alone conveys importance. On hover of the whole branch
   card, every one of its own chips brightens together as a group — no
   single chip is ever singled out permanently. */
.skill-tree__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* Tinted with the branch's own --branch-glow (skill-tree.njk sets it per
   branch, one of the 6 same hues as blog/portfolio topic tags) rather
   than the flat, colorless grey every chip shared before — explicit
   direction: "les couleurs... c'est pas clair" — with 6 differently
   colored branches side by side, which category a chip belongs to reads
   at a glance instead of requiring reading every card header first. */
.skill-chip {
  display: inline-flex;
  align-items: center;
  padding: 6px 12px;
  border-radius: 9999px;
  background: color-mix(in srgb, var(--branch-glow, rgba(255, 255, 255, 0.4)) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--branch-glow, rgba(255, 255, 255, 0.4)) 30%, transparent);
  font-family: var(--font-satoshi);
  font-size: 12.5px;
  font-weight: 400;
  color: var(--li-text-secondary);
  white-space: nowrap;
  transition: background 0.25s ease, border-color 0.25s ease, color 0.25s ease;
}

/* Density fix (explicit direction: "trop de noms de technos... c'est
   dense") — nothing removed from the data, only the first 6 chips show
   by default; the rest stay in the DOM (real content, still readable
   with JS off / to search engines) but are hidden until the "+N" toggle
   below expands them. js/skill-tree.js only ever flips .is-expanded. */
.skill-chip--extra {
  display: none;
}

.skill-tree__chips.is-expanded .skill-chip--extra {
  display: inline-flex;
}

.skill-chip--toggle {
  appearance: none;
  cursor: pointer;
  background: transparent;
  border-style: dashed;
  color: #fff;
  font-weight: 600;
}

.skill-chip--toggle:hover,
.skill-chip--toggle:focus-visible {
  background: color-mix(in srgb, var(--branch-glow, rgba(255, 255, 255, 0.4)) 20%, transparent);
}

.skill-tree__branch:hover .skill-chip {
  color: #fff;
  border-color: rgba(139, 92, 246, 0.32);
  background: rgba(139, 92, 246, 0.1);
}

@media (prefers-reduced-motion: reduce) {
  .skill-chip {
    transition: none;
  }
}

/* ---- Entrance animation — real fix found and fixed here (explicit
   direction: "beaucoup mieux travaillé ... fluide"). The old version
   staged root -> trunk -> level1 -> level2 -> level3 off ONE single
   [data-reveal] on the whole .skill-tree, firing once, the moment the
   very top of the tree first crossed into view. On a page this tall —
   especially the mobile timeline, 6 stacked cards — a visitor scrolling
   normally would still be reading the root/level-1 cards by the time
   levels 2 and 3's own transition-delay (0.45s/0.65s) had already
   elapsed off-screen: those cards just appeared already-settled with no
   visible entrance at all once scrolled to, which read as flat/lifeless
   rather than a real animation.
   Each .skill-tree__level now carries its OWN [data-reveal] (skill-
   tree.njk) — js/reveal.js's IntersectionObserver already supports any
   number of independent targets, nesting included — so every level's
   connector + two branches animate in together right as THAT level
   actually scrolls into view, all the way down the tree, not just the
   first one. Root/stem keep the original single tree-level reveal
   (they're always the first thing in view anyway, so there's nothing to
   gain from observing them separately). */
.skill-tree__root,
.skill-tree__stem {
  opacity: 0;
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.skill-tree__root {
  transform: translateY(-8px) scale(0.96);
}

.skill-tree__stem {
  transform: scaleY(0);
  transform-origin: top;
  transition: opacity 0.4s ease, transform 0.4s ease;
  transition-delay: 0.15s;
}

.skill-tree__gutter::before {
  opacity: 0;
  transform: translateX(-50%) scaleY(0);
  transform-origin: top;
  transition: opacity 0.4s ease, transform 0.4s ease;
}

.skill-tree__branch {
  opacity: 0;
  transform: translateY(18px) scale(0.98);
  transition: opacity 0.45s ease, transform 0.45s ease;
}

/* A small left-then-right cascade within each level (explicit direction:
   more "travaillé" than both cards snapping in at the exact same
   instant) — real values a real person would tune, not a round number. */
.skill-tree__branch--right {
  transition-delay: 0.09s;
}

.skill-tree[data-reveal],
.skill-tree__level[data-reveal] {
  /* Overrides base.css's generic [data-reveal] block-level fade (each of
     these elements' OWN opacity/transform is handled by the staged
     rules below instead, keyed off its own .is-visible class — the
     level container itself doesn't visibly move, only its children
     do). */
  opacity: 1;
  transform: none;
  transition: none;
}

.skill-tree.is-visible .skill-tree__root {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.skill-tree.is-visible .skill-tree__stem {
  opacity: 1;
  transform: scaleY(1);
}

.skill-tree__level.is-visible .skill-tree__gutter::before {
  opacity: 1;
  transform: translateX(-50%) scaleY(1);
}

.skill-tree__level.is-visible .skill-tree__branch {
  opacity: 1;
  transform: translateY(0) scale(1);
}

@media (prefers-reduced-motion: reduce) {
  .skill-tree__root,
  .skill-tree__stem,
  .skill-tree__gutter::before,
  .skill-tree__branch {
    transition: none;
    opacity: 1;
    transform: none;
  }
}

/* ---- Tablet: keep the left/right tree structure, just tighten the
   center column slightly. ---- */
@media (max-width: 900px) {
  :root {
    --tree-gap-x: 16px;
    --tree-center-w: 48px;
    --tree-stub: 40px;
  }
}

/* ---- Mobile (<768px): vertical timeline — trunk moves to the left
   edge, all 6 branches stack in the same order they already appear in
   the DOM (level1-left, level1-right, level2-left, ...), each full
   width. No horizontal navigation, no carousel, no accordion. ---- */
@media (max-width: 767px) {
  /* Real spacing fix (explicit direction: "tout est collé c'est
     horrible") — every one of these roughly doubles the old, cramped
     22px value: more room between the trunk and the page edge, more air
     between stacked cards, a bigger and more visible joint dot. One
     local scale so the trunk/dot math below stays consistent with
     whatever these end up being, rather than the old scattered literal
     22px repeated seven times.
     A real, honest bug found and fixed here: these three custom
     properties were first written as bare declarations directly inside
     this @media block, with no selector wrapping them — invalid CSS (a
     media query can only contain rules, not loose declarations), which
     was silently dropping this entire block in every browser tested, no
     console error to point at it. `:root` fixes it the same way the
     file's own top-of-file custom properties are declared. */
  /* Real redesign here (explicit direction: the old left-rail timeline —
     trunk beside the cards, cards indented off it — read wrong; wanted
     instead is a single centered trunk that runs straight down the
     middle of the column and passes BEHIND the cards, the same "line
     hidden by whatever's stacked on top of it, visible only in the
     gaps" idea the desktop gutter already uses between its two columns,
     just brought to a single mobile column). Cards are full width now
     (no more --tree-m-indent, no more per-card left margin) — the trunk
     is drawn by each card's own ::before, centered under it, extending
     into the gaps above/below and clipped from view everywhere the
     card's own opaque background sits on top of it (isolation: isolate
     on .skill-tree__branch, set already, keeps that z-index: -1 segment
     confined behind THIS card specifically). --tree-m-gap also now
     matches the stem's own height exactly (both reference the same
     variable) — real number found and fixed: the root->first-card and
     card->card gaps were already meant to be the same value, but
     weren't literally guaranteed equal until both were pinned to one
     variable (explicit direction: same space as "AI Engineering" ->
     "GenAI & Agentic AI", for every gap after that too). */
  :root {
    --tree-m-gap: 40px; /* space between EVERY consecutive card, root included */
    --tree-m-dot: 9px;
  }

  .skill-tree__level {
    display: block;
  }

  .skill-tree__level + .skill-tree__level {
    margin-top: 0;
  }

  .skill-tree__gutter {
    display: none;
  }

  .skill-tree__stem {
    height: var(--tree-m-gap);
  }

  .skill-tree__branch {
    position: relative;
    margin-left: 0;
    padding: 24px 22px;
    /* Touch feedback (no :hover on a real touchscreen, explicit
       direction still wants "fluide" here) — a quick, composited
       transform + border-color response on tap, same two properties as
       the desktop hover, nothing new to repaint. */
    transition: transform 0.15s ease, border-color 0.15s ease;
  }

  .skill-tree__branch:active {
    transform: scale(0.985);
    border-color: var(--li-border-hover);
  }

  .skill-tree__branch:active .skill-tree__branch-glow {
    opacity: 0.7;
  }

  /* Real bug found and fixed here: the DOM has a `.skill-tree__gutter`
     div sitting between each level's left and right branch (needed for
     the desktop grid, just display: none here) — so `.skill-tree__branch
     + .skill-tree__branch` never actually matched anything (a branch's
     immediately-preceding sibling is always either that gutter or
     nothing), and every branch fell back to browsers' true default
     margin-top of 0. Combined with :first-child/:last-child (used
     further down for the trunk's own top/bottom) matching every level's
     OWN left/right branch rather than the tree's true first/last one —
     since each level is a separate parent, its own left branch IS the
     literal first-child of THAT parent — the cards ended up stacked
     back-to-back with no real gap, reading as overlapping/cramped. Every
     branch gets the gap by default now; only the tree's genuinely first
     card has it removed (the stem already provides that one), addressed
     unambiguously via its real position in the DOM rather than a
     sibling/first-child guess that didn't hold up once a gutter div sat
     in the way. */
  .skill-tree__branch {
    margin-top: var(--tree-m-gap);
    /* A touch more opaque than the shared --li-card-bg (0.55) here
       specifically — the trunk now correctly paints behind the card
       (see the connector fix below), but at the shared token's normal
       alpha a faint trace of it still blended through the glass. Scoped
       to this component only, not the shared token every other card on
       the site also uses. */
    background:
      linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 42%),
      rgba(28, 24, 40, 0.94);
  }

  .skill-tree__level--first .skill-tree__branch--left {
    margin-top: 0;
  }

  /* Real fix found and fixed here: a box's own background always paints
     before (behind) every one of its own descendants — pseudo-elements
     included, no matter their z-index — so a ::before living *inside*
     .skill-tree__branch could structurally never render behind that
     same branch's own card surface (confirmed by a zoomed screenshot:
     the line was clearly drawn on top of the chips, not tucked behind
     them). .skill-tree__connector is a real sibling instead — a plain
     <span> living BEFORE the branch inside .skill-tree__branch-wrap
     (skill-tree.njk) — so ordinary same-stacking-level rules (later DOM
     order paints on top) let the branch's own opaque surface actually
     cover it, the same way the desktop version's gutter line sits
     safely *beside* its cards rather than needing to hide under one.
     --branch-glow is redeclared on the wrap (not just the branch it
     already lived on) so the connector — a sibling, not a descendant of
     the branch — can inherit the correct left/right tint too. */
  .skill-tree__branch-wrap:has(.skill-tree__branch--left) {
    --branch-glow: rgba(63, 123, 248, 0.55);
  }

  .skill-tree__branch-wrap:has(.skill-tree__branch--right) {
    --branch-glow: rgba(184, 107, 248, 0.5);
  }

  .skill-tree__branch-wrap {
    display: block;
    position: relative;
  }

  /* The trunk: one line down the middle, not a rail off to the side —
     a soft static glow behind the crisp line, instead of a flat, thin
     rule (explicit direction: "beau, sexy"). box-shadow here is never
     animated, so there's no repaint cost to it — a fixed value the
     browser paints once and composites like everything else on this
     static line. Every card's connector gets a full segment (extends
     into the gap both above and below) by default; only the tree's true
     first and last branch — addressed the same explicit way as the
     margin fix above — get theirs trimmed to just one side. */
  .skill-tree__connector {
    display: block;
    position: absolute;
    top: calc(-1 * var(--tree-m-gap));
    left: 50%;
    width: 2px;
    height: calc(100% + 2 * var(--tree-m-gap));
    transform: translateX(-50%);
    background: linear-gradient(180deg, rgba(184, 107, 248, 0.5), rgba(63, 123, 248, 0.35));
    box-shadow: 0 0 8px 0 rgba(139, 92, 246, 0.35);
    border-radius: 0;
  }

  .skill-tree__level--first .skill-tree__branch-wrap:has(.skill-tree__branch--left) .skill-tree__connector {
    top: 0;
    height: calc(100% + var(--tree-m-gap));
  }

  .skill-tree__level--last .skill-tree__branch-wrap:has(.skill-tree__branch--right) .skill-tree__connector {
    height: var(--tree-m-gap);
  }

  /* A junction dot in the gap between consecutive cards, centered on the
     trunk — not before the very first card, which connects to the root
     via the stem instead, the same way the desktop tree never puts a
     junction dot between the root and level 1 either. Lives on the
     connector now too (::after of the sibling span, not of the branch)
     for the same occlusion reason, though the dot itself was never
     actually hidden behind anything — it sits in the empty gap, not
     over a card. Real coordinate bug found and fixed moving this from
     the branch to the connector: the dot's containing block is now the
     CONNECTOR's own box, whose top edge already sits `--tree-m-gap`
     above the branch — so reaching the same "middle of the gap above
     this card" point needs `+gap/2` from here, not the old `-gap/2`
     (which was correct only when this was positioned relative to the
     branch's own top instead). */
  .skill-tree__connector::after {
    content: "";
    position: absolute;
    top: calc(var(--tree-m-gap) / 2);
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--tree-m-dot);
    height: var(--tree-m-dot);
    border-radius: 50%;
    background: var(--branch-glow, #b86bf8);
    box-shadow: 0 0 0 3px rgba(20, 16, 30, 0.9), 0 0 10px 1px var(--branch-glow, rgba(139, 92, 246, 0.55));
  }

  .skill-tree__level--first .skill-tree__branch-wrap:has(.skill-tree__branch--left) .skill-tree__connector::after {
    display: none;
  }

  /* Re-guards the tap feedback above specifically at this width — the
     general prefers-reduced-motion block earlier in this file sets the
     same transition to `none`, but this mobile block's own `transition`
     comes later in the file, so on a mobile-width + reduced-motion visit
     it would otherwise win by source order alone despite matching the
     same media condition. */
  @media (prefers-reduced-motion: reduce) {
    .skill-tree__branch {
      transition: none;
    }

    .skill-tree__branch:active {
      transform: none;
    }
  }
}
