/* ============================================================================
   shared/scroll.css  ·  the scroll idiom every page on this site shares:
   full-viewport snap sections, the downward arrow cue, the right-edge section
   indicator, and the reveal sequence its replay control restarts.

   Pairs with shared/scroll.js, which builds the rail and does the paging:
     <link rel="stylesheet" href="shared/scroll.css">
     <script defer src="shared/scroll.js"></script>

   SPECIFICITY CONTRACT
   --------------------
   Shared owns BEHAVIOUR (snap locking, the rail, the arrow's motion), the page
   owns LAYOUT. Every layout default below sits inside :where(), which has zero
   specificity, so a page's own `.vp` or `.vp-arrow` rule wins no matter which
   stylesheet loads last. Only behavioural properties are stated at normal
   specificity, and they are ones a page has no reason to restate.

   No colour is invented here. Every value reads a page token (--paper, --ink,
   --line, --quiet, --muted) with the site's own value as the fallback, so a
   page that forgot a token still renders in the right palette.

   Snap paging is gated on html[data-vp], which scroll.js sets only when the
   page actually contains .vp sections, so including these two files on a page
   without them changes nothing.
   ========================================================================== */

:root{
  /* gesture knobs. scroll.js reads all three.
     After a turn the rest of that swipe is consumed. Navigation re-arms only
     after --vp-pause ms of quiet (or a real reversal), never because the
     snap animation finished, and never because the tail travelled "enough". */
  --vp-hold: 60;      /* px of wheel travel absorbed before a section turns    */
  --vp-pause: 100;    /* ms of genuine quiet that ends a gesture and re-arms   */
  --vp-noise: 10;     /* px; smaller ticks are tail-dust, not a new impulse    */
  --vp-ease: cubic-bezier(.22,.61,.36,1);
  --vp-ease-out: cubic-bezier(.16,1,.3,1);
}

/* the page-accent contract: a page may set --page-accent on :root or body to
   tint the active rail dash + label, the arrow's hover colour, and the
   replay focus ring where shared styles own one. Like every other page
   token here, it is never DECLARED in this file, only read, with --ink as its
   fallback, at each point of use. Declaring it would clobber a page's own
   value, because pages set their tokens before linking this stylesheet. */

/* ---------- 1 · the snap scroller -------------------------------------- */

html[data-vp]{
  scroll-snap-type: y mandatory;
  /* JS owns the smooth scroll. html { scroll-behavior:smooth } plus
     scrollIntoView({behavior:"smooth"}) plus mandatory snap is what makes
     Safari skip and rubber-band; Chrome mostly survives it. */
  scroll-behavior: auto;
  scroll-padding-block: 0;
  overscroll-behavior-y: none;
}
html[data-vp], html[data-vp] body{ overflow-x: clip; }

/* Desktop Safari: the JS pager is the snap. Native snap running at the same
   time consumes the trackpad gesture we did not fully cancel (Safari inertia
   wheel events are often not cancelable) and walks extra sections. iPhone
   keeps CSS snap (touch, no wheel pager). */
html[data-vp][data-vp-safari]{
  scroll-snap-type: none;
}

/* A section can outgrow the window for reasons a page's own step-downs cannot
   reach: browser zoom, a window narrow enough that the page has stacked its
   one-screen layout. Such a section has no flush resting place, and MANDATORY
   snapping insists on one, so the reader gets pulled back mid-paragraph.
   scroll.js flags THAT section while it is on screen; proximity snapping then
   leaves the tall stretch alone and keeps mandatory snap on every section
   that does fit. */
html[data-vp][data-vp-tall]{ scroll-snap-type: y proximity; }

/* behaviour */
.vp{
  scroll-snap-align: start;
  scroll-snap-stop: always;   /* one section per gesture, never skip past one */
}
/* layout default, overridable by any page rule */
:where(.vp){
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(40px, 7vh, 88px);
}

/* ---------- 2 · the arrow cue ------------------------------------------ */
/* The idle motion lives on the SVG, not on the button, so a page that
   positions its arrow with a transform (translateX(-50%), say) keeps it. */

.vp-arrow svg{ animation: vp-arrow-idle 2.8s var(--vp-ease) infinite; }
.vp-arrow:hover svg{ animation-play-state: paused; }
.vp-arrow[hidden]{ display: none; }

:where(.vp-arrow){
  align-self: center;
  margin-top: clamp(26px, 4.5vh, 54px);
  width: 30px; height: 22px;
  display: grid; place-items: center;
  padding: 0; border: 0; background: none;
  color: var(--quiet, #8A9499);
  cursor: pointer;
  transition: color 180ms var(--vp-ease);
}
:where(.vp-arrow svg){
  display: block; width: 20px; height: 11px;
  fill: none; stroke: currentColor;
  stroke-width: 1.4; stroke-linecap: square; stroke-linejoin: miter;
}
:where(.vp-arrow:hover){ color: var(--page-accent, var(--ink, #111517)); }

@keyframes vp-arrow-idle{
  0%,100%{ transform: translateY(0);   opacity: .62 }
  46%    { transform: translateY(4px); opacity: 1 }
  72%    { transform: translateY(0);   opacity: .82 }
}

/* ---------- 3 · the right-edge section indicator ------------------------ */
/* D7-b, per Oliver's D7 note, rebuilt in round 6 after "I feel like it's weird
   that they disappear as you scroll, and then the subcategories are all messed
   up. You really need to fix the scrollbar."

   Every section is a 12px DASH, always the same length: a section is not more
   important for taking longer to read. Length is carried by the FILL inside the
   dash, so a one-screen section reads full the moment you are in it and a
   pinned zone fills across its walk.

   NAMES, and this is the whole of the round-6 rule: at 1380 and wider EVERY
   section is spelled out, all the time. No name appears, disappears, moves or
   changes size as you scroll; the section you are in is told apart by ink, not
   by being the only one you can read. A name too wide for the clear strip
   beside the measure WRAPS, at the same 11px — it is never set smaller, never
   clipped and never allowed to cross into the text.

   Below 1380 the strip is narrower than one word, so there are no names beside
   the marks at all: bare dashes, and the section you are in is named once in
   the top-right caption, on the chrome band opposite the back pill.

   SUB-PAGES. data-toc-sub marks a SUB-PAGE of the app, never a walk step and
   never a point. Its dots exist only while you are inside their section, sit
   directly under their own section's dash, and only the one being read is
   named beside its dot.

   The alternative treatment, three names at rest on one line at every width,
   is in track1-build/shared-rail-alt.html.                                    */

.vp-rail{
  position: fixed; right: 16px; top: 50%;
  transform: translateY(-50%);
  z-index: 60;
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 8px;
  margin: 0; padding: 0; border: 0; background: none;
}
.vp-grp{ display: flex; flex-direction: column; align-items: flex-end; gap: 0; }

.vp-mark{
  display: flex; align-items: center; justify-content: flex-end;
  gap: 0; min-height: 12px;
  margin: 0; padding: 2px 0; border: 0; background: none;
  cursor: pointer; line-height: 1;
  --acc: var(--page-accent, var(--ink, #111517));
}
.vp-mark:focus-visible{ outline: 2px solid var(--link, #4D687A); outline-offset: 3px; border-radius: 2px; }

/* Hidden is the DEFAULT, and the media query below is the only thing that ever
   shows a name. display, not max-width: a wrapping label collapsed to zero
   width would still be several lines tall and would push the marks apart. */
.vp-mark-label{
  display: none;
  box-sizing: border-box;
  text-align: right;
  /* the cap is the clear strip between the text measure and the dashes,
     measured by scroll.js. Nothing may reach past it into the page. */
  max-width: var(--vp-strip, 96px);
  white-space: normal;
  overflow-wrap: normal;          /* a word is never broken to fit */
  font-family: var(--display, var(--font-display, ui-sans-serif, system-ui, sans-serif));
  font-weight: 500;
  font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  line-height: 1.24;
  padding-right: 9px;
  color: var(--muted, #5E686E);
  transition: color 160ms var(--vp-ease);
}
/* AT 1380 AND WIDER: every section name, always. Plus the one sub-page being
   read, beside its own dot. */
@media (min-width: 1380px){
  .vp-mark:not(.vp-sub) .vp-mark-label,
  .vp-mark.vp-sub[aria-current="true"] .vp-mark-label{ display: block; }
}
/* the section you are in, and the sub-page you are on, in the page's ink */
.vp-mark[aria-current="true"] .vp-mark-label{
  color: var(--vp-label-ink, var(--acc));
}

/* THE CAPTION, below 1380. The name leaves the rail's column altogether and
   sits in the top-right corner, on the same band as the back pill in the
   top-left, where it has the whole page width: one line, never wrapped, and
   nowhere near the measure. It is parented to the body, because the rail's own
   translateY(-50%) would otherwise be its containing block. Mono, because up
   there it is a caption on the page rather than a label on a mark. It goes
   with the rail under 901. */
.vp-rail-name{
  display: none;
  position: fixed;
  top: var(--chrome-back-top, 18px);
  right: 22px;
  z-index: 60;
  max-width: none;
  white-space: nowrap;
  font-family: var(--mono, var(--font-mono, ui-monospace, monospace));
  font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  line-height: 1;
  color: var(--vp-label-ink, var(--page-accent-ink, var(--page-accent, var(--ink, #111517))));
  padding: 9px 15px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--line, #C3CCD1) 65%, transparent);
  background: color-mix(in srgb, var(--paper, #F9FAFA) 88%, transparent);
  -webkit-backdrop-filter: blur(11px) saturate(1.3);
  backdrop-filter: blur(11px) saturate(1.3);
  box-shadow: 0 1px 3px color-mix(in srgb, var(--ink, #111517) 9%, transparent);
  pointer-events: none;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){
  .vp-rail-name{ background: color-mix(in srgb, var(--paper, #F9FAFA) 97%, transparent); }
}
@media (min-width: 901px) and (max-width: 1379px){ .vp-rail-name{ display: block; } }
@media (max-height: 860px){
  .vp-rail-name{ padding: 6px 12px; letter-spacing: .1em; }
}

/* the dash, and the progress inside it */
.vp-dash{
  flex: none; position: relative; display: block;
  width: 12px; height: 1.5px;
  background: var(--line, #C3CCD1);
}
.vp-dash i{
  position: absolute; inset: 0 auto 0 0; display: block;
  width: 0; background: var(--acc);
  transition: width 200ms linear;
}
.vp-mark[aria-current="true"] .vp-dash{
  height: 2px;
  background: color-mix(in srgb, var(--acc) 22%, var(--line, #C3CCD1));
}
.vp-mark:hover .vp-dash{ background: color-mix(in srgb, var(--acc) 45%, var(--line, #C3CCD1)); }

/* sub-pages: dots, and only while their section is the one you are in */
.vp-subs{
  display: grid; grid-template-rows: 0fr; overflow: hidden; opacity: 0;
  transition: grid-template-rows 240ms var(--vp-ease-out), opacity 180ms var(--vp-ease);
}
.vp-grp.is-open .vp-subs{ grid-template-rows: 1fr; opacity: 1; }
.vp-subs > div{
  min-height: 0;
  display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
  padding: 0;
}
/* the padding exists only while open: on a collapsed grid row min-height:0
   zeroes the content, but padding would still hold the row several px tall. */
.vp-grp.is-open .vp-subs > div{ padding: 7px 0 1px; }

.vp-mark.is-sub{ min-height: 8px; padding: 0; }
.vp-mark.is-sub .vp-mark-label{ padding-right: 8px; }
/* the dot's centre sits on the dash's centre above it, so a sub reads as
   hanging off its own section and not off the rail in general */
.vp-dot{
  flex: none; display: block;
  width: 5px; height: 5px; border-radius: 50%; margin-right: 3.5px;
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--line, #C3CCD1);
  transition: background-color 180ms var(--vp-ease), box-shadow 180ms var(--vp-ease);
}
.vp-mark.is-sub[aria-current="true"] .vp-dot{
  background: var(--acc);
  box-shadow: inset 0 0 0 1px var(--acc);
}

@media (max-width: 900px){ .vp-rail, .vp-rail-name{ display: none !important } }

/* ---------- 4 · animated sequence + replay ------------------------------ */
/* A sequence root carries an id and class .vp-seq. scroll.js sets
   data-vp-armed on it only when it is actually going to animate, and
   data-vp-play="on" when the sequence should run. Parts therefore REST in
   their final state; the armed attribute is what hides them beforehand.
   That is what makes reduced motion and a failed script both land on the
   finished picture rather than on an empty box.

   The replay control itself is deliberately unstyled here. Pages already style
   their own, and a shared [data-replay] rule would silently outrank them
   depending on stylesheet order.                                           */

.vp-seq[data-vp-armed] .vp-part{ opacity: 0; }
.vp-seq[data-vp-armed][data-vp-play="on"] .vp-part{ opacity: 1; }

/* ---------- 5 · reduced motion ----------------------------------------- */
/* Snap becomes plain stacked sections. Arrow static but still clickable.
   Rail stays, without animated transitions. Sequences show their final
   state, and replay still works as an instant reset.                       */

@media (prefers-reduced-motion: reduce){
  html[data-vp]{ scroll-snap-type: none; scroll-behavior: auto; }
  .vp{ scroll-snap-align: none; scroll-snap-stop: normal; }
  :where(.vp){ min-height: 0; padding-block: clamp(34px, 5vh, 64px); }
  .vp-arrow svg{ animation: none; }
  :where(.vp-arrow){ transition: none; }
  .vp-mark-label, .vp-dash, .vp-dash i, .vp-dot, .vp-subs{ transition: none; }
  .vp-seq *{ animation: none !important; transition: none !important; }
}
