/* spine.css — The Spine (Vertebra) view, v2. An anatomical/mechanical spine that
   extends the Forge identity: a segmented cord (intervertebral disc gaps) with a
   forged fill that flows energy upward, day nodes as forged vertebra segments, rib
   connectors that draw each card off the cord, and every day rendered in a glass
   day-card. Mobile keeps a left-gutter cord; desktop centres it with cards
   alternating on curved ribs. State is carried in shape + fill + glow AND the
   visually-hidden label — never colour alone. Read-only lens; moves no number.
   The DNA layer at the foot (gated by .sp-dna, added by spine.js only when motion
   is allowed) winds this same flat spine into a twisting double helix around the axis. */

.spine-view { max-width: var(--maxread); margin: 0 auto; padding: var(--s5) var(--s4) var(--s8);
  --sp-cord: 22px;              /* mobile: cord centre-x in the left gutter */
  --sp-vert: 15px;             /* vertebra segment size */
}

/* ---------- header ---------- */
.spine-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2) var(--s4); margin-bottom: var(--s6); }
.spine-title { font-size: var(--t-2xl); font-weight: 800; letter-spacing: -.02em; position: relative; }
.spine-title::after { content: ""; display: block; height: 3px; width: 46px; margin-top: 6px; border-radius: 3px;
  background: linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent-2, var(--accent)) 90%, var(--accent))); }
.spine-sub { flex: 1; min-width: 16ch; color: var(--text-2); font-size: var(--t-sm); }
.spine-jump { padding: 8px 15px; border-radius: var(--r-pill); font-size: var(--t-sm); font-weight: 650;
  color: var(--accent-ink); background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  transition: background var(--dur), border-color var(--dur), transform var(--dur-fast), box-shadow var(--dur-fast); }
.spine-jump:hover { background: color-mix(in srgb, var(--accent) 18%, var(--surface)); border-color: var(--accent);
  box-shadow: 0 4px 14px -6px color-mix(in srgb, var(--accent) 55%, transparent); }
.spine-jump:active { transform: translateY(1px); }

/* ---------- the cord ---------- */
.spine { position: relative; list-style: none; margin: 0; padding: 0; }
.spine::before, .spine::after {
  content: ""; position: absolute; left: calc(var(--sp-cord) - 3px); width: 6px; border-radius: 6px; top: 6px; bottom: 6px;
}
/* unbuilt track: a faint channel segmented into vertebra discs */
.spine::before {
  background:
    linear-gradient(color-mix(in srgb, var(--border-2) 78%, transparent), color-mix(in srgb, var(--border-2) 78%, transparent));
  -webkit-mask: repeating-linear-gradient(to bottom, #000 0 15px, transparent 15px 21px);
          mask: repeating-linear-gradient(to bottom, #000 0 15px, transparent 15px 21px);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-2) 40%, transparent);
}
/* forged portion: a bright BRAIDED cord that fills to --progress. Two layers — a diagonal
   twist stripe over the flowing energy gradient — animate together so the cord reads as a
   twisting, glowing rope climbing the spine. */
.spine::after {
  background:
    repeating-linear-gradient(48deg, transparent 0 4px, color-mix(in srgb, #fff 16%, transparent) 4px 6px),
    linear-gradient(to bottom, var(--accent) 0%, var(--accent-2, var(--accent)) 55%, var(--accent) 100%);
  background-size: 14px 14px, 100% 260%;
  transform-origin: top; transform: scaleY(var(--progress, 0));
  transition: transform var(--dur-slow) var(--ease);
  box-shadow: 0 0 14px color-mix(in srgb, var(--accent) 45%, transparent),
              0 0 4px color-mix(in srgb, var(--accent) 70%, transparent);
}
@media (prefers-reduced-motion: no-preference) {
  .spine::after { animation: spineEnergy 3.4s linear infinite; }
}
@keyframes spineEnergy { from { background-position: 0 0, 0 0; } to { background-position: 14px 0, 0 -260%; } }

/* ---------- phase = a forged interchange on the cord ---------- */
.sp-phase { list-style: none; margin: 0 0 var(--s5); }
.sp-phase-head { position: relative; display: flex; align-items: center; gap: var(--s3); padding-left: 48px;
  margin: var(--s6) 0 var(--s3); font-size: var(--t-lg); font-weight: 750; letter-spacing: -.01em; }
.sp-phase:first-child .sp-phase-head { margin-top: 0; }
.sp-interchange { position: absolute; left: calc(var(--sp-cord) - 13px); width: 26px; height: 26px; z-index: 2;
  isolation: isolate; display: grid; place-items: center;
  font-size: 11px; font-weight: 800; color: var(--accent-ink); }
.sp-interchange::before { content: ""; position: absolute; inset: 0; z-index: -1; border-radius: 8px; transform: rotate(45deg);
  background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 22%, var(--surface)), var(--surface));
  border: 2.5px solid var(--accent);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 10%, transparent),
              0 4px 12px -4px color-mix(in srgb, var(--accent) 55%, transparent); }
.sp-phase-link { color: var(--text); }
.sp-phase-link:hover { color: var(--accent-ink); }
.sp-phase-sum { margin-left: auto; font-size: var(--t-xs); font-weight: 650; color: var(--text-3);
  font-variant-numeric: tabular-nums; padding: 3px 9px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--surface-2) 70%, transparent); }

/* ---------- days ---------- */
.sp-days { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
.sp-node { position: relative; }
.sp-link { display: flex; align-items: center; gap: var(--s3); padding: 3px var(--s2) 3px 44px;
  border-radius: var(--r2); text-decoration: none; color: inherit; outline-offset: 3px; }

/* vertebra segment, clipped onto the cord */
.sp-dot { position: absolute; left: calc(var(--sp-cord) - var(--sp-vert) / 2); top: 50%;
  width: var(--sp-vert); height: var(--sp-vert); transform: translateY(-50%); z-index: 3;
  display: grid; place-items: center; border-radius: 5px; background: var(--surface);
  border: 2px solid color-mix(in srgb, var(--accent) 40%, var(--border-2));
  box-shadow: 0 1px 3px rgba(0, 0, 0, .16);
  transition: width var(--dur-fast) var(--ease), height var(--dur-fast) var(--ease),
    background var(--dur-fast), border-color var(--dur-fast), box-shadow var(--dur-fast), transform var(--dur-fast); }
.sp-dot-core { width: 4px; height: 4px; border-radius: 1.5px; background: transparent;
  transition: background var(--dur-fast), box-shadow var(--dur-fast); }

/* rib: a short curved connector from the cord to the card */
.sp-conn { position: absolute; left: calc(var(--sp-cord) + 3px); top: 50%; transform: translateY(-50%);
  width: 20px; height: 16px; pointer-events: none;
  color: color-mix(in srgb, var(--accent) 36%, var(--border-2));
  transition: color var(--dur-fast); }
.sp-conn svg { width: 100%; height: 100%; display: block; }
.sp-conn path { stroke: currentColor; stroke-width: 2; stroke-linecap: round; }

/* glass day-card */
.sp-card { position: relative; display: flex; align-items: center; gap: var(--s3); flex: 1; min-width: 0;
  padding: 9px 12px; border-radius: var(--r2);
  background: color-mix(in srgb, var(--surface) 80%, transparent);
  border: 1px solid color-mix(in srgb, var(--border-2) 72%, transparent);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
  -webkit-backdrop-filter: blur(9px) saturate(1.06); backdrop-filter: blur(9px) saturate(1.06);
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast),
    border-color var(--dur-fast), background var(--dur-fast); }
.sp-card::before { content: ""; position: absolute; left: 0; top: 14%; bottom: 14%; width: 3px;
  border-radius: 0 3px 3px 0; background: transparent; transition: background var(--dur-fast); }

.sp-date { flex: none; display: flex; flex-direction: column; align-items: center; width: 34px; line-height: 1;
  font-variant-numeric: tabular-nums; }
.sp-date b { font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .04em; color: var(--text-3); font-weight: 700; }
.sp-date span { font-size: var(--t-lg); font-weight: 750; margin-top: 2px; }
.sp-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.sp-title { font-size: var(--t-sm); font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sp-meta-row { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; }
.sp-meta { flex: none; font-size: var(--t-xs); color: var(--text-3); font-variant-numeric: tabular-nums; font-weight: 600; }
.sp-theme { font-size: var(--t-xs); color: var(--text-3); opacity: .82; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* mini completion ring */
.sp-prog { --p: 0; position: relative; flex: none; width: 30px; height: 30px; border-radius: 50%;
  background: conic-gradient(var(--accent) calc(var(--p) * 360deg), color-mix(in srgb, var(--border-2) 55%, transparent) 0);
  display: grid; place-items: center; transition: background var(--dur); }
.sp-prog::before { content: ""; position: absolute; inset: 3.5px; border-radius: 50%; background: var(--surface); }
.sp-prog-num { position: relative; font-size: 9px; font-weight: 800; color: var(--text-2); font-variant-numeric: tabular-nums; }

/* ---- states — shape + fill + glow, plus the visually-hidden label ---- */
.sp-done .sp-dot { background: var(--accent); border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 14%, transparent), 0 1px 4px color-mix(in srgb, var(--accent) 40%, transparent); }
.sp-done .sp-dot-core { background: color-mix(in srgb, #fff 60%, var(--accent)); }
.sp-done .sp-card::before { background: color-mix(in srgb, var(--accent) 70%, transparent); }
.sp-done .sp-title { color: var(--text-2); }
.sp-done .sp-conn { color: color-mix(in srgb, var(--accent) 60%, transparent); }

.sp-current .sp-dot { width: calc(var(--sp-vert) + 5px); height: calc(var(--sp-vert) + 5px); border-radius: 6px;
  left: calc(var(--sp-cord) - (var(--sp-vert) + 5px) / 2);
  background: var(--accent); border-color: var(--accent);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 18%, transparent), 0 0 18px color-mix(in srgb, var(--accent) 62%, transparent);
  animation: spinePulse 2.4s var(--ease) infinite; }
.sp-current .sp-dot-core { width: 5px; height: 5px; background: #fff; box-shadow: 0 0 7px #fff; }
.sp-current .sp-card { border-color: color-mix(in srgb, var(--accent) 52%, var(--border-2));
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent),
              0 12px 30px -14px color-mix(in srgb, var(--accent) 55%, transparent); }
.sp-current .sp-card::before { background: var(--accent); }
.sp-current .sp-title { color: var(--text); font-weight: 750; }
.sp-current .sp-conn { color: var(--accent); }

/* subway "you are here" marker on the current day-card */
.sp-now { flex: none; padding: 3px 9px; border-radius: var(--r-pill); font-size: 10px; font-weight: 800;
  letter-spacing: .03em; text-transform: uppercase; white-space: nowrap; color: var(--accent-ink);
  background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent); }

.sp-upcoming .sp-title, .sp-upcoming .sp-date span { color: var(--text-3); }
.sp-upcoming .sp-conn { color: color-mix(in srgb, var(--border-2) 82%, transparent); }
.sp-upcoming .sp-card { background: color-mix(in srgb, var(--surface) 62%, transparent); }

.sp-off .sp-card { opacity: .62; padding-top: 6px; padding-bottom: 6px; }
.sp-off .sp-dot { width: 8px; height: 8px; border-radius: 2px; left: calc(var(--sp-cord) - 4px); }
.sp-off .sp-title { font-style: italic; color: var(--text-3); font-weight: 550; }

/* ---- hover / focus micro-interactions ---- */
.sp-link:hover .sp-card, .sp-link:focus-visible .sp-card {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--accent) 46%, var(--border-2));
  box-shadow: 0 10px 26px -10px color-mix(in srgb, var(--accent) 38%, rgba(0, 0, 0, .4)); }
.sp-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: var(--r2); }
.sp-link:hover .sp-dot { transform: translateY(-50%) scale(1.14); }
.sp-link:hover .sp-conn { color: var(--accent); }
.sp-link:hover .sp-prog { background: conic-gradient(var(--accent) calc(var(--p) * 360deg), color-mix(in srgb, var(--accent) 22%, transparent) 0); }

@keyframes spinePulse {
  0%, 100% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 18%, transparent), 0 0 18px color-mix(in srgb, var(--accent) 58%, transparent); }
  50%      { box-shadow: 0 0 0 8px color-mix(in srgb, var(--accent) 6%, transparent),  0 0 26px color-mix(in srgb, var(--accent) 78%, transparent); }
}

/* visually-hidden state label — announced to a screen reader, not shown */
.sp-vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }

/* ---- scroll choreography (progressive enhancement; nodes VISIBLE by default) ----
   Preferred path is pure CSS: where the browser has scroll-driven animations, each
   node reveals on a view() timeline as it enters. Fallback (older browsers, added by
   spine.js only): an IntersectionObserver flips .is-visible on BELOW-fold nodes and a
   rAF handler sets --scroll for the cord glow. If neither runs, the spine is fully drawn. */
/* 3D reveal: nodes emerge with a per-side page-turn — odd rotate in from the left, even from
   the right (--sp-ry, set by parity on desktop) — plus a tilt-up and slight zoom as they enter. */
.sp-node { --sp-ry: 0deg; }
.sp-days { perspective: 1100px; }
@keyframes spNodeIn {
  from { opacity: 0; transform: translateY(22px) rotateX(-18deg) rotateY(var(--sp-ry)) scale(.94); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .spine .sp-node { animation: spNodeIn linear both; animation-timeline: view(block); animation-range: entry 1% entry 42%; }
  }
}
/* JS fallback reveal — spine.js adds .sp-reveal to below-fold nodes only, .is-visible on enter */
.sp-reveal { opacity: 0; transform: translateY(22px) rotateX(-18deg) rotateY(var(--sp-ry)) scale(.94);
  transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease); }
.sp-reveal.is-visible { opacity: 1; transform: none; }
/* fallback glow tied to the rAF --scroll (0..1) the handler writes on .sp-fallback */
.sp-fallback.spine::after { box-shadow: 0 0 calc(10px + var(--scroll, 0) * 14px) color-mix(in srgb, var(--accent) calc(24% + var(--scroll, 0) * 34%), transparent),
  0 0 4px color-mix(in srgb, var(--accent) 70%, transparent); }

/* reduce-motion / motion-off: static, fully-drawn end-state — no pulse, no flow, no reveal */
@media (prefers-reduced-motion: reduce) {
  .sp-current .sp-dot { animation: none; }
  .spine::after, .sp-card, .sp-dot, .sp-conn, .sp-prog { transition: none; }
  .spine .sp-node, .spine::after { animation: none; }
  .sp-reveal, .sp-reveal.is-visible { opacity: 1; transform: none; transition: none; }
}
:root[data-motion="off"] .sp-current .sp-dot { animation: none; }
:root[data-motion="off"] .spine::after,
:root[data-motion="off"] .sp-card,
:root[data-motion="off"] .sp-dot,
:root[data-motion="off"] .sp-conn,
:root[data-motion="off"] .sp-prog { transition: none; }
:root[data-motion="off"] .spine .sp-node,
:root[data-motion="off"] .spine::after { animation: none; }
:root[data-motion="off"] .sp-reveal,
:root[data-motion="off"] .sp-reveal.is-visible { opacity: 1; transform: none; transition: none; }

/* NOTE: content-visibility:auto was applied here for the ~105-node scroll cost, but it
   SKIPS the off-screen node's initial paint — so the scroll-reveal (a transition / a
   view() timeline) had no hidden first frame to animate from and the card just POPPED in.
   The reveal is a UI feature the user asked to keep, so it wins: nodes stay fully painted.
   Cards are opaque and their helix transform is static, so 105 of them composite cheaply;
   the heavier day cards on the roadmap view keep their own content-visibility. */

/* ---- desktop: cord centred, day cards alternate off it on curved ribs ---- */
@media (min-width: 900px) {
  .spine { --sp-cord: 50%; }
  .spine::before { left: calc(50% - 3px); }
  .spine::after  { left: calc(50% - 3px); }

  .sp-phase-head { justify-content: center; padding-left: 0; text-align: center; }
  .sp-interchange { left: calc(50% - 13px); }
  .sp-phase-sum { position: absolute; right: 0; margin-left: 0; }

  /* per-side 3D reveal axis — cards turn in from their own side */
  .sp-node:nth-child(odd)  { --sp-ry: 13deg; }
  .sp-node:nth-child(even) { --sp-ry: -13deg; }

  /* a day occupies one half; odd → left (card hugs the centre), even → right */
  .sp-link { width: 50%; box-sizing: border-box; padding: 4px 26px 4px 0; }
  .sp-node:nth-child(odd)  .sp-link { flex-direction: row-reverse; }
  .sp-node:nth-child(even) .sp-link { margin-left: 50%; padding: 4px 0 4px 26px; }

  /* vertebra centring rides --sp-cord (now 50%), so no per-state left override here */

  /* rib curves out to the card side */
  .sp-conn { width: 24px; }
  .sp-node:nth-child(odd)  .sp-conn { left: auto; right: calc(50% + 3px); transform: translateY(-50%) scaleX(-1); }
  .sp-node:nth-child(even) .sp-conn { left: calc(50% + 3px); right: auto; transform: translateY(-50%); }

  /* the accent edge sits on the cord side of each card */
  .sp-node:nth-child(odd)  .sp-card::before { left: auto; right: 0; border-radius: 3px 0 0 3px; }
  .sp-node:nth-child(odd)  .sp-card { flex-direction: row-reverse; text-align: right; }
  .sp-node:nth-child(odd)  .sp-meta-row { justify-content: flex-end; }

  .sp-link:hover .sp-dot { transform: translateY(-50%) scale(1.14); }
}

/* ================================================================================== *
 *  Interactive live layer — JS-wired via .sp-live (spine.js initSpineChoreo).
 *  Everything here is a progressive enhancement driven by custom props the script
 *  writes; NONE of it exists under reduce-motion (the class is never added there).
 *  The cord stays perfectly straight — only the branching day list reacts, so the
 *  spine reads as a living structure, not a jittering page.
 * ================================================================================== */
.sp-live { isolation: isolate; --sp-vel: 0; --sp-vel-abs: 0; }

/* velocity warp — the day list liquid-shears off the straight cord as you scroll, then
   springs back to rest (spine.js decays --sp-vel to 0). The strange-but-alive effect. */
.sp-live .sp-days { transform: skewY(calc(var(--sp-vel) * 0.85deg)); transform-origin: var(--sp-cord) center; will-change: transform; }

/* the forged fill flares brighter the faster you move */
.sp-live.spine::after {
  box-shadow: 0 0 calc(12px + var(--sp-vel-abs) * 30px) color-mix(in srgb, var(--accent) calc(45% + var(--sp-vel-abs) * 40%), transparent),
              0 0 4px color-mix(in srgb, var(--accent) 75%, transparent);
}

/* comet — a bright blurred head that rides the cord down to the scroll position */
.sp-comet {
  position: absolute; left: calc(var(--sp-cord) - 4px); top: calc(6px + var(--scroll, 0) * (100% - 12px));
  width: 8px; height: 26px; transform: translateY(-50%); pointer-events: none; z-index: 2; border-radius: 8px;
  background: linear-gradient(color-mix(in srgb, #fff 42%, var(--accent)), color-mix(in srgb, var(--accent) 8%, transparent));
  filter: blur(.4px);
  box-shadow: 0 0 18px 5px color-mix(in srgb, var(--accent) 70%, transparent),
              0 0 42px 12px color-mix(in srgb, var(--accent) 28%, transparent);
  opacity: calc(.4 + var(--sp-vel-abs, 0) * .6);
  transition: opacity .25s var(--ease);
}

/* aura — a soft radial bloom behind the cards that follows the cursor */
/* A self-contained circle that FOLLOWS the cursor via transform (centre at --mx/--my), instead of
   a gradient painted inside a fixed box — so it's never clipped to half when the pointer nears an
   edge (the old `inset:-40px` box cut the circle off there). */
.sp-aura {
  position: absolute; left: 0; top: 0; width: 320px; height: 320px; border-radius: 50%;
  z-index: -1; pointer-events: none; opacity: 0;
  transform: translate(calc(var(--mx, 0px) - 160px), calc(var(--my, 0px) - 160px));
  background: radial-gradient(circle, color-mix(in srgb, var(--accent) 17%, transparent), transparent 70%);
  transition: opacity .4s var(--ease); will-change: transform, opacity;
}
.sp-aura-on .sp-aura { opacity: 1; }

/* pulses — nerve-like sparks that continuously fire UP the cord, so the spine always
   feels alive even when idle. Two, offset half a cycle. */
.sp-pulse {
  position: absolute; left: calc(var(--sp-cord) - 2px); width: 4px; height: 16px; border-radius: 4px;
  pointer-events: none; z-index: 1; top: 100%; opacity: 0;
  background: color-mix(in srgb, #fff 34%, var(--accent));
  box-shadow: 0 0 12px 3px color-mix(in srgb, var(--accent) 60%, transparent);
  animation: spPulse 3.6s linear infinite;
}
.sp-pulse-2 { animation-delay: 1.8s; }
@keyframes spPulse {
  0%   { top: 100%; opacity: 0; }
  10%  { opacity: .95; }
  90%  { opacity: .95; }
  100% { top: 0%;   opacity: 0; }
}

/* magnetic ignite band — spine.js sets --sp-mag (0..1) on vertebrae near the cursor's
   height; they brighten, their core lights, and their ring glows. No transform (so it
   never fights hover/reveal) — a wave of light that follows the pointer down the spine. */
.sp-live .sp-node { --sp-mag: 0; }
/* only lit cards (.sp-mag-on, toggled by spine.js) get a filter — the other ~105 stay filter-free
   so they don't each spawn a render surface for a no-op brightness(1). */
.sp-live .sp-card { transition: filter .18s var(--ease); }
.sp-live .sp-mag-on .sp-card { filter: brightness(calc(1 + var(--sp-mag) * 0.17)); }
.sp-live .sp-dot-core { box-shadow: 0 0 calc(var(--sp-mag) * 12px) color-mix(in srgb, var(--accent) 82%, transparent); }
.sp-live .sp-dot { border-color: color-mix(in srgb, var(--accent) calc(40% + var(--sp-mag) * 55%), var(--border-2)); }
.sp-live .sp-prog { box-shadow: 0 0 calc(var(--sp-mag) * 11px) color-mix(in srgb, var(--accent) 55%, transparent); }

/* the live layer is never mounted under reduced motion (spine.js returns null), but
   neutralise defensively in case the setting flips while it is on screen */
@media (prefers-reduced-motion: reduce) {
  .sp-comet, .sp-aura, .sp-pulse { display: none; }
  .sp-live .sp-days { transform: none; }
  .sp-live .sp-card { filter: none; }
}
:root[data-motion="off"] .sp-comet,
:root[data-motion="off"] .sp-aura,
:root[data-motion="off"] .sp-pulse { display: none; }
:root[data-motion="off"] .sp-live .sp-days { transform: none; }
:root[data-motion="off"] .sp-live .sp-card { filter: none; }

/* ================================================================================== *
 *  DNA DOUBLE HELIX — the flat spine refactored into a twisting double strand.
 *  A pure visual re-interpretation of the SAME markup: spine.js writes per-node trig
 *  (--sp-x sway, --sp-cz depth, --sp-depth 0..1, --sp-rung base-pair span, --sp-mx the
 *  complementary bead, --sp-side fly-in direction) + a .sp-strand-b class for the 2nd hue.
 *  Days are nucleotides docking on two sugar–phosphate backbones that wind 180° out of
 *  phase around a central axis; a base-pair RUNG crosses between them, pinching to nothing
 *  at each crossover and opening widest mid-groove. The existing forged cord is kept as the
 *  slim helix AXIS (progress energy still climbs it; the comet/pulses/aura core still rides
 *  it). Gated by .sp-dna — added only when motion is allowed — so reduce-motion / no-JS keep
 *  the flat spine above. Strip the one class and the view reverts wholesale.
 * ================================================================================== */
/* The helix needs more room than the reading column, so widen the view for it. NO overflow
   clip: an earlier `overflow-x: clip` here is what trimmed the cursor aura at the edges.
   Instead the radii are kept so centre ± sway ± half-card provably fits inside the widened
   view on mobile and desktop — nothing overflows, so the aura bloom spills to the edges uncut. */
.spine-view:has(.sp-dna) { max-width: min(980px, 94vw); }

.sp-dna {
  --sp-cord: 50%;                          /* both strands wind around a centred axis on all widths */
  --sp-R: clamp(34px, 15vw, 230px);        /* backbone sway radius — centre ± R ± half-card stays inside */
  --sp-Rz: clamp(70px, 18vw, 185px);       /* depth of the twist (the main 3D cue — costs no width) */
  --sp-persp: 1120px;
  --sp-strand-a: var(--accent);            /* the two complementary strands read in two hues */
  --sp-strand-b: var(--accent-2, var(--accent));
  isolation: isolate;                      /* own compositing context → stable 3D layers, no smears */
}
/* the forged cord recedes to a faint HAIRLINE axis the two strands wind around — any wider
   and it reads as a central spine with branches instead of a double helix. The --progress
   fill still climbs it (invisible at 0), just quiet behind the strands; comet/pulses hidden. */
.sp-dna.spine::before { width: 2px; left: calc(50% - 1px); opacity: .12; }
.sp-dna.spine::after  { width: 2px; left: calc(50% - 1px); opacity: .5; }
.sp-dna .sp-days { gap: 10px; }

/* phase header = a crossover junction on the axis. In-flow so the centred title never sits
   under the diamond. CRITICAL: the interchange stays `position: relative`, NOT `static` — its
   ::before diamond is `position:absolute; inset:0; transform:rotate(45deg)`, so a static
   (non-positioned) interchange lets the ::before escape to fill the whole .sp-phase-head and
   rotate into a giant diagonal bar. `relative` re-anchors it to the 26px diamond. */
.sp-dna .sp-phase-head { justify-content: center; flex-wrap: wrap; padding-left: 0; gap: var(--s2) var(--s3); }
.sp-dna .sp-interchange { position: relative; left: auto; }
.sp-dna .sp-phase-sum { position: relative; right: auto; margin-left: var(--s2); }
/* spine.js prepends the strands <svg> as the ol's first child, so the first phase is no longer
   :first-child — re-zero its top margin off the first LI instead. */
.sp-dna .sp-phase:first-of-type .sp-phase-head { margin-top: 0; }

/* the two continuous BACKBONE strands — flowing sine curves spine.js threads through the
   per-row strand points, so the beads/dots sit on them as nucleotides. Behind the cards. */
.sp-strands { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; overflow: visible; }
/* bg-coloured casing under each strand → the front strand's casing cuts a clean gap in the back
   strand where they cross, giving the over/under weave (subway-map trick). */
.sp-strand-casing { fill: none; stroke: var(--bg); stroke-width: 14; stroke-linecap: round; stroke-linejoin: round; }
/* Each strand is drawn as stacked paths so the glow uses NO filter at all. The strand runs the whole
   helix (~8500px), and a drop-shadow over a path that tall needs a filter buffer past the GPU's max
   texture size — so the browser clipped it at the TOP (the residual top-node flicker) and paid to
   composite that buffer on every scroll (the slight scroll cost). Instead the halo is faked with two
   translucent stacked strokes (round caps, no filter): no oversized buffer, nothing to re-raster,
   cheap to scroll. The crisp CORE on top width-BREATHES; filter-free, its width change is a clean
   anti-aliased redraw (the earlier shimmer came only from animating a FILTERED path's geometry). */
.sp-strand-glow, .sp-strand-halo, .sp-strand-line { fill: none; stroke-linecap: round; stroke-linejoin: round; }
.sp-strand-glow { stroke-width: 11px; }
.sp-strand-halo { stroke-width: 7px; }
.sp-strand-line { stroke-width: 4.75px; }
.sp-strand-a-glow { stroke: color-mix(in srgb, var(--accent) 13%, transparent); }
.sp-strand-a-halo { stroke: color-mix(in srgb, var(--accent) 30%, transparent); }
.sp-strand-a-line { stroke: color-mix(in srgb, var(--sp-strand-a, var(--accent)) 92%, transparent);
  animation: spStrandBreathe 4.2s ease-in-out infinite; }
.sp-strand-b-glow { stroke: color-mix(in srgb, var(--accent-2, var(--accent)) 13%, transparent); }
.sp-strand-b-halo { stroke: color-mix(in srgb, var(--accent-2, var(--accent)) 30%, transparent); }
.sp-strand-b-line { stroke: color-mix(in srgb, var(--sp-strand-b, var(--accent-2, var(--accent))) 92%, transparent);
  animation: spStrandBreathe 4.2s ease-in-out infinite; animation-delay: -2.1s; }
@keyframes spStrandBreathe { 0%, 100% { stroke-width: 4px; } 50% { stroke-width: 5.5px; } }   /* 4 → 5.5 → 4 ≈ +38%, clearly perceptible, purely in-place */

/* TRAVELLING LIGHT — a soft pulse of colour that flows DOWN each strand. Why this one can't flicker
   like the old one: (1) it rides the CORE, which now carries NO filter, so its motion can't re-raster a
   drop-shadow; (2) it is a SEAMLESSLY-TILED stream, not one lone pulse. The dash period (25+75 = 100)
   divides pathLength (1000, set in spine.js) exactly 10 times, so ten identical pulses are always spaced
   along the line — the path is never dark and no single pulse's exit is a conspicuous event. The
   keyframe shifts stroke-dashoffset by exactly one period (-100), and since the pattern repeats every
   100 units, offset -100 is pixel-identical to 0 → the loop restarts with zero jump. The two strands are
   offset in time so their streams don't pulse in unison. Gated with the whole SVG under reduce-motion. */
.sp-strand-flow { fill: none; stroke-linecap: round; stroke-width: 3.5px; stroke-dasharray: 25 75;
  animation: spStrandFlow 3.4s linear infinite; }
.sp-strand-a-flow { stroke: color-mix(in srgb, color-mix(in srgb, #fff 55%, var(--accent)) 72%, transparent); }
.sp-strand-b-flow { stroke: color-mix(in srgb, color-mix(in srgb, #fff 55%, var(--accent-2, var(--accent))) 72%, transparent);
  animation-delay: -1.7s; }
@keyframes spStrandFlow { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -100; } }   /* one full period → seamless, no restart pop */

/* hide the whole strands SVG if motion is off, so a mid-session toggle can't
   leave the curved sine backbones drawn behind the flattened column */
@media (prefers-reduced-motion: reduce) { .sp-strands { display: none; } }
:root[data-motion="off"] .sp-strands { display: none; }

/* each day is its own little 3D scene, so a 105-node helix never distorts as one long
   perspective would; the card sways + recedes within it. Per-node strand colours: default is
   strand A, and spine.js tags every other node .sp-strand-b so the two backbones read apart. */
.sp-dna .sp-node { perspective: var(--sp-persp);
  --sp-col: var(--sp-strand-a); --sp-col-mirror: var(--sp-strand-b); }
.sp-dna .sp-node.sp-strand-b { --sp-col: var(--sp-strand-b); --sp-col-mirror: var(--sp-strand-a); }
.sp-dna .sp-conn { display: none; }        /* the rib is replaced by the base-pair rung */

/* base-pair RUNG — a luminous bar spanning the two backbones, centred on the axis; its
   half-span is |sin| (--sp-rung) so it pinches to nothing at the crossover and opens widest
   mid-groove. Depth-lit so front rungs read brighter than the ones twisting away. */
.sp-dna .sp-node::before {
  content: ""; position: absolute; top: 50%; left: 50%; z-index: 0; height: 4px; pointer-events: none;
  width: calc(var(--sp-rung, 0) * var(--sp-R) * 2);
  transform: translate(-50%, -50%); border-radius: 4px;
  transition: transform .7s var(--ease), opacity .5s var(--ease), box-shadow .3s var(--ease);
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--sp-col-mirror) 80%, transparent),
    color-mix(in srgb, #fff 40%, transparent) 50%,
    color-mix(in srgb, var(--sp-col) 80%, transparent));
  box-shadow: 0 0 8px color-mix(in srgb, var(--sp-col) 24%, transparent);
  opacity: calc(.34 + .5 * var(--sp-depth, 1));
}

/* the complementary strand's BEAD — the backbone point on the OTHER strand at this rung, at
   --sp-mx (= −sway). The card's own backbone bead is its .sp-dot (moved onto its strand
   below); the two bead trails sway in antiphase and read as the two intertwining backbones.
   Its depth is the mirror of the card's (bright when the card recedes), so the strands cross. */
.sp-dna .sp-node::after {
  content: ""; position: absolute; top: 50%; z-index: 1; pointer-events: none;
  left: calc(50% + var(--sp-mx, 0) * var(--sp-R));
  width: 14px; height: 14px; margin: -7px 0 0 -7px; border-radius: 50%;
  background: radial-gradient(circle at 36% 30%, #fff, var(--sp-col-mirror) 78%);
  box-shadow: 0 0 6px color-mix(in srgb, var(--sp-col-mirror) 85%, transparent),
              0 0 16px color-mix(in srgb, var(--sp-col-mirror) 45%, transparent);
  opacity: calc(.5 + .45 * (1 - var(--sp-depth, 1)));
  transform: scale(1); transition: transform .6s var(--ease-spring, var(--ease)), opacity .5s var(--ease), box-shadow .3s var(--ease);
}

/* move the card's vertebra dot off the axis onto its strand's sine — the bead it docks at.
   In DNA mode it's a round, translucent nucleotide bead (NOT the flat spine's opaque rounded
   SQUARE) tinted its own strand's hue, so it sits flush on the glowing backbone instead of
   occluding the travelling light and reading as a glitchy box. Depth-lit like its card. */
.sp-dna .sp-dot { transform: translateY(-50%) translateX(calc(var(--sp-x, 0) * var(--sp-R)));
  left: calc(var(--sp-cord) - 5.5px); width: 11px; height: 11px; border: 0; border-radius: 50%;
  background: radial-gradient(circle at 36% 30%, #fff, var(--sp-col) 78%);
  box-shadow: 0 0 6px color-mix(in srgb, var(--sp-col) 80%, transparent),
              0 0 15px color-mix(in srgb, var(--sp-col) 40%, transparent);
  opacity: calc(.5 + .45 * var(--sp-depth, 1));
  transition: transform .6s var(--ease-spring, var(--ease)), opacity .5s var(--ease), box-shadow .3s var(--ease); }
.sp-dna .sp-upcoming .sp-dot-core { background: color-mix(in srgb, var(--sp-col) 60%, transparent); }
.sp-dna .sp-upcoming .sp-card::before { background: color-mix(in srgb, var(--sp-col) 42%, transparent); }

/* The anchor stays full-width for LAYOUT but must not be CLICKABLE across the empty gutters
   beside the docked card. pointer-events:none makes it click-through; only the card re-enables
   hits, so the clickable region equals the visible rectangle. Clicks still bubble to the
   anchor's handler, and it stays keyboard-focusable (pointer-events doesn't touch Tab/Enter). */
.sp-dna .sp-link { display: flex; flex-direction: row; align-items: center; justify-content: center;
  width: 100%; margin: 0; padding: 6px 0; box-sizing: border-box; pointer-events: none; }

.sp-dna .sp-card {
  flex: none; width: clamp(178px, 48vw, 296px); transform-origin: center; backface-visibility: hidden;
  transform: translateX(calc(var(--sp-x, 0) * var(--sp-R)))
             translateZ(calc((var(--sp-cz, 1) - 1) * var(--sp-Rz)));
  /* Depth reads through perspective-scale + opacity ONLY — no blur, so text stays crisp, and
     no backdrop-filter (glass blur on a dozen 3D cards recomputing each scroll frame was the
     paint hog). Opaque card. */
  opacity: calc(.62 + .38 * var(--sp-depth, 1));
  /* brightness filter is applied only to lit cards via .sp-live .sp-mag-on .sp-card (above) */
  background: color-mix(in srgb, var(--surface) 93%, transparent);
  -webkit-backdrop-filter: none; backdrop-filter: none;
  z-index: calc(40 + var(--sp-depth, 1) * 60);
  pointer-events: auto; cursor: pointer;   /* the card IS the click target (see .sp-link above) */
  /* transform .75s ease-out is the DOCK glide (attach on scroll-down, the same reversed on
     scroll-up) — see the .sp-dock-pending state below. Hover overrides it to stay snappy. */
  transition: transform .75s cubic-bezier(.2, .6, .25, 1), opacity .5s var(--ease),
    box-shadow .28s var(--ease), border-color .28s var(--ease), filter .2s var(--ease);
}
/* keep the state tints (they'd be clobbered by the opaque bg above) — blur-free + solid */
.sp-dna .sp-current .sp-card { background: color-mix(in srgb, var(--accent) 11%, var(--surface)); }
.sp-dna .sp-upcoming .sp-card { background: color-mix(in srgb, var(--surface) 90%, transparent); }

/* hover keeps the sway and lifts toward the viewer. It hangs off .sp-card:hover, not
   .sp-link:hover — the anchor is pointer-events:none so it never reports :hover; the card
   (pointer-events:auto) does. focus-visible stays on the anchor (keyboard, not pointer). */
.sp-dna .sp-card:hover, .sp-dna .sp-link:focus-visible .sp-card {
  transform: translateX(calc(var(--sp-x, 0) * var(--sp-R)))
             translateZ(calc((var(--sp-cz, 1) - 1) * var(--sp-Rz) + 34px)) translateY(-3px);
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border-2));
  box-shadow: 0 16px 36px -16px color-mix(in srgb, var(--accent) 50%, rgba(0, 0, 0, .5));
  transition: transform .28s var(--ease), box-shadow .28s var(--ease), border-color .28s var(--ease);
}
/* hovering a card lights up ITS base pair — the rung brightens and the complementary bead flares,
   so you can see which nucleotides a day bridges. (:has scopes it to the hovered node only.) */
.sp-dna .sp-node:has(.sp-card:hover)::before {
  opacity: 1; height: 5px; box-shadow: 0 0 16px color-mix(in srgb, var(--sp-col) 60%, transparent); }
.sp-dna .sp-node:has(.sp-card:hover)::after {
  box-shadow: 0 0 12px color-mix(in srgb, var(--sp-col-mirror) 90%, transparent),
              0 0 26px color-mix(in srgb, var(--sp-col-mirror) 55%, transparent); }
/* the current day's base pair reads a touch brighter so "you are here" stands out on the strand */
.sp-dna .sp-current::before { opacity: .95; box-shadow: 0 0 12px color-mix(in srgb, var(--accent) 50%, transparent); }

/* ---- DOCK — nodes come IN FROM THE SIDE and attach as they scroll into view, and detach back
   OUT to the side when they leave the bottom on scroll-up: one REVERSIBLE transition. ----
   spine.js toggles a single class, .sp-dock-pending: present = detached (card flung out along its
   own strand's side + hidden, rung retracted, bead shrunk), absent = docked (the resting sway).
   Because attach/detach ride the card/rung/bead TRANSITIONS (declared above) rather than a one-shot
   keyframe, scroll-DOWN glides a card home and scroll-UP glides it back out with the very same
   motion reversed — and a glide interrupted mid-way smoothly turns around. Gated on BOTH the
   reduce-motion media AND :root:not([data-motion="off"]) so either one disables the dock (a card
   can never be left flung + hidden if motion is turned off mid-session). */
.sp-dna .sp-node { animation: none; }   /* cancel the flat spine's node page-turn — the card moves, not the li */
@media (prefers-reduced-motion: no-preference) {
  :root:not([data-motion="off"]) .sp-dna .sp-dock-pending .sp-card { opacity: 0;
    transform: translateX(calc(var(--sp-side, 1) * (var(--sp-R) + 175px)))
               translateZ(calc((var(--sp-cz, 1) - 1) * var(--sp-Rz) - 55px))
               rotateY(calc(var(--sp-side, 1) * -24deg)); }
  :root:not([data-motion="off"]) .sp-dna .sp-dock-pending::before { opacity: 0; transform: translate(-50%, -50%) scaleX(0); }
  :root:not([data-motion="off"]) .sp-dna .sp-dock-pending::after { opacity: 0; transform: scale(0); }
  /* the card's own-strand bead fades WITH its card, so scrolling never leaves a lone bead pinned to the strand */
  :root:not([data-motion="off"]) .sp-dna .sp-dock-pending .sp-dot { opacity: 0;
    transform: translateY(-50%) translateX(calc(var(--sp-x, 0) * var(--sp-R))) scale(0); }
}

/* desktop resets — neutralise the flat alternating layout (beats the nth-child rules above) */
@media (min-width: 900px) {
  .sp-dna .sp-node:nth-child(odd) .sp-link,
  .sp-dna .sp-node:nth-child(even) .sp-link { width: 100%; margin: 0; padding: 6px 0;
    flex-direction: row; justify-content: center; }
  .sp-dna .sp-node:nth-child(odd) .sp-card { flex-direction: row; text-align: left; }
  .sp-dna .sp-node:nth-child(odd) .sp-meta-row { justify-content: flex-start; }
  .sp-dna .sp-node:nth-child(odd) .sp-card::before { left: 0; right: auto; border-radius: 0 3px 3px 0; }
}

/* The helix is deliberately STATIC per-card (see spine.js): re-transforming every card off
   --scroll each frame re-painted the whole strand on every wheel tick. The flat per-list
   velocity-skew would fight the 3D, so neutralise it — aliveness comes from the strand flow. */
.sp-dna.sp-live .sp-days { transform: none; will-change: auto; }   /* never transforms in DNA → no wasteful promotion */

/* each day-node forms its OWN 3D context (perspective), so the axis comet/pulses can't sit
   behind the cards — they'd paint OVER the strand as a stray streak. The axis is mostly hidden
   behind the cards anyway, so retire ONLY those two. The cursor AURA is kept (the hover bloom),
   at z-index:-1 behind the cards and, with the overflow clip gone above, no longer trimmed. */
.sp-dna .sp-comet, .sp-dna .sp-pulse { display: none; }

/* defensive: if motion is toggled off while a helix is already on screen (spine.js gates at
   render, so this only catches a mid-session flip) flatten to a clean centred column */
@media (prefers-reduced-motion: reduce) {
  .sp-dna .sp-card { transform: none; filter: none; opacity: 1; transition: none; }
  .sp-dna .sp-node::before, .sp-dna .sp-node::after { display: none; }
  .sp-dna .sp-dot { transform: translateY(-50%); }
}
:root[data-motion="off"] .sp-dna .sp-card { transform: none; filter: none; opacity: 1; transition: none; }
:root[data-motion="off"] .sp-dna .sp-node::before,
:root[data-motion="off"] .sp-dna .sp-node::after { display: none; }
:root[data-motion="off"] .sp-dna .sp-dot { transform: translateY(-50%); }
