/* ==========================================================================
   Infinite Canvas 1.0 — an endlessly tiled, draggable wall of pictures
   --------------------------------------------------------------------------
   Plain CSS. No framework, no build step, no external request.

   Two blocks live in this file. They are independent — take one, take both:

     .infinite-canvas        the wall
     .infinite-canvas-tabs   the floating segmented pill

   PROPORTIONS
   Every metric is an em derived from --ic-font-size. The ratios come from the
   reference (see README.md, "Provenance"); the numbers in brackets are what
   they resolve to at the reference size of 20px.

     text                1        (the reference unit)      [20 px]
     column width       15 em     the width of one picture  [300 px]
     gutter              0.4 em   horizontal and vertical   [8 px]
     column pitch       15.4 em   column + gutter           [308 px]
     picture radius      0.4 em                             [8 px]
     tab padding         0.3 em vertical, 0.7 em horizontal [6 / 14 px]
     tab height          2.1 em   1.5 line + 2 x 0.3        [42 px]
     pill padding        0.2 em                             [4 px]
     pill gap            0.3 em   between the two tabs      [6 px]
     pill radius         0.5 em                             [10 px]
     thumb radius        0.3 em                             [6 px]
     pill offset         2 em     above the bottom edge     [40 px]

   So --ic-font-size rescales everything. --ic-col is broken out separately
   because making the labels bigger and making the pictures bigger are not the
   same intention; it defaults to 15em, so it follows unless you say otherwise.

   ONE RULE IF YOU RESTYLE THIS
   Do not change font-size inside .infinite-canvas. The tokens are ems, and an
   em resolves against the element that *uses* it, not the one that declares
   it. Every metric would shift under you. Change --ic-font-size instead.

   Order of this file:
     1. Face declarations
     2. Design tokens
     3. Box sizing
     4. The wall: shell, plane, tiles, pictures
     5. The wall without JavaScript
     6. The pill: shell, tabs, thumb
     7. Focus
     8. Fallbacks: reduced motion, forced colours
   ========================================================================== */

/* ==========================================================================
   1. Face declarations
   --------------------------------------------------------------------------
   Instrument Sans 500, the weight the reference uses, in two local subsets.
   Latin alone covers French and English; latin-ext is fetched only if the page
   actually prints a character that needs it. Only the pill sets type — drop
   both @font-face rules if you use the wall on its own.
   ========================================================================== */

@font-face {
  font-family: "Instrument Sans";
  src: url("InstrumentSans-500-latin.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Instrument Sans";
  src: url("InstrumentSans-500-latin-ext.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ==========================================================================
   2. Design tokens
   --------------------------------------------------------------------------
   Everything you are meant to change is here. Colours, metrics and durations
   alike — the drift speed and the easing included, so the motion can be tuned
   without opening the JavaScript.
   ========================================================================== */

.infinite-canvas,
.infinite-canvas-tabs {
  /* Scale */
  --ic-font-size: 20px;
  --ic-col: 15em;
  --ic-gap: 0.4em;

  /* Wall */
  --ic-bg: #000;
  --ic-img-bg: #1a1a1a;
  --ic-img-radius: 0.4em;

  /* Pill */
  --ic-pill-bg: #262626;
  --ic-pill-pad: 0.2em;
  --ic-pill-gap: 0.3em;
  --ic-pill-radius: 0.5em;
  --ic-pill-bottom: 2em;
  --ic-tab-pad-y: 0.3em;
  --ic-tab-pad-x: 0.7em;
  --ic-tab-on: #000;
  --ic-tab-off: #d6d6d6;
  --ic-thumb: #fff;
  --ic-thumb-radius: 0.3em;

  /* Motion. --ic-drift is pixels per second, vertically, when nobody is
     touching the wall; 0 switches it off. --ic-friction is how fast a throw
     dies down, per frame at 60 Hz: closer to 1 glides longer. */
  --ic-drift: 8;
  --ic-friction: 0.94;
  --ic-thumb-duration: 260ms;
  --ic-ease: cubic-bezier(0.32, 0.72, 0, 1);

  /* Focus */
  --ic-focus: #fff;
  --ic-focus-width: 2px;
  --ic-focus-offset: 2px;
}

/* ==========================================================================
   3. Box sizing
   --------------------------------------------------------------------------
   Declared here rather than assumed: the component has to survive being
   dropped into a page with no reset at all.
   ========================================================================== */

.infinite-canvas,
.infinite-canvas *,
.infinite-canvas *::before,
.infinite-canvas *::after,
.infinite-canvas-tabs,
.infinite-canvas-tabs *,
.infinite-canvas-tabs *::before,
.infinite-canvas-tabs *::after {
  box-sizing: border-box;
}

/* ==========================================================================
   4. The wall
   --------------------------------------------------------------------------
   The shell clips. Inside it, one plane holds a grid of identical tiles, and
   only the plane ever moves: a single transform per frame, whatever the number
   of pictures. Tiles and pictures are laid out once and never touched again.
   ========================================================================== */

.infinite-canvas {
  position: relative;
  /* Scrolls until the script takes over — that is the whole no-JS fallback,
     and it costs nothing to get for free. */
  overflow: auto;
  background: var(--ic-bg);
  font-size: var(--ic-font-size);
  font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
  font-weight: 500;
}

/* Only once the script is in charge: before that the wall is a normal,
   natively scrollable column layout, and grabbing the page must still work. */
.infinite-canvas.is-enhanced {
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;

  /* clip, not hidden. A hidden box is still a scroll container: script can
     scroll it, and the browser will scroll it on its own to reveal whatever
     just took focus. Either one shifts the wall permanently, and nothing puts
     it back — the plane's transform is the only position the component knows
     about. clip refuses to scroll at all. hidden stays underneath for Safari
     below 16. */
  overflow: hidden;
  overflow: clip;
}

.infinite-canvas.is-dragging {
  cursor: grabbing;
}

.infinite-canvas__plane {
  position: absolute;
  top: 0;
  left: 0;
  /* Written by the script, once per frame. */
  transform: translate3d(var(--ic-x, 0px), var(--ic-y, 0px), 0);
  will-change: transform;
}

.infinite-canvas__tile {
  position: absolute;
  top: 0;
  left: 0;
  /* Placed once, at build time, on the tile grid. */
  transform: translate3d(var(--ic-tile-x, 0px), var(--ic-tile-y, 0px), 0);
}

.infinite-canvas__plane .infinite-canvas__img {
  position: absolute;
  left: var(--ic-item-x, 0px);
  top: var(--ic-item-y, 0px);
  display: block;
  width: var(--ic-col);
  height: auto;
  background: var(--ic-img-bg);
  border-radius: var(--ic-img-radius);
  object-fit: cover;
  pointer-events: none;
  -webkit-user-drag: none;
}

/* ==========================================================================
   5. The wall without JavaScript
   --------------------------------------------------------------------------
   The source list *is* the wall until the script runs. Same column width, same
   gutter, same radius — it simply scrolls instead of being dragged, which is
   what a wall of pictures should do when nothing is listening.

   Once enhanced, the same list stays in the DOM but leaves the page: it is
   what screen readers read. The moving plane is decorative repetition and is
   marked aria-hidden by the script, so each picture is announced exactly once.
   ========================================================================== */

.infinite-canvas__source {
  columns: var(--ic-col) auto;
  column-gap: var(--ic-gap);
  padding: var(--ic-gap);
}

.infinite-canvas__source .infinite-canvas__img {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: var(--ic-gap);
  background: var(--ic-img-bg);
  border-radius: var(--ic-img-radius);
  break-inside: avoid;
}

/* Off the page, still in the accessibility tree. Not display:none — a hidden
   control cannot take focus, and a hidden image is not announced. */
.infinite-canvas.is-enhanced .infinite-canvas__source {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ==========================================================================
   6. The pill
   --------------------------------------------------------------------------
   Native radios in labels: grouping, arrow keys, Home/End and the form value
   all come from the browser. The script adds the sliding thumb and nothing
   else — without it the white background simply sits on the checked tab, which
   is pixel-for-pixel the same result, minus the slide.
   ========================================================================== */

.infinite-canvas-tabs {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--ic-pill-gap);
  padding: var(--ic-pill-pad);
  background: var(--ic-pill-bg);
  border-radius: var(--ic-pill-radius);
  font-size: var(--ic-font-size);
  font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
  font-weight: 500;
  line-height: 1.5;
}

/* Floating variant: centred over the bottom edge of its positioned parent. */
.infinite-canvas-tabs--floating {
  position: absolute;
  bottom: var(--ic-pill-bottom);
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
}

.infinite-canvas-tabs__tab {
  position: relative;
  display: block;
  cursor: pointer;
}

/* 1px and transparent rather than display:none, so it still takes focus and
   still answers to the arrow keys. */
.infinite-canvas-tabs__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}

.infinite-canvas-tabs__text {
  position: relative;
  z-index: 1;
  display: block;
  padding: var(--ic-tab-pad-y) var(--ic-tab-pad-x);
  border-radius: var(--ic-thumb-radius);
  color: var(--ic-tab-off);
  white-space: nowrap;
  transition: color var(--ic-thumb-duration) var(--ic-ease);
}

/* No script: the checked tab carries the white background itself. */
.infinite-canvas-tabs__input:checked + .infinite-canvas-tabs__text {
  background: var(--ic-thumb);
  color: var(--ic-tab-on);
}

/* Script: the background moves out onto the thumb, which can slide. */
.infinite-canvas-tabs.is-enhanced .infinite-canvas-tabs__input:checked
  + .infinite-canvas-tabs__text {
  background: transparent;
  color: var(--ic-tab-on);
}

.infinite-canvas-tabs__thumb {
  position: absolute;
  top: var(--ic-pill-pad);
  left: var(--ic-pill-pad);
  height: calc(1.5em + 2 * var(--ic-tab-pad-y));
  width: var(--ic-thumb-w, 0px);
  background: var(--ic-thumb);
  border-radius: var(--ic-thumb-radius);
  transform: translateX(var(--ic-thumb-x, 0px));
  transition:
    transform var(--ic-thumb-duration) var(--ic-ease),
    width var(--ic-thumb-duration) var(--ic-ease);
}

/* The very first placement must not animate from zero. */
.infinite-canvas-tabs__thumb[data-ic-settling] {
  transition: none;
}

/* ==========================================================================
   7. Focus
   --------------------------------------------------------------------------
   :focus-visible only, so a mouse click does not draw a ring, but Tab does.
   ========================================================================== */

.infinite-canvas-tabs__input:focus-visible + .infinite-canvas-tabs__text {
  outline: var(--ic-focus-width) solid var(--ic-focus);
  outline-offset: var(--ic-focus-offset);
}

.infinite-canvas:focus-visible {
  outline: var(--ic-focus-width) solid var(--ic-focus);
  outline-offset: calc(-1 * var(--ic-focus-width));
}

/* ==========================================================================
   8. Fallbacks
   ========================================================================== */

/* Reduced motion: no drift, no glide, no slide. Delays go to zero too — a
   delay is movement spread over time. The wall is still fully draggable; what
   stops is everything that moves on its own. */
@media (prefers-reduced-motion: reduce) {
  .infinite-canvas,
  .infinite-canvas-tabs {
    --ic-drift: 0;
    --ic-friction: 0;
    --ic-thumb-duration: 1ms;
  }

  .infinite-canvas-tabs__text,
  .infinite-canvas-tabs__thumb {
    transition-duration: 1ms;
    transition-delay: 0s;
  }
}

/* Forced colours: system colours win, so the thumb has to be redrawn as a
   border rather than a fill, and the pictures need an edge to exist at all. */
@media (forced-colors: active) {
  .infinite-canvas {
    border: 1px solid CanvasText;
  }

  .infinite-canvas__img {
    border: 1px solid CanvasText;
  }

  .infinite-canvas-tabs {
    border: 1px solid CanvasText;
  }

  .infinite-canvas-tabs__thumb {
    background: Highlight;
    forced-color-adjust: none;
  }

  .infinite-canvas-tabs__text {
    color: CanvasText;
  }

  .infinite-canvas-tabs__input:checked + .infinite-canvas-tabs__text {
    color: HighlightText;
    background: Highlight;
  }

  .infinite-canvas-tabs.is-enhanced
    .infinite-canvas-tabs__input:checked
    + .infinite-canvas-tabs__text {
    color: HighlightText;
    background: transparent;
  }
}
