/* ==========================================================================
   Snap Sections 1.0 — full-height panels that snap, with a progress rail
   --------------------------------------------------------------------------
   Plain CSS. No framework, no build step, no external request.

   A scroll container whose children each fill it exactly and stop on their
   own edge, plus a thin rail on the right whose thumb reports where you are.
   The snapping itself is native CSS (scroll-snap-type), so it works with no
   script at all: the wheel, a trackpad, a touch drag, the keyboard and the
   browser's own scrollbar all keep working. The script only draws the rail.

   PROPORTIONS
   The component owns exactly two lengths, and both derive from
   --ss-font-size, the body size of the text a panel carries. The ratios come
   from the reference implementation (see README.md, "Provenance"):

     panel text      1                 (the reference unit, 30 px there)
     rail width      0.2 em            (6 px — the reference default)
     rail radius     0.1 em            (half the width: a pill)
     thumb height    100% / N          (N panels, so four panels give 25%)
     thumb travel    (N - 1) x 100%    (of its own height — see §5)
     enter fade      0.3s, cubic-bezier(.25, .1, .25, 1)

   So changing --ss-font-size rescales the whole component, rail included, and
   nothing else needs to be touched. Everything that is a colour, a length or
   a duration is a variable in §2; the one setting that is neither is the
   status wording, which is a data-* attribute (see snap-sections.js).

   Order of this file:
     1. Custom property types
     2. Design tokens
     3. Shell
     4. Viewport: the scroll container
     5. Panels
     6. Rail and thumb
     7. Enter motion
     8. Focus and status
     9. Before the script runs, or without it
    10. Fallbacks: reduced motion, forced colours

   Naming follows BEM: .snap-sections, .snap-sections__part.
   ========================================================================== */

/* ==========================================================================
   1. Custom property types
   --------------------------------------------------------------------------
   Typing the two numbers the script writes buys three things: a bad value
   falls back to the initial one instead of killing the whole calc(), the
   value can be transitioned, and the browser parses it once instead of at
   every substitution. Both inherit, because the rail reads what the root
   holds.
   ========================================================================== */

@property --ss-progress {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

@property --ss-count {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

/* ==========================================================================
   2. Design tokens
   --------------------------------------------------------------------------
   Note the shape of the lengths: calc(<ratio> * var(--ss-font-size)), never
   a bare em. A custom property written in em is resolved by whoever reads it,
   so `0.2em` read from a 60 px heading would be 12 px, not 6. Written as an
   absolute calc() here, it means the same thing everywhere.
   ========================================================================== */

.snap-sections {
  /* The module. Everything else is a ratio of it. */
  --ss-font-size: 1.875rem; /* 30 px */

  /* Rail */
  --ss-rail-width: calc(0.2 * var(--ss-font-size));
  --ss-rail-radius: calc(0.5 * var(--ss-rail-width));
  --ss-rail-color: rgba(0, 0, 0, 0.1);
  --ss-thumb-color: #000;

  /* How far the thumb runs, as a share of its own height. (N - 1) x 100%
     puts its bottom edge on the bottom of the rail at the end of the scroll.
     The reference ships 100%, which stops it short; see §5. */
  --ss-thumb-travel: calc((var(--ss-count) - 1) * 100%);

  /* Left at 0s the thumb tracks the scroll frame by frame, as the reference
     does. A small value (0.08s) smooths a trackpad's jitter without lagging. */
  --ss-thumb-glide: 0s;

  /* Entrance fade, measured on the reference: 0.3s on the `ease` curve. */
  --ss-enter-duration: 0.3s;
  --ss-enter-easing: cubic-bezier(0.25, 0.1, 0.25, 1);
  --ss-enter-delay: 0s;

  /* How the keyboard and the snap itself travel. */
  --ss-scroll-behavior: smooth;

  /* `always` forbids skipping a panel on a fast flick. The reference leaves
     it at `normal`, which lets a hard flick fly past several. */
  --ss-snap-stop: normal;

  /* Focus ring on the scroll container. */
  --ss-focus-color: currentcolor;
  --ss-focus-width: 3px;
}

/* ==========================================================================
   3. Shell
   --------------------------------------------------------------------------
   The component declares its own box-sizing and assumes no reset: dropped
   into a page that has none, the rail must still be 6 px wide and not 6 px
   plus a border.
   ========================================================================== */

.snap-sections,
.snap-sections *,
.snap-sections *::before,
.snap-sections *::after {
  box-sizing: border-box;
}

.snap-sections {
  position: relative;
  width: 100%;
  height: 100%;
  font-size: var(--ss-font-size);
}

/* ==========================================================================
   4. Viewport: the scroll container
   --------------------------------------------------------------------------
   overflow-y is `scroll` and not `auto` on purpose: a container that only
   grows a scrollbar once it overflows shifts its own content the moment it
   does, and here that shift would move every panel sideways by the width of
   a scrollbar.

   scroll-behavior lives in CSS rather than in the script's scrollTo() calls,
   so that reduced motion (§10) turns the keyboard jumps instant without the
   script having to know the preference exists.
   ========================================================================== */

.snap-sections__viewport {
  height: 100%;
  width: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scroll-behavior: var(--ss-scroll-behavior);
}

/* ==========================================================================
   5. Panels
   --------------------------------------------------------------------------
   Each panel is exactly the viewport: same width, same height, its top edge
   the snap position. The reference wraps a second, identically sized box
   inside each panel; measured, the two rectangles coincide to the pixel, so
   here the panel is the section — one box instead of two, same geometry.

   Centring is the reference's own: a flex box centred on both axes, which is
   why a panel's content sits in the middle without asking for it.
   ========================================================================== */

.snap-sections__panel {
  height: 100%;
  width: 100%;
  scroll-snap-align: start;
  scroll-snap-stop: var(--ss-snap-stop);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ==========================================================================
   6. Rail and thumb
   --------------------------------------------------------------------------
   The rail is laid over the content, flush right, exactly as the reference
   does — it does not take a column of its own, so the last 6 px of a panel
   sit under it. Give a panel a right padding if its content must clear it.

   The thumb is positioned by a single number, --ss-progress, which the
   script writes on the root at each scroll frame. translateY in percent is
   a share of the element's own height, which is what makes the arithmetic
   below work with no measurement in JavaScript.

   The reference multiplies that percentage by 100, i.e. the thumb travels
   its own height and no further: with four panels it reaches a quarter of
   the rail and stops, still 500 px short of the bottom at 100% scrolled.
   The default here is (N - 1) x 100%, the distance that actually remains
   under the thumb. Set --ss-thumb-travel: 100% to get the reference back.
   ========================================================================== */

.snap-sections__rail {
  position: absolute;
  right: 0;
  top: 0;
  width: var(--ss-rail-width);
  height: 100%;
  background: var(--ss-rail-color);
  border-radius: var(--ss-rail-radius);
  /* The rail reports; it never captures a click meant for the panel. */
  pointer-events: none;
}

.snap-sections__thumb {
  width: 100%;
  height: calc(100% / var(--ss-count));
  background: var(--ss-thumb-color);
  border-radius: inherit;
  transform: translateY(calc(var(--ss-progress) * var(--ss-thumb-travel)));
  transition: --ss-progress var(--ss-thumb-glide) linear;
}

/* ==========================================================================
   7. Enter motion
   --------------------------------------------------------------------------
   The keyframe describes its `from` only. With animation-fill-mode backwards
   the arrival state stays whatever the rules above say, so the fade cannot
   overwrite an opacity a page has set on a panel for its own reasons.

   All panels come up together — measured on the reference, the spread
   between the four is 0.0000 — so the delay is shared, not staggered.
   ========================================================================== */

@keyframes snap-sections-enter {
  from {
    opacity: 0;
  }
}

.snap-sections__panel {
  animation: snap-sections-enter var(--ss-enter-duration) var(--ss-enter-easing)
    var(--ss-enter-delay) backwards;
}

/* ==========================================================================
   8. Focus and status
   --------------------------------------------------------------------------
   A scrollable box is not reachable by keyboard in Chrome or Safari unless
   it is focusable, so the viewport carries tabindex="0" in the markup. The
   ring is inset, otherwise it would be drawn outside a box that fills its
   parent and get clipped.

   The status line is the only thing a screen reader is told about the rail,
   which is decorative and hidden. It is one line of text, moved out of sight
   without being removed from the accessibility tree.
   ========================================================================== */

.snap-sections__viewport:focus-visible {
  outline: var(--ss-focus-width) solid var(--ss-focus-color);
  outline-offset: calc(-1 * var(--ss-focus-width));
}

.snap-sections__status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================
   9. Before the script runs, or without it
   --------------------------------------------------------------------------
   Without the script nothing writes --ss-progress, so a drawn thumb would
   sit at the top and claim you are on the first panel whatever you do. A
   frozen indicator is worse than none: until the script says it is ready,
   the rail stays hidden and the browser's own scrollbar is left in place,
   which reports the truth. The swap is one attribute, so the two states
   cannot disagree.
   ========================================================================== */

.snap-sections__rail {
  display: none;
}

.snap-sections[data-ss-ready] .snap-sections__rail {
  display: block;
}

.snap-sections[data-ss-ready] .snap-sections__viewport {
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.snap-sections[data-ss-ready] .snap-sections__viewport::-webkit-scrollbar {
  display: none;
}

/* ==========================================================================
   10. Fallbacks
   ========================================================================== */

/* A delay is motion spread over time, so it goes to zero along with the
   durations. The snapping itself stays: it is a position, not an effect. */
@media (prefers-reduced-motion: reduce) {
  .snap-sections {
    --ss-enter-duration: 0s;
    --ss-enter-delay: 0s;
    --ss-thumb-glide: 0s;
    --ss-scroll-behavior: auto;
  }
}

/* In forced colours the rail's 10% black disappears into the background and
   the thumb's fill is dropped, so both are restated in system colours. The
   border is inside the 6 px, thanks to the border-box above. */
@media (forced-colors: active) {
  .snap-sections__rail {
    background: Canvas;
    border: 1px solid CanvasText;
  }

  .snap-sections__thumb {
    background: CanvasText;
  }

  .snap-sections__viewport:focus-visible {
    outline-color: Highlight;
  }
}
