/* Les œuvres de Luc Laumet
   ------------------------------------------------------------------------
   The design is made of photographed objects — torn paper, rope, crumpled
   balls, a framed canvas — laid on red paper. Everything here is in service
   of that: no gradients, no rounded corners, no drop shadows that a real
   object would not cast.

   The site's Content-Security-Policy is `style-src 'self'`, which blocks
   style ATTRIBUTES and injected <style> elements but not the CSSOM: setting
   `el.style.left` from a script is allowed, and that is how the paper balls
   are scattered. Anything that repeats — the card rotations — is still a
   class here rather than something written per element. */

/* ---------- fonts ---------- */

/* One face for the whole site. The title is the same face asking for 700,
   which the browser thickens for itself. */
@font-face {
  font-family: "Special Elite";
  src: url("/fonts/specialelite.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ---------- tokens ---------- */

:root {
  /* Measured from the middle of the red-paper photograph, away from its
     vignetted edges. */
  --red: #930c11;
  /* The exact flat red the rope dividers are cut out against — every solid
     pixel in both files is this value, so the gallery band must be it too or
     a seam appears where the divider meets the band. */
  --red-dark: #590000;
  --ink: #16100f;          /* the near-black of the hand-set title */
  /* The mount a print is laid on. A warm off-white rather than a true white:
     the photographed paper on this page — the torn scraps, the crumpled balls
     — measures around luminance 220, so a mount at 252 glared next to it. This
     sits at 231, close to that paper but warm where the photographs are cool. */
  --paper: #efe6d6;
  --line: #a3151a;         /* the thin red rule drawn around each print */
  --cream: #efe7d9;        /* body text on red */

  /* The rope dividers are drawn at the same scale as everything else rather
     than stretched to the page, so their height follows from their own native
     size (3003 × 205 and 3003 × 221) exactly like the paper scraps do. At that
     scale they are wider than the page and get clipped, which is fine — they
     are a length of rope, not a picture with edges that matter. */
  --rope-top-h: calc(var(--scrap) * 205);
  --rope-bottom-h: calc(var(--scrap) * 221);
  /* Neither divider is half rope and half red. The top one only turns solid
     at 87.3% of its height, the bottom one stops being solid at 27.1% — so
     those, not one half, are how far each must overlap the dark band. */
  --rope-top-overlap: 0.127;
  --rope-bottom-overlap: 0.271;

  --body: "Special Elite", "Courier New", ui-monospace, monospace;

  /* ---- object scale ----------------------------------------------------
     Every photographed object in a group was shot at one scale, so the
     lettering on the paper scraps is the same size on every scrap, and the
     paper balls are the same size as the ones lying around the frame in the
     Luck photograph. The page only preserves that if each object is drawn at
     the SAME fraction of its own native pixel size.

     These two numbers are that fraction, expressed as viewport width per
     native pixel. Each object then asks for `calc(var(--x) * <native width>)`
     — never a hand-picked percentage, which is how the two groups drifted
     apart in the first place. To resize a whole group, change one number.

       --scrap  the torn-paper buttons: categories 374px, Chope 259px,
                FR/EN 159px, Luc (Luck) 374px
       --art    the hero photographs: the Luck frame 841px and the fifteen
                paper balls, 156px to 364px

     Native widths are listed beside each rule so the arithmetic is checkable
     against the files in Assets/. Both groups sit at the same 0.600 here, and
     the rope dividers take --scrap too.

     The `min(…, 1px)` is what stops anything being drawn larger than the
     photograph it came from. Because each object multiplies this one unit by
     its own native width, capping the unit at 1px caps every object at exactly
     its native size, all at once — here that happens at a 2133px viewport.
     Past that the composition stops growing and holds its size in the middle
     of the page; the extra width fills with more paper balls instead. */
  --scrap: min(0.046875vw, 1px);
  --art: min(0.046875vw, 1px);

  /* Where the cap bites, and therefore how wide the centred composition can
     ever get: every object at native size, laid out as at that width. */
  --stage-max: 2133px;

  /* The three biography photographs are drawn a little under native size, so
     that the arrangement can be spread far enough to leave at least three
     quarters of each one showing. Desktop and tablet lay them out differently
     and so want different figures; the phone stacks them down the page and
     wants none. Because all three photographs read from this one number they
     keep their sizes relative to one another, which is the whole point of
     normalising them to equal area in the first place. */
  --bio-scale: 0.88;


  /* Shadows cast by anything the page draws itself, matched to the ones
     already in the photographs. Measured off Papier 5, the penumbra is two
     pixels across a 246px ball — the light is hard, so these are nearly crisp
     rather than soft. They fall down and to the right, like the photographed
     ones, and they are dark: the shadow in the nav cut-outs reads at about
     half the luminance of the paper it lies on. */
  --cast: 9px 11px 4px rgba(0, 0, 0, 0.55);

  /* How many paper balls to throw. app.js reads this, so the decision stays
     next to the other breakpoint decisions rather than being duplicated in
     JavaScript. Keep it to what the free red will comfortably hold: ask for
     more than fits and a different number lands on every load, which reads as
     broken rather than as chance. */
  --balls: 8;
}

/* ---------- base ---------- */

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  font-family: var(--body);
  font-size: clamp(0.82rem, 1.02vw, 1rem);
  line-height: 1.65;
  color: var(--cream);
  /* The red paper, drawn rather than photographed.

     Not a grey veil over a flat colour — grey over red desaturates it, and two
     such layers measured #971F24 against the photograph's #940C11. Instead the
     turbulence is mapped straight onto a red ramp by the per-channel transfer
     below, so the tile IS the paper: opaque, already the right hue, mean
     landing on #930C11 with roughly twice the photograph's grain.

     Two layers. Underneath, the felt itself: a fine tooth on a 300px tile.
     Over it at half opacity, a much slower turbulence on an 800px tile, which
     is the gentle unevenness of the table the felt is lying on. Red over red,
     so the hue survives the blend where grey over red would not.

     Three details matter. `color-interpolation-filters='sRGB'` — filters
     default to linearRGB, which is what threw an earlier attempt far off
     colour. `stitchTiles` with baseFrequency × tile size a whole number
     (0.613333 × 300 = 184, 0.00208333 × 960 = 2), which is what lets each
     repeat without a seam — it is why the warp's tile grew with its features
     rather than staying at 800. And two octaves on the felt, not more: at this
     frequency the second is already past the pixel grid, so further octaves
     add nothing but per-pixel noise. --red stays as the fallback for anything
     that cannot render the filter. */
  background-color: var(--red);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='960' height='960'%3E%3Cfilter id='w' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.00208333' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.432' intercept='0.36'/%3E%3CfeFuncG type='linear' slope='0.16' intercept='-0.033'/%3E%3CfeFuncB type='linear' slope='0.192' intercept='-0.029'/%3E%3CfeFuncA type='table' tableValues='1 1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='960' height='960' filter='url(%23w)' opacity='0.5'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='f' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.613333' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.324' intercept='0.414'/%3E%3CfeFuncG type='linear' slope='0.12' intercept='-0.013'/%3E%3CfeFuncB type='linear' slope='0.144' intercept='-0.005'/%3E%3CfeFuncA type='table' tableValues='1 1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23f)'/%3E%3C/svg%3E");
  background-size: 960px 960px, 300px 300px;
  background-repeat: repeat, repeat;
  overflow-x: hidden;
}

body.locked { overflow: hidden; }

img { max-width: 100%; }

a { color: inherit; }

:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 3px;
}

/* Present to screen readers and search engines, absent from the design. */
.sr-only,
.catalogue-index {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- hero ---------- */

/* Taller than the 1280 × 660 of the mockup: at this scale the frame and the
   button row very nearly touch in that space.

   The hero runs the full width of the page but its height stops growing where
   the art stops growing, so a very wide window gets a band of red either side
   of the composition rather than a taller and taller one. */
.hero {
  position: relative;
  width: 100%;
  aspect-ratio: 1280 / 820;
  min-height: 520px;
  max-height: calc(var(--stage-max) * 820 / 1280);
}

/* Everything except the balls sits in here, centred, and stops widening once
   every object has reached its native size. */
.hero-stage {
  position: relative;
  width: 100%;
  max-width: var(--stage-max);
  height: 100%;
  margin: 0 auto;
}

/* Special Elite ships one weight, so 700 is synthesised by the browser —
   which is the thickening the title wants. */
.hero-title {
  position: absolute;
  left: 3.2%;
  top: 7%;
  margin: 0;
  font-family: var(--body);
  font-weight: 700;
  font-size: clamp(1.15rem, 2.45vw, 2.1rem);
  line-height: 1.22;
  letter-spacing: -0.005em;
  color: var(--ink);
}

/* The framed "Luck" canvas: centred, and set down from the top edge so it has
   room to breathe rather than sitting flush against it. The photograph already
   carries its own scatter of paper balls. */
.hero-frame {
  position: absolute;
  left: 50%;
  top: 8%;
  width: calc(var(--art) * 841); /* Luck.png is 841px wide */
  height: auto;
  transform: translateX(-50%);
}

/* Paper scraps that act as links: the shop and the language switch. */
.scrap {
  position: absolute;
  display: block;
  transition: transform 0.18s ease;
}

.scrap img { display: block; width: 100%; height: auto; }

.scrap:hover, .scrap:focus-visible { transform: scale(1.06) rotate(-1.5deg); }

.scrap-shop { left: 84%; top: 42%; width: calc(var(--scrap) * 259); } /* Shop.png 259px */

/* Pinned to the top-right corner, so it reads as page furniture rather than
   as one more thing scattered on the paper. */
.scrap-lang { right: 3%; top: 4%; width: calc(var(--scrap) * 159); } /* FR.png 159px */

/* ---------- the paper scatter ---------- */

/* Balls are allowed to hang off the left, right and top edges, so this clips
   them at exactly those edges — which is also what stops a ball that runs
   past the right-hand side from widening the document and putting a
   horizontal scrollbar on the page. */
.papers {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.paper {
  position: absolute;
  height: auto;
}

/* Size is never chosen and never varies — each ball is drawn at --art times
   its own native width, so all fifteen stay in proportion with one another
   and with the balls photographed around the frame. They are never rotated
   or rescaled either: they are photographs of real objects, and stretching
   one would show. */
.paper-1  { width: calc(var(--art) * 184); }
.paper-2  { width: calc(var(--art) * 179); }
.paper-3  { width: calc(var(--art) * 206); }
.paper-4  { width: calc(var(--art) * 179); }
.paper-5  { width: calc(var(--art) * 246); }
.paper-6  { width: calc(var(--art) * 202); }
.paper-7  { width: calc(var(--art) * 210); }
.paper-8  { width: calc(var(--art) * 224); }
.paper-9  { width: calc(var(--art) * 218); }
.paper-10 { width: calc(var(--art) * 245); }
.paper-11 { width: calc(var(--art) * 209); }
.paper-12 { width: calc(var(--art) * 364); }
.paper-13 { width: calc(var(--art) * 176); }
.paper-14 { width: calc(var(--art) * 190); }
.paper-15 { width: calc(var(--art) * 156); }

/* Fallback positions, for anyone without JavaScript. With it, app.js hides
   every ball and then shows and places the handful it has drawn. */
.paper-1  { left: 12.5%; top: 22%; }
.paper-2  { left: 2.5%;  top: 38%; }
.paper-3  { left: 27%;   top: 2%; }
.paper-5  { left: 66%;   top: 8%; }
.paper-7  { left: 76%;   top: 2%; }
.paper-9  { left: 91%;   top: 26%; }
.paper-11 { left: 63%;   top: 54%; }
.paper-13 { left: 30%;   top: 56%; }

.paper-4, .paper-6, .paper-8, .paper-10, .paper-12, .paper-14, .paper-15 {
  display: none;
}

/* ---------- category buttons ---------- */

/* The four buttons sit side by side, centred on the page. The gap is small:
   the button PNGs are close to full-bleed — Estampes runs from 0.3% to 99.5%
   of its own width — so almost none of the visible separation comes from
   transparent margin, and it all has to be real space. */
.cats {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 8%;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  gap: 1.2%;
}

/* No max-width: capping this one and not the Chope or FR/EN scraps is exactly
   what makes the lettering drift apart on a wide screen. */
.cat {
  position: relative;
  display: block;
  width: calc(var(--scrap) * 374); /* the category PNGs are 374px wide */
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  transition: transform 0.18s ease;
}

.cat img {
  display: block;
  width: 100%;
  height: auto;
  transition: opacity 0.22s ease;
}

/* Both states are in the markup and cross-fade, so selecting a category never
   waits on a network request and never flashes an empty box. */
.cat-lit {
  position: absolute;
  inset: 0;
  opacity: 0;
}

.cat.on .cat-lit { opacity: 1; }
.cat.on .cat-dim { opacity: 0; }

.cat:hover, .cat:focus-visible { transform: translateY(-3px) rotate(-1deg); }

.cat:hover .cat-lit, .cat:focus-visible .cat-lit { opacity: 1; }
.cat:hover .cat-dim, .cat:focus-visible .cat-dim { opacity: 0; }

/* ---------- rope dividers ---------- */

/* The rope keeps the scale of the paper scraps rather than stretching to the
   page. Usually that makes it wider than the window and the page shows the
   middle of it. On a window wider than the rope's native 3003px it tiles
   instead — another length of rope joined on at each end. The join shows, but
   it only happens past 3000-odd pixels, and a rope that is one continuous
   length no matter how wide the window is would have to be stretched.

   Drawn as a background rather than an <img> because only a background can
   repeat. `auto 100%` sizes it by the height, so the proportions — and with
   them the point where the divider turns solid red — are untouched. */
.rope {
  position: relative;
  z-index: 3;
  background-repeat: repeat-x;
  background-position: center top;
  background-size: auto 100%;
  pointer-events: none;
}

/* Each divider is pulled so that the point where it turns solid red lands
   exactly on the edge of the dark band. Both are then the same #590000 and
   the join cannot be seen. */
.rope-top {
  height: var(--rope-top-h);
  background-image: url("/images/ui/rope-top.webp");
  margin-bottom: calc(var(--rope-top-h) * var(--rope-top-overlap) * -1);
}

.rope-bottom {
  height: var(--rope-bottom-h);
  background-image: url("/images/ui/rope-bottom.webp");
  margin-top: calc(var(--rope-bottom-h) * var(--rope-bottom-overlap) * -1);
}

/* ---------- the gallery ---------- */

/* Flat, not the grained red: it has to match the divider cut-outs exactly,
   and those were cut against a flat colour. */
.gallery {
  position: relative;
  background: var(--red-dark);
  padding:
    calc(var(--rope-top-h) * var(--rope-top-overlap) + 3.5rem) 4%
    calc(var(--rope-bottom-h) * var(--rope-bottom-overlap) + 3.5rem);
}

/* Masonry by way of multi-column: prints keep their own proportions and stack
   without gaps, and because every image carries its intrinsic width and height
   the columns are laid out correctly before a single photo has loaded. */
.grid {
  columns: 4;
  column-gap: clamp(1rem, 2.4vw, 2.4rem);
  max-width: 1400px;
  margin: 0 auto;
}

.card {
  display: block;
  width: 100%;
  margin: 0 0 clamp(1rem, 2.4vw, 2.4rem);
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  break-inside: avoid;
  transition: transform 0.2s ease;
}

/* A photographic print: an even 10px of mount hugging the artwork on all four
   sides, and nothing drawn over the work itself. */
.card-paper {
  position: relative;
  display: block;
  background: var(--paper);
  padding: 10px;
  box-shadow: var(--cast);
}

/* The artwork alone, and the positioning context for the badge drawn over
   it. It has to be the artwork and not the mount: a captioned card has a
   deeper foot, and a badge measured from the mount would sit below the
   photograph, on the paper, where its light ink is invisible. */
.card-art {
  position: relative;
  display: block;
}

.card-paper img {
  display: block;
  width: 100%;
  height: auto;
}

/* The name and year, written on the foot of the mount the way a print is
   captioned by hand. Only exhibitions carry one — they are the one category
   where every entry has a real title. The ink is the red of the band behind
   the card, the darkest colour already on the page. */
.card-caption {
  display: block;
  margin: 0.62rem 0.1rem 0.2rem;
  font-family: var(--body);
  font-size: clamp(0.72rem, 0.92vw, 0.88rem);
  line-height: 1.3;
  text-align: center;
  color: var(--red-dark);
}

/* How many photographs this work has, for the ones that have several. The
   mount is now too narrow to hold it, so it sits just inside the foot of the
   artwork; the shadow is what keeps it legible over a light one. */
.card-badge {
  position: absolute;
  right: 0.4rem;
  bottom: 0.28rem;
  font-family: var(--body);
  font-size: 0.7rem;
  line-height: 1;
  color: var(--paper);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

/* Prints laid down by hand sit at slight angles. */
.tilt-0 { transform: rotate(-0.66deg); }
.tilt-1 { transform: rotate(0.48deg); }
.tilt-2 { transform: rotate(-0.24deg); }
.tilt-3 { transform: rotate(0.78deg); }
.tilt-4 { transform: rotate(-0.54deg); }
.tilt-5 { transform: rotate(0.3deg); }

.card:hover, .card:focus-visible { transform: scale(1.03) rotate(0deg); }

.empty {
  font-family: var(--body);
  font-size: clamp(1.1rem, 2.2vw, 1.6rem);
  text-align: center;
  padding: 4rem 0;
  color: var(--cream);
}

/* The photographed "Plus" / "More" scrap, at the same scale as every other
   paper button. */
.more {
  display: block;
  width: calc(var(--scrap) * 159); /* Plus.png and More.png are 159px wide */
  margin: 2.5rem auto 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  transition: transform 0.18s ease;
}

.more img { display: block; width: 100%; height: auto; }

.more:hover, .more:focus-visible { transform: scale(1.06) rotate(-1.5deg); }

.more[hidden] { display: none; }

/* ---------- biography ---------- */

/* The bottom divider now sits entirely above this section, so no room needs
   reserving for it. The foot is for the pile of objects, which is anchored to
   the bottom-left and would otherwise reach up into the portrait. */
.bio {
  position: relative;
  padding: 3rem 5% 5rem;
  display: grid;
  grid-template-columns: 34% 1fr;
  column-gap: 2%;
}

/* Three photographs need the whole row, so unlike the two-photograph version
   this is not indented. */
.bio-head {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 3%;
}

/* Three photographs thrown down like prints. The arrangement changes shape
   with the page — spread out across a desktop, heaped on a tablet, stacked
   down a phone — but the photographs themselves never change size relative to
   one another. `npm run about` normalises them to equal area, so the widths
   below differ only because the shapes do. */
.bio-photos {
  flex: 1 1 auto;
  display: flex;
  justify-content: center;
  align-items: center;
}

/* Photographic prints, mounted white-edged like the gallery cards. */
.bio-photo {
  flex: none;
  display: block;
  height: auto;
  border: clamp(5px, 0.9vw, 12px) solid var(--paper);
  box-shadow: var(--cast);
}

.bio-photo-1 { width: calc(var(--scrap) * 536 * var(--bio-scale)); } /* 536 × 671 */
.bio-photo-2 { width: calc(var(--scrap) * 672 * var(--bio-scale)); } /* 672 × 536 */
.bio-photo-3 { width: calc(var(--scrap) * 706 * var(--bio-scale)); } /* 706 × 510 */

/* Luck_2 is the one to look at first: it overlaps the other two and rides a
   little higher than them. */
.bio-photo-2 { position: relative; z-index: 2; }
.bio-photo-1 { z-index: 1; }
.bio-photo-3 { z-index: 1; }

/* Desktop: spread out sideways, the outer two tucked under the middle one.
   The overlap is in --scrap units rather than a percentage, because a
   percentage would resolve against a container whose width the photographs
   are themselves deciding.

   134 is what leaves three quarters of the two outer photographs showing.
   Luck_2 is shorter than Luck_1 and does not cover its whole height, so the
   hidden share is smaller than the overlap looks: 134 units is a quarter of
   Luck_1's width but only 23% of its area. Measured, not estimated — 77.0%
   and 78.7% of the two outer photographs are visible. Widening the overlap
   again means shrinking --bio-scale to match, or the row stops clearing the
   Luc (Luck) button beside it. */
.bio-photo-1 { transform: rotate(-2.4deg); margin-right: calc(var(--scrap) * -134); }
.bio-photo-2 { transform: rotate(1.1deg) translateY(-7%); }
.bio-photo-3 { transform: rotate(2.8deg); margin-left: calc(var(--scrap) * -134); }

/* Perso.png is 374px, same as the category scraps, so it takes the same
   scale — "Luc (Luck)" is lettered at the size of "Tableaux". */
.cat-perso {
  flex: 0 0 auto;
  width: calc(var(--scrap) * 374);
}

.bio-body {
  grid-column: 2;
  text-align: right;
  margin-top: 2.5rem;
}

.bio-body p { margin: 0 0 1.4em; }

/* Contact line and colophon sit together at the foot of the text column, the
   contact level with the signature rather than dropped below the rights line. */
.bio-foot {
  grid-column: 2;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: clamp(2rem, 6vw, 6rem);
  margin-top: 2rem;
}

.contact { margin: 0; }

.contact a { text-decoration: none; border-bottom: 1px solid rgba(239, 231, 217, 0.4); }

.contact a:hover { border-bottom-color: var(--cream); }

.colophon { text-align: center; }

.signature { display: block; width: clamp(120px, 14vw, 210px); height: auto; margin: 0 auto; }

/* Deliberately quiet: the rights line is a shade darker than the paper it
   sits on, the way a printed colophon would be. */
.rights {
  margin: 0.4rem 0 0;
  font-size: 0.72rem;
  line-height: 1.4;
  color: #6d0a0d;
}

.foot-objets {
  position: absolute;
  left: 0;
  bottom: 0;
  /* Scaled with the paper scraps rather than as a share of the page, so the
     pile keeps its size relative to everything else and, like them, is never
     drawn larger than the photograph. */
  width: calc(var(--scrap) * 822); /* foot_objets_2.png is 822px wide */
  height: auto;
  pointer-events: none;
  z-index: 0;
}

/* At this size the pile's shadow reaches under the text column, so the words
   have to sit above it. */
.bio-body, .bio-foot { position: relative; z-index: 1; }

/* ---------- lightbox ---------- */

/* Three columns: an arrow, the photograph, an arrow. A grid rather than
   arrows floated over the picture, because then the arrows cannot land on the
   artwork at any window size — the middle column gives up width to them, and
   the photograph, which is bounded by both edges of that column, shrinks to
   suit. On anything but a phone that costs nothing: the enlarged photograph is
   limited by the window's height long before its width, so there is idle dark
   ground either side for the arrows to sit in. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  justify-items: center;
  gap: clamp(0.5rem, 1.6vw, 2rem);
  padding: 2rem;
  background: rgba(24, 2, 3, 0.92);
}

.lightbox[hidden] { display: none; }

.lb-panel {
  grid-column: 2;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  max-width: min(1200px, 100%);
  max-height: 100%;
}

.lb-stage {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}

#lb-img.lb-step { cursor: pointer; }

#lb-img {
  display: block;
  max-width: 100%;
  max-height: calc(100vh - 9rem);
  width: auto;
  height: auto;
  background: var(--paper);
  padding: 10px; /* the same mount the cards have */
  box-shadow: 14px 17px 6px rgba(0, 0, 0, 0.62);
}

.lb-btn, .lb-arrow {
  border: 0;
  background: none;
  color: var(--cream);
  font-family: var(--body);
  cursor: pointer;
  line-height: 1;
}

.lb-close {
  position: absolute;
  top: -2.2rem;
  right: 0;
  font-size: 1.5rem;
  padding: 0.2rem 0.5rem;
}

/* Off-white on the darkened ground: --cream at nine tenths reads clearly
   without the glare of a full-strength white, which beside a photograph lit
   this low would be the brightest thing on the screen. */
.lb-arrow {
  font-size: clamp(3rem, 5vw, 5rem);
  padding: 0.3rem 0.6rem;
  opacity: 0.9;
  transition: opacity 0.15s ease;
}

.lb-arrow:hover, .lb-arrow:focus-visible { opacity: 1; }

/* A work with one photograph has nowhere to step: the buttons go, and their
   two columns collapse to nothing. The gaps either side of the middle column
   remain, so the photograph stays centred. */
.lb-arrow[hidden] { display: none; }

.lb-arrow-prev { grid-column: 1; }
.lb-arrow-next { grid-column: 3; }

.lb-bar {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1.5rem;
}

.lb-bar p { margin: 0; font-family: var(--body); }

#lb-count { font-size: 0.8rem; opacity: 0.7; }

/* ---------- tablet ---------- */

@media (max-width: 900px) {
  /* The lettering is part of the photograph, so it shrinks with the scrap.
     Below this width the desktop scale would leave the button labels too
     small to read, so both groups step up — and every object inside a group
     follows, because they all read from these two numbers. */
  :root { --scrap: min(0.075vw, 1px); --art: min(0.075vw, 1px); }

  /* Larger scraps no longer fit on one line, so the buttons wrap to two rows,
     and the hero is tall enough to give the frame, the Chope scrap and those
     two rows a band each. */
  .hero { aspect-ratio: 1280 / 1880; }

  .hero-title { top: 4%; }

  /* Lower than the desktop 8%, to clear the title: at 0.600 the frame is wide
     enough that its left edge reaches under the second line. */
  .hero-frame { top: 12%; }

  .scrap-shop { left: 60%; top: 50%; }

  /* A wide column gap leaves room for exactly two per row, so the four wrap
     two-and-two rather than an unbalanced three-and-one. */
  .cats {
    flex-wrap: wrap;
    justify-content: center;
    gap: 2% 9%;
    bottom: 4%;
  }

  /* Fewer balls: once the frame, the two scraps and two rows of buttons have
     their places there is much less free red than on a desktop. */
  :root { --balls: 5; }

  .paper-5, .paper-11 { display: none; }

  .paper-1  { left: 2%;  top: 22%; }
  .paper-2  { left: 1%;  top: 42%; }
  .paper-3  { left: 27%; top: 2%; }
  .paper-7  { left: 72%; top: 2%; }
  .paper-9  { left: 87%; top: 22%; }
  .paper-13 { left: 32%; top: 45%; }

  .grid { columns: 2; }

  /* Tablet and phone read straight down: the Luc (Luck) button, the
     photographs, the text, then the foot. The foot is itself a row — the pile
     of objects on the left, and on the right the contact line above the
     signature and the rights notice.

     The pile joins the flow here rather than hanging off the bottom-left
     corner as it does on a desktop, and that is what keeps it off the words.
     At these widths it is 62% of the page: there is no arrangement of text
     around an object that size that stays readable, so instead of moving the
     text out of its way, the foot gives it a column of its own. It can then
     stay at full size — the same --scrap as everything else, so it holds its
     proportion against the paper buttons. */
  .bio {
    grid-template-columns: minmax(0, auto) minmax(9rem, 1fr);
    grid-template-areas:
      "head head"
      "body body"
      "pile foot";
    column-gap: 4%;
    row-gap: 2rem;
  }

  .bio-head { grid-area: head; }

  .bio-body { grid-area: body; }

  /* Out of the corner and into the row. The 9rem floor on the second column
     is what the contact line and the signature need, and it is never called
     on across the tablet range — at 621px, the narrowest, the pile is 383px
     of a 559px row and the stack still gets 154px. The phone block below
     takes over where it stops being true. */
  .foot-objets {
    grid-area: pile;
    position: static;
    align-self: end;
    max-width: 100%;
  }

  .bio-foot {
    grid-area: foot;
    align-self: end;
    flex-direction: column;
    align-items: flex-end;
    justify-content: flex-end;
    gap: 1.5rem;
    margin-top: 0;
  }

  /* Piled rather than spread, so the row no longer needs the overlap that
     kept the desktop version narrow. */
  .bio-photo-1, .bio-photo-3 { margin-left: 0; margin-right: 0; }

  /* Tablet: a heap. All three share one grid cell, so the block sizes itself
     to the largest of them, and each is nudged out from the middle.

     The heap has the row to itself, with the Luc (Luck) button above it:
     the nudges are transforms, which do not enlarge the box, so set beside
     the button the topmost photograph would slide over it.

     Nudging out from a shared centre is the trap here. Every photograph
     starts on the same point, so a nudge of a fifth of its own width barely
     parts them — that arrangement left only 23% of Luck_1 showing. Two things
     fix it. The sideways nudges are in --scrap units, not percentages of each
     photograph's own differing width, so the heap can be centred on its
     rotated extents and spread to the full width available. And the heap is
     staggered up and down as well as sideways: vertical room is free here,
     the button having moved below, and lifting the outer two clear of Luck_2
     buys back far more area than widening alone can. 80.0% and 80.7% of the
     outer two are visible, measured. */
  /* Reversed, so the Luc (Luck) button reads above the heap rather than
     below it. The heap reserves its own upward stagger as padding, so the
     raised photographs cannot reach the button. */
  .bio-head { flex-direction: column-reverse; align-items: center; gap: 2rem; }

  :root { --bio-scale: 0.78; }

  .bio-photos { display: grid; place-items: center; }

  /* The stagger throws the photographs clear of the grid cell, which is only
     as tall as the tallest of them. This is the room they need, so that the
     Luc (Luck) button below is not landed on. */
  .bio-photos { padding: calc(var(--scrap) * 126) 0 calc(var(--scrap) * 114); }

  .bio-photo { grid-area: 1 / 1; }

  .bio-photo-1 { transform: rotate(-8deg) translate(calc(var(--scrap) * -314.6), -28%); margin: 0; }
  .bio-photo-2 { transform: rotate(2.5deg) translate(calc(var(--scrap) * -42), 36%); }
  .bio-photo-3 { transform: rotate(9deg) translate(calc(var(--scrap) * 254.9), -24%); margin: 0; }


  /* Width comes from --scrap, which already steps down here. */
  .foot-objets { opacity: 0.9; }
}

/* ---------- phone ---------- */

@media (max-width: 620px) {
  /* The hero turns portrait: title, frame, shop scrap, then the buttons in a
     two-by-two block, matching the mobile design.

     The scraps have to be readable on a phone, so they take proportionally
     more of the width than the hero photographs do. Within each group
     everything still moves together. */
  /* 0.334 on a phone. */
  :root { --scrap: min(0.089vw, 1px); --art: min(0.089vw, 1px); }

  /* Taller than the desktop hero in proportion, so the frame, the two scraps
     and the two rows of buttons each get their own band without touching. */
  .hero { aspect-ratio: 284 / 620; min-height: 660px; }

  .hero-title { font-size: clamp(1rem, 4.6vw, 1.4rem); top: 3%; }

  .hero-frame { top: 10%; }

  .scrap-shop { left: 56%; top: 42%; }

  /* At 0.334 three buttons would fit across, which leaves one alone on the
     second row. The column gap is what holds them to two and two. */
  .cats {
    left: 0;
    right: 0;
    bottom: 5%;
    flex-wrap: wrap;
    justify-content: center;
    gap: 3% 6%;
  }

  /* A handful on a phone. The portrait hero is mostly frame and buttons, and
     three balls read as scattered where six would read as crowded. */
  :root { --balls: 3; }

  .paper-5, .paper-7, .paper-11, .paper-13 { display: none; }

  .paper-1  { left: 1%;  top: 21%; }
  .paper-2  { left: 86%; top: 45%; }
  .paper-3  { left: 52%; top: 1%; }
  .paper-9  { left: 1%;  top: 36%; }

  .gallery { padding-left: 6%; padding-right: 6%; }

  .bio { padding-left: 7%; padding-right: 7%; }

  /* The button is already above the photographs here; centred to sit over
     the middle of the column they make, as it does on a tablet. */
  .bio-head { flex-direction: column-reverse; align-items: center; gap: 1.5rem; }

  /* Phone: stacked down the page, Luck_2 at the head of the column and
     overlapping the two below it. `order` moves it without disturbing the
     reading order of the markup. */
  .bio-photos {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
  }

  /* Stacked one above another with the width to themselves, so they need no
     shrinking and the heap's stagger and padding are both undone. */
  :root { --bio-scale: 1; }

  .bio-photos { padding: 0; }

  .bio-photo-2 { order: -1; transform: rotate(1.6deg); }
  .bio-photo-1 { transform: rotate(-3.2deg); margin: -5% 0 0; }
  .bio-photo-3 { transform: rotate(3.8deg); margin: -5% 0 0; }

  .bio-body { text-align: left; margin-top: 2rem; }

  /* The foot row carries over from the tablet; only the spacing inside the
     right-hand stack is tighter, there being less of the page to spend.

     A phone cannot hold this object beside anything: at 375px the pile is
     274px of a 323px column, which leaves the contact line and the signature
     45px between them. Shrinking it is the wrong answer — it has to keep its
     proportion against the paper buttons. So it runs off the left edge
     instead, the way the paper balls hang off the top of the hero and the
     ropes off both sides. Its column takes whatever the stack leaves, the
     photograph is drawn at full size, and justify-self pins its right edge to
     that column so all the overflow goes leftwards, off the page. Overflow to
     the left of the viewport is clipped; it does not scroll. */
  .bio { grid-template-columns: minmax(0, 1fr) minmax(9rem, auto); }

  .foot-objets { max-width: none; justify-self: end; }

  .bio-foot { gap: 1rem; }

  .colophon { text-align: right; margin-left: 0; }



  /* A phone is the one case where the photograph is bounded by the window's
     width and not its height — 343px of a 375px screen — so an arrow beside it
     would be taken straight out of the artwork. They go underneath instead,
     one to each corner, which also puts them under the thumb rather than at
     the far edges of the reach. Swiping and tapping the photograph work here
     too, and are the more likely gestures. */
  .lightbox {
    padding: 1rem;
    grid-template-columns: auto 1fr auto;
    grid-template-rows: auto auto;
    /* Both rows packed into the middle together, rather than the photograph
       centred in a tall first row and the arrows stranded at the foot of the
       screen with a hand's width of nothing between them. */
    align-content: center;
    row-gap: 1.2rem;
  }

  .lb-panel { grid-column: 1 / -1; grid-row: 1; }

  /* Room for the row of arrows, on top of what the caption and the close
     button already take. */
  #lb-img { max-height: calc(100vh - 13rem); }

  .lb-arrow { grid-row: 2; font-size: 3rem; padding: 0.2rem 1rem; }

  .lb-arrow-prev { grid-column: 1; justify-self: start; }
  .lb-arrow-next { grid-column: 3; justify-self: end; }
}

/* ---------- wider than the art ---------- */

/* Past 2133px nothing grows any further, so every extra pixel of window is
   bare red either side of the composition. Throw more balls into it. */
@media (min-width: 2134px) { :root { --balls: 12; } }
/* Fifteen is the whole pool, so this is as high as the number can usefully
   go — ask for more and the same handful simply arrives every time. */
@media (min-width: 2700px) { :root { --balls: 14; } }

/* ---------- motion ---------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .card, .cat, .scrap, .cat img { transition: none; }
  .card:hover, .card:focus-visible { transform: none; }
  .cat:hover, .cat:focus-visible { transform: none; }
  .scrap:hover, .scrap:focus-visible { transform: none; }
}
