/* THE ROUND LAUNCH — the ladder collapsing into the round's own pill.
 *
 * It runs in two places and they are a page apart: at the end of the intro carousel
 * (trivia.html) and on the home screen the moment Play is tapped (index.html), which
 * then navigates. George, 2026-08-22: "we're not supposed to duplicate code, so I
 * guess you'd use the same code from the onboarding screen so it stays consistent."
 * This file and launch.js are that shared code.
 *
 * THE HOST CARRIES THE STATE, not the overlay. The two pages hang this off different
 * elements — a full-screen intro overlay on one, a purpose-built layer on the other —
 * so every rule below is scoped to .zl-host, which both of them add. The states are
 * the same three either way: launching (four rungs fall away, one swells and pulses),
 * morphing (that rung becomes the pill and takes the round's name), dissolving (the
 * ground goes, leaving the pill over whatever is underneath).
 *
 * WHY THE PILL NEVER MOVES. The rail sits at --ob-top and the countdown's pill at
 * --pill-top, and those resolve to the same centre by construction — so the rung
 * morphs into the pill and the pill is already where the countdown wants it. Asserted
 * as geometry in tests/e2e/round-launch.e2e.test.mjs, because if the two ever stop
 * agreeing the pill visibly jumps at the handover and nothing else would notice.
 */

/* WHERE THE PILL LANDS. Must equal trivia.html's --pill-top: the rung morphs into
 * the countdown's pill, and on the Home entrance that happens on a DIFFERENT PAGE —
 * so if these two ever drift, the pill jumps at the moment of navigation and there
 * is nothing in either file to notice. tests/invite_screens.test.mjs compares them. */
:root {
  --zl-top: 26.07vh;
  /* AND ITS HEIGHT, for the same reason. The rung the pill is copied from is a day
     card's .th-rail-dot — 45px — while the countdown's pill is --pill-h, 42.5px. Two
     and a half pixels is small and perfectly visible: the pill lands, the countdown
     paints, and it changes size. George, 2026-08-25: "pill height during animation
     and on countdown screen differ."
     Mirrored rather than read, because it is needed on index.html where trivia.html's
     variables do not exist — and asserted equal by tests/invite_screens.test.mjs, the
     same guard --zl-top has. */
  --zl-pill-h: 42.5px;
}

.zl-rail { display: flex; gap: 9px; }

/* SIZE AND ANIMATION ARE SEPARATE, because the animation runs on TWO different sets
   of dots. The carousel builds its own rail at 42.5px; the home screen animates the
   day card's REAL rungs, which are 45px and already on screen — George, 2026-08-23:
   "shouldn't the animation on the home screen occur where the existing dots are?
   Would be cooler if it used the existing visual elements." Giving those dots .zl-dot
   would resize them mid-tap, so they get only the two state classes.

   42.5 = 34 x 1.25. THE NEGATIVE MARGIN BELOW IS DERIVED FROM THIS, not chosen:
   collapsing a rung to nothing means pulling back its whole footprint, so it is
   -(dot + gap) / 2 on each side. Left at -21.5px it would leave four ghost gaps
   of 4.25px and the pill would land off-centre. */
.zl-dot {
  width: 42.5px; height: 42.5px; border-radius: 50%; flex: none; display: flex;
  align-items: center; justify-content: center; color: #fff; font-weight: 700;
  font-family: var(--font-ui); font-size: 1.02rem;
}
.zl-dot, .zl-first, .zl-rest {
  transition: opacity .42s ease, transform .5s cubic-bezier(.32,.72,0,1),
              margin .5s cubic-bezier(.32,.72,0,1);
}

/* 1 · the four they are not playing fall away, and the one they are swells. */
.zl-host.launching .zl-rest {
  opacity: 0; transform: scale(.4);
  margin-left: var(--zl-pull, -25.75px); margin-right: var(--zl-pull, -25.75px);
}
/* The day card's rail: 45px dots, 12px gap, so -(45 + 12) / 2. Set on the host by
   launch.js rather than guessed at here — a wrong pull leaves ghost gaps and the
   pill lands off-centre, which is the whole reason this number is derived. */
.zl-host.zl-onrail { --zl-pull: -28.5px; }
/* THE PULSE IS THE ROUND'S OWN COLOUR. It was a hardcoded green, with a teal variant
   for paper — so tapping Tricky produced a yellow rung inside a green halo, and the
   halo is the larger shape. George, 2026-08-23: "the pill outline also goes a bit
   green, even though I was on the yellow Tricky round." --zl-glow is set from the
   tier by launch.js, from the same TV2.TIERS the rung itself is painted from. */
.zl-host.launching .zl-first {
  transform: scale(1.5); animation: zlPulse 1s ease-out .28s 2;
}
@keyframes zlPulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in oklab, var(--zl-glow, #5C9F6E) 60%, transparent); }
  70%  { box-shadow: 0 0 0 32px color-mix(in oklab, var(--zl-glow, #5C9F6E) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--zl-glow, #5C9F6E) 0%, transparent); }
}

/* 2 · …and then the rung stops being a dot and becomes the round's own label.
   IT TAKES THE ROUND'S FILL, whatever the rung looked like a moment ago. On the day
   card an UNPLAYED round is a dashed outline, not a disc — so morphing one produced a
   pill-shaped outline with a label in it and no colour at all: George, "I just see an
   oval outline." The pill always means the same thing, so it always looks the same. */
/* THE LAST FALLBACK IS A COLOUR, NOT A TOKEN. `var(--accent)` with nothing behind it is
   invalid at computed-value time wherever --accent is undefined — and this file is
   loaded by trivia.html, which declares --t-accent and has NEVER declared --accent. An
   invalid var() does not fall back to the previous declaration; the whole property
   unsets, so `background-color` became `transparent` and the pill vanished for the
   length of the morph on both trivia paths (the carousel and between rounds). buildRail
   now names --tc so this chain is not reached at all, and the literal is here so that
   never again decides whether the player can see the pill. */
.zl-host.morphing .zl-first {
  background: var(--tc, var(--zl-glow, var(--accent, #1F8A75))) !important;
  border-color: transparent !important; border-style: solid !important;
}
.zl-host.morphing .zl-first {
  width: 128px; border-radius: 999px; font-size: 0; transform: scale(1); animation: none;
  transition: width .5s cubic-bezier(.32,.72,0,1), border-radius .5s ease,
              transform .5s cubic-bezier(.32,.72,0,1);
}
.zl-host.morphing .zl-first::after {
  content: attr(data-tier); font-size: 0.82rem; letter-spacing: .12em; font-weight: 700;
  color: #fff; text-transform: uppercase;
  /* The built rail's label, the marked rung's .zl-label, the flying copy and
     .countdown-tier are FOUR renderings of one pill, and this was the only one
     still inheriting `normal` — a pixel of vertical drift at the handover, which
     is invisible to anything that reads the DOM. */
  line-height: 1;
  /* STATIC, and it has to be said. The day card's rung already has an ::after of its
     own — absolutely positioned, for the marker it draws under the dot — and this
     rule only replaces the CONTENT, inheriting that positioning. So the round's name
     rendered above the pill instead of in it, on the halo, reading as a caption for a
     button rather than the button's own label. Static puts it back in the flex row,
     where the rung's centring takes over. */
  position: static; inset: auto; transform: none;
  opacity: 0; animation: zlFade .34s ease .22s forwards;
}
/* THE RUNG'S OWN CONTENTS GO. A built rung holds a number, which font-size:0 above is
   enough to hide — but the day card's real rung holds an ARROW, a lock or a tick in a
   child element, and those ignore it. Left showing, the label stacked above the pill
   and the arrow stayed inside it: two things in a control that should say one word. */
.zl-host.morphing .zl-first > * { display: none !important; }
/* …except the label the launch put there itself. On a real rung this is what carries
   the round's name; ::after above serves the built rail, where nothing competes. */
.zl-label { display: none; }
.zl-host.morphing .zl-first > .zl-label {
  display: block !important; color: #fff; font-family: var(--font-ui);
  font-size: 0.82rem; font-weight: 700; letter-spacing: .12em; line-height: 1;
  opacity: 0; animation: zlFade .34s ease .22s forwards;
}
/* One label, never two: a marked rung has its own, so the pseudo-element stands down. */
.zl-host.zl-onrail.morphing .zl-first::after { content: none; }

/* THE ROUND NUMBER — on the day card's rungs too, not only on the built ladder.
   The carousel's rail (buildRail) writes "1…5" INTO each rung, so the launch there is a
   numbered ladder collapsing. Home and the archive animate the day card's real rungs,
   which hold a tick, a padlock or an arrow, so the same five rounds fell away with
   nothing saying which one was starting. George, 2026-08-26: "ensure round numbers are
   included in that pill animation always." markRail adds the child; this is what shows
   it, and only for the length of the launch — at rest the rung must look like itself.

   NO !important, DELIBERATELY. `.zl-host.morphing .zl-first > * { display: none
   !important }` two rules up is what retires the number at the morph, exactly as
   font-size:0 retires the built rung's own digit, leaving the round's NAME alone in the
   pill. An !important here would leave a number sitting inside "OPENER". */
.zl-num { display: none; }
/* THE INK DEFAULTS TO THE RUNG'S OWN COLOUR, and white is the exception.
 *
 * It was the other way round — white by default, with --tc named on `.locked` and
 * `.paywall` — which is a list of the rungs that are NOT filled, and the list was
 * wrong. A rung you are part-way through is `.current.part`, whose background is
 * `var(--surface)` with the tier as an inset ring: paper, not a disc. So a RESUMED
 * round drew a white number on cream and the number simply was not there. George,
 * 2026-08-26: "when resuming an existing round, the number is still not showing."
 *
 * Enumerating the unfilled rungs fails to invisible; enumerating the FILLED ones
 * fails to legible. So the default is the one that reads on paper, and only the
 * two solid discs need naming — anything anyone adds later is right by default.
 *
 * INK, NOT THE TIER COLOUR. The first attempt at this used `--tc` on paper, which
 * is visible but barely: measured 1.84:1 for a resumed Tricky round, gold on cream.
 * The disc already says which tier it is — the ring around it IS `--tc` — so the
 * number's only job is to be read, and the number is the thing that was missing.
 * One rule either way: maximum contrast against whatever disc it sits on. */
.zl-host.launching .zl-first > .zl-num,
.zl-host.launching .zl-rest > .zl-num {
  display: flex; align-items: center; justify-content: center;
  color: var(--text, #1F1A14);
  font-family: var(--font-ui); font-weight: 700; font-size: 1.02rem;
  line-height: 1;
}
/* The two that ARE a solid tier-coloured disc: a cleared round and the one you are
   about to start fresh. `.part` is excluded — see above, it is paper. */
.zl-host.launching .th-rail-dot.done > .zl-num,
.zl-host.launching .th-rail-dot.current:not(.part) > .zl-num,
.zl-host.launching .zl-dot > .zl-num { color: #fff; }
/* …and the tick / padlock / arrow that was in the rung steps aside for it, or the two
   stack in one 45px disc. */
.zl-host.launching .zl-first > *:not(.zl-num),
.zl-host.launching .zl-rest > *:not(.zl-num) { display: none; }

/* AND THE CARD BENEATH IT STEPS BACK. The rail lives inside the day card, so the card
   cannot simply fade — that would take the rungs with it. Its surface goes instead:
   no fill, no border, no shadow, leaving the rungs alone on the page. */
.zl-fade-around .th-card,
.zl-fade-around .th-acc-card {
  transition: background .42s ease, box-shadow .42s ease, border-color .42s ease;
  background: transparent !important; box-shadow: none !important;
  border-color: transparent !important;
}
@keyframes zlFade { to { opacity: 1; } }

/* 3 · THE PILL DOES NOT MOVE. Only the ground changes: it fades and whatever is
   behind resolves through it. George: "for dramatic effect it should stay in the
   same location, the teal should fade out and countdown fade in." */
.zl-host.dissolving { background: transparent !important; transition: background .5s ease; }

/* THE PILL TRAVELS IN A FIXED LAYER, not inside the card. George:
   "pill appears, home screen fades out, pill moves up to position, countdown starts."
   liftPill copies the formed pill to documentElement at the pixels it already
   occupies and animates it from there — nothing can clip a fixed element, and
   nothing about the page's scrolling has to be disturbed to let it out.
   Its own styles are written inline at that moment, because outside the host none of
   the .zl-host rules apply any more. */

/* THE HOME-SCREEN HOST. On index.html there is no overlay to borrow, so the launch
   brings its own: a full-screen paper layer with the rail at exactly the height the
   countdown's pill will occupy on the next page. Home fades out UNDERNEATH it rather
   than being covered, which is what makes this read as the screen resolving into the
   round instead of a new screen arriving over it. */
.zl-layer {
  position: fixed; inset: 0; z-index: 900; display: flex;
  justify-content: center; align-items: flex-start;
  /* TRANSPARENT, so Home is seen GOING. Painted with the page's own colour it read
     as Home vanishing on the tap and a ladder arriving on a blank screen — the fade
     was happening underneath something opaque, which is the same as not happening.
     The ground behind is the page's own background either way. */
  background: transparent;
  padding-top: calc(var(--zl-top, 220px) - 21.25px);
  pointer-events: none;
}
/* The rest of Home going quiet. A transform is deliberately NOT used: this element is
   the page body and moving it would take the fixed chrome with it. */
.zl-fade { transition: opacity .42s ease; opacity: 0 !important; }
/* EVERYTHING BUT THE RUNGS. When the animation runs on the day card's real rail, the
   rail must NOT fade with the rest — it is the thing being watched. So the page dims
   around it and the rail is lifted out, which is the same idea as a note's hole. */
.zl-fade-around .th-card > *:not(.th-rail),
.zl-fade-around .th-acc-card > *:not(.th-rail),
.zl-fade-around .th-secrow,
.zl-fade-around .th-archwrap,
.zl-fade-around .th-archbtn,
.zl-fade-around .th-tabbar,
.zl-fade-around .th-head { transition: opacity .42s ease; opacity: 0 !important; }
.zl-fade-around .th-rail { position: relative; z-index: 2; }
/* AND EVERYTHING THE RAIL IS INSIDE. The rules above name what recedes, and one of
   them — .th-archwrap — contains every past day, so an expanded past-day card was
   faded out along with the rung the launch was animating. launch.js marks the rail's
   whole ancestor chain; this is what exempts it.
   AFTER the fade rules and at equal specificity, so source order decides — which is
   why this block must stay last. */
.zl-fade-around .zl-keep { opacity: 1 !important; }
/* NO OVERFLOW OVERRIDE HERE, DELIBERATELY. Clipping was a real problem — the
   accordion opens with grid-template-rows and an inner overflow:hidden — but forcing
   the chain to overflow:visible also hits #th-main, which is the page's SCROLL
   CONTAINER, and destroying a scrollport collapses a page that had been scrolled.
   That is what made the blanking depend on whether the accordion had been scrolled.
   The pill no longer travels inside the page at all (see liftPill in launch.js), so
   there is nothing left to clip and nothing to override. */

/* THE DAYS ABOVE GET OUT OF THE WAY. The pill travels from the card up to the
   countdown's line, crossing every row between — and those rows are opaque paper.
   Fading them is what George asked for and is also the honest version of the effect:
   the launch is supposed to leave the rungs alone on the screen. */
.zl-fade-around .th-acc-hdr { transition: opacity .42s ease; opacity: 0 !important; }
/* …and it passes OVER anything still painted. Earlier siblings lose to it on DOM
   order alone, but a row that makes its own stacking context would not. */
.zl-fade-around .th-rail { z-index: 60; }

/* THE ARCHIVE PLAYS ON PLAIN PAPER TOO. The day sheet is a white card on a dimmed
   calendar, and the rungs were collapsing inside all of it. George, 2026-08-24:
   "from the archive the animation works, but we should fade out the background so
   it's on a plain background." The surfaces go, the rail stays — the same treatment
   .th-card already gets on Home, applied to the two boxes the sheet adds. */
/* THE PANEL IS THE PAPER — fading the whole overlay reveals HOME behind it, which
   is what "it returns me to the home screen and runs the animation there" was.
   George, 2026-08-24. So the calendar's CONTENTS go and its background stays, and
   the launch happens where the player is: in the sheet they just opened. */
.zl-fade-around .archive-overlay-header,
.zl-fade-around .cal-weekday-bar,
.zl-fade-around #archive-cal-scroll { transition: opacity .42s ease; opacity: 0 !important; }
.zl-fade-around #cal-day-sheet { transition: background .42s ease; background: transparent !important; }
.zl-fade-around .cal-day-sheet-panel {
  transition: background .42s ease, box-shadow .42s ease;
  background: transparent !important; box-shadow: none !important;
}

@media (prefers-reduced-motion: reduce) {
  .zl-dot, .zl-host.dissolving, .zl-fade { transition-duration: .001s !important; }
  .zl-host.launching .zl-first { animation: none !important; }
}
