/* ==========================================================================
   Focus Gallery 1.0 — a vertical gallery where the centred image takes the light
   --------------------------------------------------------------------------
   Plain CSS. No dependency, no build step, no external request.

   The principle fits in one sentence: the rail scrolls inside a window the
   height of the view, and the image whose centre is closest to the centre of
   that window goes to 1.5 while the others stay at 0.7.

   The scrolling itself is written nowhere. It comes from padding: the rail
   carries half a view minus half an image at the top and at the bottom, so the
   first image is already centred at rest and the last one is centred at the end
   of the run. Not a line of JavaScript for the layout — the script decides one
   thing only, which of the images is at the centre.

   PROPORTIONS
   Everything is a multiple of --fg-font-size, measured on the reference (see
   _mesures/README.md). The measurements all fall exactly on a 16 px grid, which
   gives away the drawing unit of the mock-up:

     image            18.75 × 12.5   (300 × 200 px, 3:2 format)
     radius            0.75          (12 px)
     gap between two   3.125         (50 px)
     scroll pitch      15.625        (250 px = image + gap)
     margin of texts   2.5           (40 px)
     title             2.5           (40 px, line height 1, tracking −0.04 em)
     note              1.5           (24 px, line height 1.3, tracking −0.01 em)
     active shadow     0 1.25 2.5    (0 20px 40px)
     resting shadow    0 0.625 1.25  (0 10px 20px)

   Lengths are written calc(N * var(--fg-font-size)) and not N em, even though
   the result is the same in most cases. The reason: em resolves against the
   font size of the element that *uses* the variable. On the title, which is at
   2.5 times the body copy, --fg-marge: 2.5em would come to 100 px instead of
   40. By multiplying explicitly by --fg-font-size, a variable is worth the same
   thing everywhere, wherever it is read.

   Order of this file:
     1. Tokens
     2. Shell
     3. View and rail
     4. Frames: the two states
     5. Title and note
     6. Focus
     7. Without JavaScript
     8. Narrow screens
     9. Reduced motion, forced colours

   Naming follows BEM: .focus-gallery, .focus-gallery__part.
   ========================================================================== */

/* ==========================================================================
   1. Tokens
   ========================================================================== */

.focus-gallery {
  /* --------------------------------------------------------------------
     The reference body copy, and the only thing to touch to rescale
     everything.

     The ceiling is 1rem. Below that, the width decides: the active image takes
     18.75 × 1.5 = 28.125 times the body copy, and it needs that room. The
     division takes both variables back rather than a hard-coded 28.125, so
     that a change of image format or of scale stays consistent.
     -------------------------------------------------------------------- */
  --fg-corps-max: 1rem;
  --fg-air-ecran: 2rem;
  --fg-font-size: min(
    var(--fg-corps-max),
    calc((100vw - var(--fg-air-ecran)) / (var(--fg-image-l-nu) * var(--fg-echelle-active)))
  );

  /* Format of the image, in multiples of the body copy. The two bare numbers
     also serve the calculation above, hence their separate existence. */
  --fg-image-l-nu: 18.75;
  --fg-image-h-nu: 12.5;
  --fg-image-l: calc(var(--fg-image-l-nu) * var(--fg-font-size));
  --fg-image-h: calc(var(--fg-image-h-nu) * var(--fg-font-size));

  --fg-rayon: calc(0.75 * var(--fg-font-size));
  --fg-ecart: calc(3.125 * var(--fg-font-size));
  --fg-ecart-legende: calc(1.25 * var(--fg-font-size));
  --fg-marge: calc(2.5 * var(--fg-font-size));

  /* Height of the scrolling window. It is what drives the padding of the rail,
     so it is set here and never through `height`. */
  --fg-hauteur: 100svh;

  --fg-echelle-active: 1.5;
  --fg-echelle-repos: 0.7;
  --fg-opacite-active: 1;
  --fg-opacite-repos: 0.4;

  --fg-ombre-active: 0 calc(1.25 * var(--fg-font-size)) calc(2.5 * var(--fg-font-size))
    rgb(0 0 0 / 0.15);
  --fg-ombre-repos: 0 calc(0.625 * var(--fg-font-size)) calc(1.25 * var(--fg-font-size))
    rgb(0 0 0 / 0.08);

  /* 400 ms ease-out: fitted over 59 frames of the switch of the reference,
     RMS 0.001 — see _mesures/README.md. The duration of the shadow is separate
     because the reference does not animate it: --fg-duree-ombre: 0s gives its
     abrupt switch back. */
  --fg-duree: 400ms;
  --fg-duree-ombre: var(--fg-duree);
  --fg-courbe: ease-out;

  --fg-fond: #fff;
  --fg-encre: #000;
  --fg-focus: #1149ff;
  --fg-focus-epaisseur: 3px;
  --fg-focus-ecart: 4px;

  --fg-titre-corps: calc(2.5 * var(--fg-font-size));
  --fg-titre-graisse: 700;
  --fg-note-corps: calc(1.5 * var(--fg-font-size));
  --fg-note-graisse: 500;

  /* 25 % is the value of the reference, and it is the one that applies for as
     long as it holds. The second term is the room genuinely available between
     the edge and the active image: half the width, minus half an enlarged
     image, minus the margin, minus one body copy of air. Without it, a text
     allowed to take a quarter of the width bites into the image as soon as the
     image goes past half the frame — which never happens on the reference at
     1440 px, but does happen in a preview or in a narrow section. Above that
     threshold the guard changes nothing: at 1440 px it comes to 439 px against
     a ceiling of 360. */
  --fg-texte-largeur: min(
    25%,
    calc(
      50% - (var(--fg-image-l-nu) * var(--fg-echelle-active) * var(--fg-font-size)) / 2 -
        var(--fg-marge) - var(--fg-font-size)
    )
  );
  --fg-police: system-ui, -apple-system, "Segoe UI", roboto, sans-serif;
}

/* ==========================================================================
   2. Shell
   ========================================================================== */

/* The component lays down its own box-sizing: it assumes no reset from the page
   that holds it, and imposes none on the rest. */
.focus-gallery,
.focus-gallery *,
.focus-gallery *::before,
.focus-gallery *::after {
  box-sizing: border-box;
}

.focus-gallery {
  position: relative;
  height: var(--fg-hauteur);
  overflow: hidden;
  background: var(--fg-fond);
  color: var(--fg-encre);
  font-family: var(--fg-police);
  font-size: var(--fg-font-size);

  /* The drop shadows of the images must not get mixed up with the stacking of
     the host page. */
  isolation: isolate;
}

/* ==========================================================================
   3. View and rail
   ========================================================================== */

.focus-gallery__vue {
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;

  /* The reference hides its scrollbar. The content stays reachable with the
     wheel, the finger and the keyboard; only the visual cue goes, which is the
     price of the composition. */
  scrollbar-width: none;
  -ms-overflow-style: none;

  /* No scroll-snap here: the reference leaves the scrolling free, and the
     scale switches at the half pitch. Snapping only appears without
     JavaScript, § 7, where it replaces what the script can no longer do. */
  scroll-behavior: smooth;
}

.focus-gallery__vue::-webkit-scrollbar {
  width: 0;
  height: 0;
}

.focus-gallery__rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--fg-ecart);

  /* The scroll run, in one declaration.

     Half a view minus half an image at the top and at the bottom: the first
     image is centred at rest, the last one is centred at the end of the run,
     and the run is exactly (n − 1) pitches. Nothing to compute, nothing to
     update on resize, and the number of images does not have to be known.

     max(0px, …) for the case of a view shorter than an image, where negative
     padding would make no sense. */
  padding: max(0px, calc((var(--fg-hauteur) - var(--fg-image-h)) / 2)) 0;

  margin: 0;
  list-style: none;
}

.focus-gallery__cellule {
  display: flex;
  flex-direction: column;
  align-items: center;

  /* Measured on the reference, where it serves no purpose: its cell has only
     one child. It takes its role back as soon as a caption is slipped under the
     image. */
  gap: var(--fg-ecart-legende);
}

/* ==========================================================================
   4. Frames: the two states
   --------------------------------------------------------------------------
   The frame is a <button>, not a clickable <div>: it comes with the role, the
   focus, the Enter key and the space bar. All that is taken away from it is its
   styling.
   ========================================================================== */

.focus-gallery__cadre {
  flex: none;
  width: var(--fg-image-l);
  height: var(--fg-image-h);
  padding: 0;
  border: 0;
  border-radius: var(--fg-rayon);
  overflow: hidden;
  background: none;
  font: inherit;
  color: inherit;
  appearance: none;
  -webkit-appearance: none;

  /* Default state = full image. That is what a visitor without JavaScript
     sees; the script then switches to the two measured states. */
  transform: none;
  opacity: 1;
  box-shadow: var(--fg-ombre-active);
  cursor: default;

  transition:
    transform var(--fg-duree) var(--fg-courbe),
    opacity var(--fg-duree) var(--fg-courbe),
    box-shadow var(--fg-duree-ombre) var(--fg-courbe);
}

.focus-gallery__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;

  /* The image is decorative from the point of view of the button: it is the
     button that carries the accessible name. It must not intercept the drag. */
  -webkit-user-drag: none;
  user-select: none;
}

/* The two states only apply once the script is in place: it alone knows which
   of the images is at the centre, and a resting image with no active image
   would be an entirely greyed-out gallery. */
.focus-gallery[data-fg-pret] .focus-gallery__cadre {
  transform: scale(var(--fg-echelle-repos));
  opacity: var(--fg-opacite-repos);
  box-shadow: var(--fg-ombre-repos);
  cursor: pointer;
  will-change: transform;
}

.focus-gallery[data-fg-pret] .focus-gallery__cadre[aria-current="true"] {
  transform: scale(var(--fg-echelle-active));
  opacity: var(--fg-opacite-active);
  box-shadow: var(--fg-ombre-active);

  /* Nothing more to do on the image at the centre: it is the default cursor
     that says it is already where a click would bring it. Measured on the
     reference, which does not react to hover either. */
  cursor: default;
}

/* ==========================================================================
   5. Title and note
   --------------------------------------------------------------------------
   Both texts are anchored at 50 % of the height *by their top*, with no
   translation to recentre them: that is what the reference does, and it is what
   places the title slightly below the optical axis of the image.
   ========================================================================== */

.focus-gallery__titre,
.focus-gallery__note {
  position: absolute;
  top: 50%;
  max-width: var(--fg-texte-largeur);
  margin: 0;
  z-index: 2;

  /* Without this, the wheel used over a text scrolls nothing: the event would
     start from the text, which is not inside the scrolling area. The text also
     stops being selectable — the reference accepts that, and it is the price of
     two captions laid over a scrolling area. */
  pointer-events: none;
}

.focus-gallery__titre {
  left: var(--fg-marge);
  font-size: var(--fg-titre-corps);
  font-weight: var(--fg-titre-graisse);
  line-height: 1;
  letter-spacing: -0.04em;
  text-wrap: balance;
}

.focus-gallery__note {
  right: var(--fg-marge);
  font-size: var(--fg-note-corps);
  font-weight: var(--fg-note-graisse);
  line-height: 1.3;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

/* ==========================================================================
   6. Focus
   --------------------------------------------------------------------------
   The outline is set outside the frame. It survives overflow: hidden, which
   only clips the children, and it follows the scale of the image since it is
   painted after the transform.
   ========================================================================== */

.focus-gallery__cadre:focus-visible {
  outline: var(--fg-focus-epaisseur) solid var(--fg-focus);
  outline-offset: var(--fg-focus-ecart);
}

/* ==========================================================================
   7. Without JavaScript
   --------------------------------------------------------------------------
   The reference is a React component: its page is empty if the script does not
   run. Here the layout is already complete — all that is missing is the choice
   of the centred image, which is replaced by native snapping.
   ========================================================================== */

.focus-gallery:not([data-fg-pret]) .focus-gallery__vue {
  scroll-snap-type: y mandatory;
}

.focus-gallery:not([data-fg-pret]) .focus-gallery__cellule {
  scroll-snap-align: center;
}

/* ==========================================================================
   8. Narrow screens
   --------------------------------------------------------------------------
   The reference does not adapt: at 390 px wide, its active image runs 30 px
   past each side. Two corrections, and only two.

   The width is already handled by --fg-font-size, § 1. What is left is the room
   for the texts: set on the sides, they end up biting into the image. The
   threshold can be computed — the margin and the quarter width of the text have
   to fit within (100vw − 28.125 body copies) / 2, which gives 1060 px for a
   16 px body copy. Below that, the texts move above and below.
   ========================================================================== */

@media (max-width: 1060px) {
  .focus-gallery__titre,
  .focus-gallery__note {
    top: auto;
    left: var(--fg-marge);
    right: var(--fg-marge);
    max-width: none;
  }

  .focus-gallery__titre {
    top: var(--fg-marge);
  }

  .focus-gallery__note {
    bottom: var(--fg-marge);
    text-align: right;
  }
}

/* ==========================================================================
   9. Reduced motion, forced colours
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .focus-gallery {
    /* The durations are reset to zero through the tokens rather than through
       the properties: a transition cancelled on one side and redeclared on the
       other always comes back in the end. The delays follow, a delay being
       nothing but movement spread over time. */
    --fg-duree: 0s;
    --fg-duree-ombre: 0s;
  }

  .focus-gallery__vue {
    scroll-behavior: auto;
  }
}

@media (forced-colors: active) {
  /* Shadows and opacities are no longer rendered: without them, nothing tells
     the image at the centre apart. That distinction is handed to the outline,
     and the faded images are brought back to full opacity so that they stay
     readable. */
  .focus-gallery[data-fg-pret] .focus-gallery__cadre {
    opacity: 1;
    box-shadow: none;
    outline: 1px solid CanvasText;
  }

  .focus-gallery[data-fg-pret] .focus-gallery__cadre[aria-current="true"] {
    outline: var(--fg-focus-epaisseur) solid Highlight;
  }

  .focus-gallery__cadre:focus-visible {
    outline: var(--fg-focus-epaisseur) solid Highlight;
    outline-offset: var(--fg-focus-ecart);
  }
}
