/* ============================================================
   WASP · changelog
   The build log. Inherits Obsidian Chrome from main.css and the
   document scaffold (.doc, .doc__head, .doc__foot, .doc__pill)
   from legal.css.

   Concept: the ladder. The engine section of the homepage reads as
   rungs on a meter that fills as pressure climbs. This page is the
   same figure turned on its side: a spine down the page, one node
   per release, filling as you scroll.

   Like the legal pages, this is a reading surface. No GSAP, no
   pinning, no scrub. Progress and reveal are a few lines of vanilla
   JS, and everything is legible with JS switched off.
   ============================================================ */

/* The homepage runs its own smoothing (Lenis) and main.css disables the
   native behaviour for it. This page has no such library, and its month
   links jump a long way, so give them the browser's own smoothing. */
@media (prefers-reduced-motion: no-preference) {
  html:has(.cl) { scroll-behavior: smooth; }
}

.cl {
  /* The spine sits exactly on the boundary between the two columns,
     so every node, tick and fill anchors off this one expression. */
  --cl-rail: 9.5rem;
  --cl-gap: 2.5rem;
  --cl-spine: calc(var(--cl-rail) + var(--cl-gap));
  --cl-pad: clamp(1.25rem, 2.4vw, 2rem);
  --cl-node: 9px;
}

/* ---------- masthead extras ---------- */
.cl__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr));
  gap: 1.75rem 2.5rem;
  margin-top: clamp(2.25rem, 5vh, 3.25rem);
  padding-top: clamp(1.75rem, 4vh, 2.5rem);
  border-top: 1px solid var(--outline-variant);
}

.cl__stat strong {
  display: block;
  font-size: clamp(1.5rem, 1.15rem + 1.5vw, 2.3rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.025em;
  color: var(--white);
  margin-bottom: .55rem;
  font-variant-numeric: tabular-nums;
}

.cl__stat .mono-label { display: block; }

/* ---------- sticky control bar ---------- */
.cl__controls {
  position: sticky;
  top: var(--nav-h);
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .75rem 1.5rem;
  margin-top: clamp(2.5rem, 6vh, 4rem);
  /* Bled past the page gutter on both sides, with matching padding so the
     chips stay aligned to the content. The spine sits on the content's left
     edge, so without the bleed a milestone and its halo pass BESIDE the bar
     instead of behind it, which reads as a half-clipped glowing dot. */
  margin-left: calc(var(--gutter) * -1);
  margin-right: calc(var(--gutter) * -1);
  padding: .85rem var(--gutter);
  border-bottom: 1px solid var(--outline-variant);
  /* Solid, not glass. This page is dense text, and even a lightly
     translucent bar leaves it legible through the chips. */
  background: var(--onyx);
}

/* The strip between the top of the viewport and the stuck bar. Without
   it, entries scroll up into the nav's gradient and collide with the
   nav's own links. Inherits the bar's bleed, so left/right are 0 here. */
.cl__controls::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: var(--nav-h);
  background: var(--onyx);
  pointer-events: none;
}

.cl__filters {
  display: flex;
  flex-wrap: wrap;
  gap: .45rem;
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.cl__filters legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

.cl__filter {
  display: inline-flex;
  align-items: baseline;
  gap: .45rem;
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .07em;
  text-transform: uppercase;
  padding: .45rem .8rem;
  border: 1px solid var(--outline-variant);
  border-radius: 999px;
  color: var(--muted);
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}

.cl__filter:hover { color: var(--alabaster); border-color: var(--outline); }

.cl__filter[aria-pressed="true"] {
  color: var(--onyx);
  background: var(--alabaster);
  border-color: var(--alabaster);
}

.cl__filter-n {
  font-variant-numeric: tabular-nums;
  opacity: .55;
}

.cl__filter[aria-pressed="true"] .cl__filter-n { opacity: .6; }

.cl__jump {
  display: flex;
  gap: .9rem;
  flex-wrap: wrap;
  min-width: 0;
}

.cl__jump a {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
  transition: color var(--dur-fast) var(--ease);
}

.cl__jump a:hover { color: var(--white); }

/* Announced when a filter changes; visually redundant with the chips. */
.cl__status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }

/* ---------- the spine ---------- */
.cl__timeline {
  position: relative;
  padding: clamp(2.75rem, 7vh, 4.5rem) 0 clamp(3.5rem, 9vh, 6rem);
  /* No-JS fallback numbering only: entry count + 1, counting down.
     With JS the numbers are stamped in and this is switched off, so
     drifting out of date here costs nothing to anyone running scripts. */
  counter-reset: entry 24;
}

/* Track and fill are both pinned to the column boundary. */
.cl__timeline::before,
.cl__progress {
  content: "";
  position: absolute;
  left: var(--cl-spine);
  width: 1px;
  top: clamp(2.75rem, 7vh, 4.5rem);
}

.cl__timeline::before {
  bottom: clamp(3.5rem, 9vh, 6rem);
  background: var(--outline-variant);
}

.cl__progress {
  height: 0;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--slate-2) 25%, transparent),
    var(--slate-2));
  pointer-events: none;
}

/* ---------- month markers ---------- */
.cl__month + .cl__month { margin-top: clamp(2.5rem, 6vh, 4rem); }

.cl__month-head {
  display: grid;
  grid-template-columns: var(--cl-rail) minmax(0, 1fr);
  column-gap: var(--cl-gap);
  align-items: center;
  margin-bottom: clamp(1.75rem, 4vh, 2.75rem);
  scroll-margin-top: calc(var(--nav-h) + 5rem);
}

.cl__month-head h2 {
  grid-column: 1;
  text-align: right;
  font-family: var(--font-mono);
  font-size: .68rem;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--alabaster);
}

/* The tick that crosses the spine, echoing the engine meter's rungs. */
.cl__month-head::after {
  content: "";
  grid-column: 2;
  justify-self: start;
  width: 1.5rem;
  height: 1px;
  margin-left: calc(var(--cl-gap) * -1);
  background: var(--outline);
}

.cl__month[hidden] { display: none; }

/* ---------- entries ---------- */
.cl__entry {
  position: relative;
  display: grid;
  grid-template-columns: var(--cl-rail) minmax(0, 1fr);
  column-gap: var(--cl-gap);
  padding-bottom: clamp(2.5rem, 6vh, 3.75rem);
}

.cl__entry[hidden] { display: none; }

/* The node on the spine. Dim until the fill line actually reaches it, so
   the milestones light one by one as you read down rather than all at
   once when the entry happens to enter the viewport. */
.cl__entry::before {
  content: "";
  position: absolute;
  left: var(--cl-spine);
  top: .5rem;
  width: var(--cl-node);
  height: var(--cl-node);
  margin-left: calc(var(--cl-node) / -2);
  border-radius: 50%;
  background: var(--onyx);
  border: 1px solid var(--outline);
  z-index: 1;
  transition: background .5s var(--ease),
              border-color .5s var(--ease),
              box-shadow .5s var(--ease);
}

/* Reached. White centre with a slate halo: the halo is what makes it read
   as illuminated rather than just filled, and slate keeps it on the one
   accent the site allows. */
.cl__entry.is-lit::before {
  background: var(--white);
  border-color: var(--white);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--slate-2) 22%, transparent),
              0 0 14px 2px color-mix(in srgb, var(--slate-2) 50%, transparent);
  animation: cl-ignite .6s var(--ease);
}

/* One-shot as the line arrives. Re-fires if you scroll back up and down,
   because the class comes off and goes on again. */
@keyframes cl-ignite {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.9); }
  100% { transform: scale(1); }
}

/* The newest release breathes, the way the live marker does on the
   homepage's follow-through chart. */
.cl__entry--latest::after {
  content: "";
  position: absolute;
  left: var(--cl-spine);
  top: .5rem;
  width: var(--cl-node);
  height: var(--cl-node);
  margin-left: calc(var(--cl-node) / -2);
  border-radius: 50%;
  border: 1px solid var(--slate-2);
  animation: cl-pulse 2.8s var(--ease) infinite;
  pointer-events: none;
}

@keyframes cl-pulse {
  0%   { transform: scale(1);   opacity: .8; }
  70%  { transform: scale(3.2); opacity: 0; }
  100% { transform: scale(3.2); opacity: 0; }
}

/* ---------- entry meta (the rail) ---------- */
.cl__meta {
  grid-column: 1;
  text-align: right;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .4rem;
  padding-top: .1rem;
}

.cl__idx {
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .08em;
  color: var(--slate-2);
  font-variant-numeric: tabular-nums;
}

/* No-JS fallback numbering. A CSS counter skips entries a filter has set
   to display:none, so once JS has stamped stable numbers in, this is
   switched off rather than left to fight with them. */
.cl__idx::before { content: counter(entry, decimal-leading-zero); }
.cl--numbered .cl__idx::before { content: none; }

.cl__entry { counter-increment: entry -1; }

.cl__date {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .04em;
  color: var(--muted);
  text-transform: uppercase;
  line-height: 1.35;
}

.cl__latest {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin-top: .15rem;
  font-family: var(--font-mono);
  font-size: .6rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--slate-2);
}

.cl__latest i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--slate-2);
}

/* ---------- entry body ---------- */
.cl__entry-body {
  grid-column: 2;
  padding-left: var(--cl-pad);
  min-width: 0;
}

.cl__title {
  font-size: clamp(1.3rem, 1.1rem + .75vw, 1.75rem);
  line-height: 1.16;
  letter-spacing: -.018em;
  font-weight: 700;
  color: var(--white);
  scroll-margin-top: calc(var(--nav-h) + 5rem);
}

.cl__stand {
  margin-top: .65rem;
  max-width: 58ch;
  font-size: 1rem;
  line-height: 1.62;
  color: var(--muted);
}

.cl__items {
  list-style: none;
  margin: 1.35rem 0 0;
  padding: 0;
  display: grid;
  gap: .8rem;
  max-width: 66ch;
}

.cl__item {
  display: grid;
  grid-template-columns: 5.75rem minmax(0, 1fr);
  gap: .9rem;
  align-items: baseline;
}

.cl__item[hidden] { display: none; }

.cl__item p {
  font-size: .975rem;
  line-height: 1.6;
  color: var(--alabaster);
}

.cl__item strong { color: var(--white); font-weight: 600; }

/* ---------- tags ----------
   Colour on this site is the escalation scale and nothing else, so the
   tags do not get the usual green/blue/red changelog treatment. Slate
   for what is new, chrome for what improved, dashed and quiet for fixes. */
.cl__tag {
  display: inline-block;
  justify-self: start;
  font-family: var(--font-mono);
  font-size: .6rem;
  letter-spacing: .085em;
  text-transform: uppercase;
  padding: .24rem .55rem;
  border: 1px solid;
  border-radius: 999px;
  white-space: nowrap;
}

.cl__item[data-tag="new"] .cl__tag {
  color: var(--slate-2);
  border-color: color-mix(in srgb, var(--slate-2) 48%, transparent);
  background: color-mix(in srgb, var(--slate-2) 11%, transparent);
}

.cl__item[data-tag="improved"] .cl__tag {
  color: var(--alabaster);
  border-color: var(--outline);
}

.cl__item[data-tag="fixed"] .cl__tag {
  color: var(--muted);
  border-color: var(--outline-variant);
  border-style: dashed;
}

/* ---------- reveal ----------
   Opt-in: the .cl--anim class is only added by JS, so with scripting
   off every entry is visible from the start. */
.cl--anim .cl__entry {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}

.cl--anim .cl__entry.is-in {
  opacity: 1;
  transform: none;
}

/* ---------- closing note ---------- */
.cl__note {
  display: grid;
  grid-template-columns: var(--cl-rail) minmax(0, 1fr);
  column-gap: var(--cl-gap);
  padding-bottom: clamp(3rem, 8vh, 4.5rem);
}

.cl__note-body {
  grid-column: 2;
  padding-left: var(--cl-pad);
  max-width: 58ch;
}

.cl__note-body p {
  font-size: .975rem;
  line-height: 1.62;
  color: var(--muted);
}

.cl__note-body p + p { margin-top: .8rem; }

.cl__note-body a {
  color: var(--slate-2);
  text-decoration: underline;
  text-underline-offset: .18em;
  text-decoration-thickness: 1px;
  text-decoration-color: color-mix(in srgb, var(--slate-2) 45%, transparent);
  transition: color var(--dur-fast) var(--ease), text-decoration-color var(--dur-fast) var(--ease);
}

.cl__note-body a:hover { color: var(--white); text-decoration-color: var(--white); }

/* ---------- empty state (every entry filtered out) ---------- */
.cl__empty {
  padding: 3rem 0;
  text-align: center;
  color: var(--muted);
  font-size: .95rem;
}

.cl__empty[hidden] { display: none; }

/* ---------- responsive ---------- */
@media (max-width: 860px) {
  /* Collapse the rail. Every anchor already derives from --cl-spine,
     so the spine, nodes and ticks follow without further overrides. */
  .cl { --cl-rail: 0rem; --cl-gap: 0rem; }

  .cl__entry,
  .cl__month-head,
  .cl__note { grid-template-columns: minmax(0, 1fr); }

  .cl__meta,
  .cl__entry-body,
  .cl__note-body { grid-column: 1; padding-left: var(--cl-pad); }

  .cl__meta {
    flex-direction: row;
    align-items: baseline;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: .3rem .75rem;
    text-align: left;
    margin-bottom: .6rem;
  }

  .cl__month-head h2 { grid-column: 1; text-align: left; padding-left: var(--cl-pad); }
  .cl__month-head::after { display: none; }

  .cl__controls { gap: .65rem 1rem; }
  .cl__jump { order: -1; width: 100%; }
}

@media (max-width: 520px) {
  .cl__item { grid-template-columns: minmax(0, 1fr); gap: .4rem; }
  .cl__stats { gap: 1.35rem 1.5rem; }
}

@media (prefers-reduced-motion: reduce) {
  .cl--anim .cl__entry { opacity: 1; transform: none; transition: none; }
  .cl__entry--latest::after { animation: none; opacity: 0; }
  /* The lit state is information, so it stays. Only the movement goes. */
  .cl__entry::before { transition: none; }
  .cl__entry.is-lit::before { animation: none; }
}

/* ---------- print ---------- */
@media print {
  .nav, .cl__controls, .doc__foot, .footer, .cl__progress { display: none !important; }
  body { background: #fff; color: #000; }
  .doc { padding: 0; }
  .cl { --cl-rail: 0rem; --cl-gap: 0rem; }
  .cl__timeline { padding: 0; }
  .cl__timeline::before { display: none; }
  .cl__entry { break-inside: avoid; grid-template-columns: minmax(0, 1fr); }
  .cl__entry::before, .cl__entry--latest::after { display: none; }
  .cl__meta, .cl__entry-body { grid-column: 1; padding-left: 0; }
  .cl__title, .cl__stat strong, .cl__item strong { color: #000; }
  .cl__stand, .cl__item p, .cl__date, .cl__note-body p { color: #333; }
}
