/* ==========================================================================
   Compare Slider 1.0 — before / after image comparison
   --------------------------------------------------------------------------
   Plain CSS. No framework, no build step, no external request.

   The component enhances a native <input type="range">. That input is the
   whole control surface: it lies transparent over the frame and it is what
   the pointer, the touch screen and the keyboard actually talk to. Nothing
   is reimplemented — pointer capture, touch dragging, arrow keys, Page keys,
   Home / End and the ARIA slider role all come from the browser.

   The reveal itself is a clip-path on one layer. Both images are laid out
   identically, at full size, one on top of the other; only the visible part
   of the top one changes. Nothing is ever rescaled, so there is no position
   at which the two halves stop lining up.

   PROPORTIONS
   Every metric derives from --cs-font-size, the size of the corner labels
   (11 px on the reference). The ratios were read off the published component
   at five viewport widths (see README.md, "Provenance"):

     label text        1                (the reference unit)
     stage inset       4.3637 em        (2.1819 em once the stage is narrow)
     frame max width  81.8182 em
     frame radius      0.3637 em
     hairline          0.0910 em        (frame border, label border, line halo)
     split line        0.1819 em wide
     handle            3.2728 em circle, chevrons 1.2728 em
     label             0.3637 em / 0.9091 em padding, 1.8182 em radius
     label offset      1.0910 em from the frame corner
     narrow threshold 78.5455 em        (the stage, border-box)

   So changing --cs-font-size rescales the whole component — frame, handle,
   labels and all — and nothing else needs to be touched.

   Order of this file:
     1. Design tokens
     2. Stage
     3. Frame and images
     4. Split line and handle
     5. Labels
     6. The control: a native range input
     7. Vertical orientation
     8. Light variant
     9. Narrow stage
    10. Without JavaScript
    11. Fallbacks: reduced motion, forced colours, no backdrop-filter

   Naming follows BEM: .compare-slider, .compare-slider__part,
   .compare-slider--variant.
   ========================================================================== */

/* ==========================================================================
   1. Design tokens
   --------------------------------------------------------------------------
   Everything that is a colour, a length or a duration lives here, and only
   here. Lengths are written calc(<ratio> * var(--cs-font-size)) rather than
   in em on purpose: a custom property written in em is resolved by whoever
   *reads* it, so the same token read from the 0.8182 em label would come out
   a fifth too small. In absolute form it means the same thing everywhere.
   ========================================================================== */

.compare-slider {
  /* The one dial that rescales the component. */
  --cs-font-size: 0.6875rem; /* 11 px at a 16 px root */
  --cs-font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Where the reveal currently sits. The script writes this on every input
     event; the value below is what the component shows before the script
     runs, and what it keeps if the script never runs at all. */
  --cs-position: 50%;

  /* Surfaces. The dark set is the default, as on the reference. */
  --cs-stage-background: #0d0d0d;
  --cs-frame-background: rgb(255 255 255 / 0.05);
  --cs-frame-border-color: rgb(255 255 255 / 0.12);
  --cs-label-color: #fff;
  --cs-label-background: rgb(0 0 0 / 0.55);
  --cs-label-border-color: var(--cs-frame-border-color);
  --cs-handle-background: #fff;
  --cs-handle-border-color: rgb(0 0 0 / 0.1);
  --cs-handle-icon-color: #0d0d0d;
  --cs-line-color: #fff;
  --cs-line-halo-color: rgb(0 0 0 / 0.15);
  --cs-accent: #c44b2b;

  /* Lengths. */
  --cs-inset: calc(4.3637 * var(--cs-font-size)); /* 48 px */
  --cs-frame-max-inline-size: calc(81.8182 * var(--cs-font-size)); /* 900 px */
  --cs-frame-radius: calc(0.3637 * var(--cs-font-size)); /*  4 px */
  --cs-hairline: calc(0.091 * var(--cs-font-size)); /*  1 px */
  --cs-line-thickness: calc(0.1819 * var(--cs-font-size)); /*  2 px */
  --cs-handle-size: calc(3.2728 * var(--cs-font-size)); /* 36 px */
  --cs-handle-icon-size: calc(1.2728 * var(--cs-font-size)); /* 14 px */
  --cs-handle-shadow:
    0 calc(0.1819 * var(--cs-font-size)) calc(0.9091 * var(--cs-font-size))
    rgb(0 0 0 / 0.25); /* 0 2px 10px */
  --cs-label-offset: calc(1.091 * var(--cs-font-size)); /* 12 px */
  --cs-label-padding-block: calc(0.3637 * var(--cs-font-size)); /*  4 px */
  --cs-label-padding-inline: calc(0.9091 * var(--cs-font-size)); /* 10 px */
  --cs-label-radius: calc(1.8182 * var(--cs-font-size)); /* 20 px */
  --cs-label-blur: calc(0.7273 * var(--cs-font-size)); /*  8 px */
  --cs-label-tracking: 0.1em;

  /* Durations. All of them, including the ones the script would otherwise
     have had an opinion about — it has none, it only writes --cs-position. */
  --cs-glide-duration: 150ms; /* the reveal catching up with the control */
  --cs-glide-timing: ease;
  /* The line and the handle follow the reveal with the same delay. On the
     reference only the reveal is animated, so at every keystroke the marker
     arrives first and the image lags behind it. Set this to 0ms to get that
     behaviour back. */
  --cs-marker-glide-duration: var(--cs-glide-duration);
  --cs-handle-grow-duration: 200ms;
  --cs-handle-grow-timing: cubic-bezier(0.77, 0, 0.175, 1);
  --cs-handle-grow-scale: 1.08;
  --cs-focus-duration: 150ms;
  --cs-stage-fade-duration: 600ms;
  --cs-stage-fade-timing: cubic-bezier(0.16, 1, 0.3, 1);

  /* The component brings its own box model rather than assuming a reset. */
  box-sizing: border-box;
}

.compare-slider *,
.compare-slider *::before,
.compare-slider *::after {
  box-sizing: border-box;
}

/* ==========================================================================
   2. Stage
   --------------------------------------------------------------------------
   The dark mat the frame sits on. It fills whatever box it is given; when it
   is given none, --cs-block-size keeps it at the height of the reference
   rather than collapsing to nothing.

   The stage is the query container, and it deliberately carries no padding:
   the inset is subtracted from the frame instead. A container that queries
   its own content box while its padding depends on the answer oscillates —
   the reference does exactly that, see README, "The wobble between 816 and
   864 px". Query the border box and the question has one stable answer.
   ========================================================================== */

.compare-slider {
  position: relative;
  display: grid;
  place-items: center;
  inline-size: 100%;
  block-size: var(--cs-block-size, calc(68 * var(--cs-font-size))); /* 748 px */
  background: var(--cs-stage-background);
  font-family: var(--cs-font-family);
  font-size: var(--cs-font-size);
  transition: background var(--cs-stage-fade-duration) var(--cs-stage-fade-timing);

  container: compare-slider / inline-size;
}

/* ==========================================================================
   3. Frame and images
   --------------------------------------------------------------------------
   The frame is the comparison itself. Both images are the same box, at the
   same size, stacked: the "after" one below, the "before" one above inside a
   layer that clip-path trims. Because neither is ever resized, the two never
   drift apart — the seam is a cut, not a join.
   ========================================================================== */

.compare-slider__frame {
  position: relative;
  inline-size: calc(100% - 2 * var(--cs-inset));
  block-size: calc(100% - 2 * var(--cs-inset));
  max-inline-size: var(--cs-frame-max-inline-size);
  overflow: hidden;
  border: var(--cs-hairline) solid var(--cs-frame-border-color);
  border-radius: var(--cs-frame-radius);
  background: var(--cs-frame-background);
}

.compare-slider__image {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  max-inline-size: none;
  object-fit: cover;
  object-position: var(--cs-image-position, 50% 50%);
  /* The drag lives on the input above; images must not intercept it, and
     must not be draggable themselves. */
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none;
}

.compare-slider__reveal {
  position: absolute;
  inset: 0;
  /* inset() trims from the far side, so the visible band grows with the
     position. Animating a clip-path is compositable; animating a width is
     not, and this runs on every pointer move. */
  clip-path: inset(0 calc(100% - var(--cs-position)) 0 0);
  transition: clip-path var(--cs-glide-duration) var(--cs-glide-timing);
}

/* Nothing must animate while a finger or a cursor is pushing it: the reveal
   has to sit exactly under the pointer, not 150 ms behind it. */
.compare-slider[data-dragging] .compare-slider__reveal,
.compare-slider[data-dragging] .compare-slider__line,
.compare-slider[data-dragging] .compare-slider__handle {
  transition: none;
}

/* ==========================================================================
   4. Split line and handle
   --------------------------------------------------------------------------
   Both are decoration: the control is the input underneath them. They are
   out of the accessibility tree and out of the way of the pointer.
   ========================================================================== */

.compare-slider__line {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--cs-position);
  inline-size: var(--cs-line-thickness);
  translate: -50%;
  background: var(--cs-line-color);
  /* A white line over a white sky would vanish; the halo is what keeps it
     readable on any image. */
  box-shadow: 0 0 0 var(--cs-hairline) var(--cs-line-halo-color);
  pointer-events: none;
  transition: inset-inline-start var(--cs-marker-glide-duration) var(--cs-glide-timing);
}

.compare-slider__handle {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: var(--cs-position);
  display: grid;
  place-items: center;
  inline-size: var(--cs-handle-size);
  block-size: var(--cs-handle-size);
  translate: -50% -50%;
  border: var(--cs-hairline) solid var(--cs-handle-border-color);
  border-radius: 50%;
  background: var(--cs-handle-background);
  box-shadow: var(--cs-handle-shadow);
  pointer-events: none;
  transition:
    inset-inline-start var(--cs-marker-glide-duration) var(--cs-glide-timing),
    scale var(--cs-handle-grow-duration) var(--cs-handle-grow-timing),
    border-color var(--cs-focus-duration) ease,
    box-shadow var(--cs-focus-duration) ease;
}

.compare-slider[data-dragging] .compare-slider__handle {
  scale: var(--cs-handle-grow-scale);
}

.compare-slider__handle-icon {
  inline-size: var(--cs-handle-icon-size);
  block-size: var(--cs-handle-icon-size);
  fill: none;
  stroke: var(--cs-handle-icon-color);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The focus ring. The reference tints the handle border and stops there,
   which on a pale photograph leaves a 2 px terracotta ring against nothing.
   The white halo underneath guarantees the contrast whatever the image is. */
.compare-slider:has(.compare-slider__control:focus-visible) .compare-slider__handle {
  border-color: var(--cs-accent);
  border-width: calc(2 * var(--cs-hairline));
  box-shadow:
    0 0 0 var(--cs-hairline) #fff,
    var(--cs-handle-shadow);
}

/* ==========================================================================
   5. Labels
   ========================================================================== */

.compare-slider__label {
  position: absolute;
  z-index: 10;
  padding: var(--cs-label-padding-block) var(--cs-label-padding-inline);
  border: var(--cs-hairline) solid var(--cs-label-border-color);
  border-radius: var(--cs-label-radius);
  background: var(--cs-label-background);
  backdrop-filter: blur(var(--cs-label-blur));
  color: var(--cs-label-color);
  font-size: 1em;
  font-weight: 600;
  letter-spacing: var(--cs-label-tracking);
  /* Pinned rather than left to the font: "normal" resolves differently on
     every platform, and these two boxes have to be the same height. */
  line-height: calc(12 / 11);
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
}

.compare-slider__label--before {
  inset-block-start: var(--cs-label-offset);
  inset-inline-start: var(--cs-label-offset);
}

.compare-slider__label--after {
  inset-block-start: var(--cs-label-offset);
  inset-inline-end: var(--cs-label-offset);
}

/* ==========================================================================
   6. The control: a native range input
   --------------------------------------------------------------------------
   Stretched over the whole frame and painted in nothing. It keeps its hit
   area, its cursor and its keyboard, and it is still a slider to assistive
   technology — role, orientation, min, max and current value included.

   The thumb is one hairline wide so that the value maps onto the frame very
   nearly linearly: a native thumb never leaves the track, so a 36 px one
   would compress the usable travel by 36 px. It is invisible either way —
   the round handle above is what the eye follows.

   touch-action lets the page keep scrolling vertically while the component
   takes the horizontal gesture. The reference blocks both, which traps the
   scroll of anyone whose thumb happens to land on the image.
   ========================================================================== */

.compare-slider__control {
  position: absolute;
  z-index: 20;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  padding: 0;
  appearance: none;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: var(--cs-cursor, ew-resize);
  touch-action: pan-y;
  /* The visible focus is drawn on the handle: an outline here would trace
     the whole frame. */
  outline: none;
}

.compare-slider[data-dragging] .compare-slider__control {
  cursor: grabbing;
}

.compare-slider__control::-webkit-slider-runnable-track {
  block-size: 100%;
  border: 0;
  background: transparent;
}

.compare-slider__control::-webkit-slider-thumb {
  appearance: none;
  inline-size: var(--cs-hairline);
  block-size: 100%;
  border: 0;
  background: transparent;
  cursor: inherit;
}

.compare-slider__control::-moz-range-track {
  block-size: 100%;
  border: 0;
  background: transparent;
}

.compare-slider__control::-moz-range-thumb {
  inline-size: var(--cs-hairline);
  block-size: 100%;
  border: 0;
  border-radius: 0;
  background: transparent;
  cursor: inherit;
}

/* ==========================================================================
   7. Vertical orientation
   --------------------------------------------------------------------------
   Set writing-mode on the input and the browser turns the slider itself: the
   gesture, the arrow keys and aria-orientation all follow, with no script
   involved. The rest is the same drawing, rotated a quarter turn.
   ========================================================================== */

.compare-slider--vertical {
  --cs-cursor: ns-resize;
}

.compare-slider--vertical .compare-slider__reveal {
  clip-path: inset(0 0 calc(100% - var(--cs-position)) 0);
}

.compare-slider--vertical .compare-slider__line {
  inset-block: auto;
  inset-inline: 0;
  inset-block-start: var(--cs-position);
  inline-size: auto;
  block-size: var(--cs-line-thickness);
  translate: 0 -50%;
  transition: inset-block-start var(--cs-marker-glide-duration) var(--cs-glide-timing);
}

.compare-slider--vertical .compare-slider__handle {
  inset-block-start: var(--cs-position);
  inset-inline-start: 50%;
  transition:
    inset-block-start var(--cs-marker-glide-duration) var(--cs-glide-timing),
    scale var(--cs-handle-grow-duration) var(--cs-handle-grow-timing),
    border-color var(--cs-focus-duration) ease,
    box-shadow var(--cs-focus-duration) ease;
}

.compare-slider--vertical .compare-slider__handle-icon {
  rotate: 90deg;
}

/* The "after" label moves out from under the "before" one, which the split
   now sits between. */
.compare-slider--vertical .compare-slider__label--after {
  inset-block-start: auto;
  inset-inline-end: auto;
  inset-block-end: var(--cs-label-offset);
  inset-inline-start: var(--cs-label-offset);
}

.compare-slider--vertical .compare-slider__control {
  writing-mode: vertical-lr;
  touch-action: pan-x;
}

/* ==========================================================================
   8. Light variant
   ========================================================================== */

.compare-slider--paper {
  --cs-stage-background: #f4f3f1;
  --cs-frame-background: rgb(13 13 13 / 0.04);
  --cs-frame-border-color: rgb(13 13 13 / 0.08);
  --cs-label-color: #0d0d0d;
  --cs-label-background: rgb(255 255 255 / 0.72);
}

/* ==========================================================================
   9. Narrow stage
   --------------------------------------------------------------------------
   A container query, not a media query: what matters is the room the
   component was given, not the size of the window. Dropped into a sidebar on
   a wide screen it tightens up all the same.

   78.5455 em is 864 px at the default scale — the width at which the
   reference switches, measured. The threshold is in em so that it follows
   --cs-font-size like everything else; inside a container query em resolves
   against the container's own font size, which is that variable.

   The rule targets the frame rather than the stage, and that is not a matter
   of taste: a container query cannot style its own container, only what is
   inside it. Written against .compare-slider it would simply never apply —
   no error, no warning, and nothing to see but a component that stays wide
   in a narrow column.
   ========================================================================== */

@container compare-slider (max-width: 78.5455em) {
  .compare-slider__frame {
    --cs-inset: calc(2.1819 * var(--cs-font-size)); /* 24 px */
  }
}

/* ==========================================================================
   10. Without JavaScript
   --------------------------------------------------------------------------
   The script adds data-ready once it has wired the input up. Until then — and
   for good if scripting is off — the control is taken out rather than left
   sitting there: a slider that moves without moving anything is worse than no
   slider at all. What remains is the comparison frozen at --cs-position, both
   images, both labels, the line and the handle. Nothing is broken and nothing
   lies; it simply does not move. There is no CSS-only way to bind a range to
   a custom property, so this is the honest floor.
   ========================================================================== */

.compare-slider:not([data-ready]) .compare-slider__control {
  display: none;
}

/* ==========================================================================
   11. Fallbacks
   ========================================================================== */

/* A delay is motion spread over time, so those go to zero too. The clip
   itself is a picture, not a movement: it stays exactly where the control
   says it is, it just gets there at once. */
@media (prefers-reduced-motion: reduce) {
  .compare-slider {
    --cs-glide-duration: 0ms;
    --cs-marker-glide-duration: 0ms;
    --cs-handle-grow-duration: 0ms;
    --cs-handle-grow-scale: 1;
    --cs-stage-fade-duration: 0ms;
    --cs-focus-duration: 0ms;
  }
}

/* In forced colours the translucent surfaces resolve to nothing readable and
   backdrop-filter is ignored. Everything falls back to system colours, and
   the line and handle are given borders so that they survive as shapes. */
@media (forced-colors: active) {
  .compare-slider {
    --cs-line-color: CanvasText;
    --cs-handle-background: Canvas;
    --cs-handle-border-color: CanvasText;
    --cs-handle-icon-color: CanvasText;
    --cs-accent: Highlight;
  }

  .compare-slider__label {
    border-color: CanvasText;
    background: Canvas;
    color: CanvasText;
    forced-color-adjust: none;
    backdrop-filter: none;
  }

  .compare-slider__frame {
    border-color: CanvasText;
  }

  .compare-slider__line {
    box-shadow: none;
    outline: var(--cs-hairline) solid Canvas;
  }
}

/* Without backdrop-filter the labels would sit on a 55 % veil of nothing.
   Opaque backgrounds keep them legible. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .compare-slider {
    --cs-label-background: rgb(0 0 0 / 0.78);
  }

  .compare-slider--paper {
    --cs-label-background: rgb(255 255 255 / 0.92);
  }
}
