/* /fun/design, the list of things Shaun thinks are beautifully designed.
   Tokens, body, h1, .lede, .fun-top and the footer all come from fun.css. Only
   what a shelf needs is here.

   WHY A SHELF

   The first version of this page was one column of seventeen entries, every
   picture blown up to the full width of the reading column and boxed in a rule,
   at 14,319px of scroll. You never saw the collection, only whichever thing you
   had scrolled to. This is the treatment Shaun picked from four: the objects
   stand on a single rail you move along, and one note is open underneath.

   It is also the homage that page earns. Further down it, in his own words, the
   Stripe Press website is "an absolute masterclass" because it shows nothing
   but books and gives the appearance of a bookshelf. So this page shows nothing
   but the objects.

   WHY EVERY PICTURE IS ONE SIZE

   Each object used to stand at a height of its own, so a ring was small and a
   pizza oven was large. Shaun asked for them all to be the same size, on the
   rail and in the note, so one number here sets the lot: --stand-h for the
   rail, and a square cell for the note. Only width still varies, because it is
   read off each picture's own proportions and a shelf that made everything the
   same width would be a shelf of stretched photographs.

   WHY THE WRAPPER IS NOT CALLED .shelf

   It was, and fun.css already uses .shelf for the list of index cards, whose
   .shelf a rule outranks .stand on specificity no matter which sheet loads
   last. Every object on the rail was quietly getting that rule's 1.35rem of
   vertical padding, which took 43px off each picture and squashed it, so the
   ring rendered at no height at all. The wrapper is .the-shelf now, and the
   collision is gone rather than out-specified.

   WHY NOTHING IS IN A BOX

   build-design-page.py flood fills the white studio background of every press
   shot to this page's paper, so ten of these objects have no edges of their
   own. A border round one of them is a box drawn around nothing. Only the six
   photographs and screenshots, which do have edges, get a hairline, and that
   difference is load bearing rather than decorative: it separates a thing from
   a picture of a thing. */

/* Wider than the 46rem reading column fun.css sets, because a shelf holding
   seventeen objects wants the room. The writing stays narrow inside it. */
main { max-width: 62rem; }

.head { max-width: 46rem; }
.head .lede { margin-bottom: 0; }

/* --- the rail ---------------------------------------------------------- */

/* Every stand is this tall, and each one's width is this multiplied by --r,
   the aspect ratio the generator writes onto it. The track reads it too, so
   the rail is exactly as tall as the things standing on it. */
.the-shelf { --stand-h: 9.5rem; margin: 1.65rem 0 0; }

/* Bleeds to main's padding edges rather than to the viewport. A 100vw bleed
   includes the scrollbar, which is how a page acquires a horizontal scroll it
   never asked for.

   The .75rem above the objects is headroom rather than spacing. The track is
   exactly as tall as the things standing on it now, so with overflow-y hidden
   and nothing above them the rail clipped both the 4px hover lift and the top
   of the focus ring. The shelf's own top margin gives the same amount back, so
   the gap under the lede is unchanged at 38px. */
.rail {
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x proximity;
  margin: 0 -1.5rem;
  padding: .75rem 1.5rem 1.2rem;
  overscroll-behavior-x: contain;
}

/* The fade is the only thing saying there is more shelf than the window shows,
   so it earns its place. It is on a class rather than on the rail itself,
   because a fade that stayed put would still be dimming the last object once
   you had scrolled all the way to it, which reads as a fault and not a hint.
   shelf.js keeps the two classes in step with the scroll position. */
.rail.more-right {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 5rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 5rem), transparent);
}
.rail.more-left {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 5rem);
  mask-image: linear-gradient(to right, transparent, #000 5rem);
}
.rail.more-left.more-right {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 5rem, #000 calc(100% - 5rem), transparent);
  mask-image: linear-gradient(to right, transparent, #000 5rem, #000 calc(100% - 5rem), transparent);
}

.track {
  display: flex;
  align-items: flex-end;
  gap: 2.6rem;
  width: max-content;
  min-height: var(--stand-h);
  padding: 0 .5rem .1rem;
  border-bottom: 2px solid var(--graphite);
}

/* One height for all seventeen, and a width read off the picture rather than
   imposed on it, so the rail is uniform and nothing is stretched to make it so. */
.stand {
  position: relative;
  flex: none;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  height: var(--stand-h);
  width: calc(var(--stand-h) * var(--r));
  padding: 0;
  scroll-snap-align: center;
  text-decoration: none;
  color: inherit;
  transition: transform .16s;
}

/* The contact shadow. Without it the objects hang above the shelf line rather
   than stand on it. */
.stand::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -1px;
  transform: translateX(-50%);
  width: 118%;
  height: 9px;
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(40, 36, 28, .28), rgba(40, 36, 28, 0) 70%);
}

.stand img,
.stand svg { display: block; height: 100%; width: 100%; }
.stand.plate img { border: 1px solid var(--rule); background: var(--paper-2); }
.stand:hover { transform: translateY(-4px); }
.stand:focus-visible { outline: 2px solid var(--orange); outline-offset: 5px; }

.stand[aria-selected="true"]::before {
  content: "";
  position: absolute;
  inset: auto 0 -.6rem 0;
  height: 2px;
  background: var(--orange);
}

/* One line of small type. It held two spans laid out with space-between until
   the label beside the hint was cut, and a flex row justifying a single child
   does nothing, so the row went with the second span. */
.rail-strip {
  margin: 1rem 0 0;
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* --- the note underneath ------------------------------------------------ */

/* Hidden here rather than by shelf.js, so seventeen entries do not paint and
   then collapse on every load. The <noscript> block in the page head puts them
   all back and takes the rail away, and the first entry ships already open, so
   a page whose script never arrives still reads. */
.entry { display: none; }

/* One picture is one cell wide, and a comparison is two of them plus the gap
   between, so a photograph is the same size whichever kind of note it is in.
   The alternative, letting a comparison halve the column it was given, is what
   made the Oura ring and its charger 130px wide beside a 272px pizza oven. */
.entry { --shot-c: 14rem; }

.entry.is-open {
  display: grid;
  grid-template-columns: var(--shot-c) 1fr;
  gap: 1.4rem 2.4rem;
  align-items: start;
  border-top: 1px solid var(--rule);
  margin: 1.5rem 0 0;
  padding: 1.7rem 0 0;
}

/* The four comparisons need both pictures side by side, and in source order,
   because the Glowstone and NOCO notes each say "on the right is" and a layout
   that moved them makes Shaun's own sentence wrong. The Nest and the Oura ring
   carry no such wording, but they are the same kind of comparison and are laid
   out the same way rather than by which of them happens to name a side. */
.entry.is-pair.is-open { grid-template-columns: calc(var(--shot-c) * 2 + .8rem) 1fr; }

/* Every picture is inscribed in a square stage of the same size, so arrowing
   along the shelf no longer swings the picture from 112px tall for the Oura
   ring to 554px for the spoons, and the page stops jumping. Nothing is cropped
   to fit, because a photograph cropped square is a different photograph.

   The size is worked out from --r, the picture's own proportions, rather than
   left to object-fit: contain. Contain renders the same pixels but leaves the
   element itself at the full square, and the hairline on the six plates has to
   sit on the edge of the photograph. On a contained image it would sit on the
   edge of the stage instead, drawing a box around the empty paper beside a wide
   screenshot, which is the one thing this page has always refused to do. */
.entry .shot {
  margin: 0;
  display: grid;
  gap: .8rem;
  min-height: var(--shot-c);
  /* The stage is always a square tall so the note never changes height, but the
     picture sits at the top of it, level with the heading opposite. Centred in
     the square, a wide screenshot started 58px below the title it belongs to. */
  align-content: start;
}
.entry .shot.two { grid-template-columns: 1fr 1fr; }

/* A picture element is inline, so it shrinks to its own image, and an image
   asking for a percentage of a parent measured from that image is the circle
   the generator's docstring warns about. Chromium answers it with the width the
   file happens to be stored at, which is how the Glowstone mug came out 142px
   wide. As a block it fills the cell, so 100% below means the cell. */
.entry .shot picture { display: block; }

.entry .shot img, .entry .shot svg {
  display: block;
  /* The square is the standard size, or the cell if that is narrower, which is
     what a phone gives a comparison. Taking the smaller of the two before the
     ratio is applied is what keeps the two halves of a comparison equal: capping
     them afterwards with max-width shortened only the wider one: measured at a
     390px viewport, the Glowstone mug stood 224px beside its rival at 205px. */
  --s: min(var(--shot-c), 100%);
  width: min(var(--s), calc(var(--s) * var(--r)));
  height: auto;
  margin-inline: auto;
}
.entry .shot.plate img { border: 1px solid var(--rule); background: var(--paper-2); }

.entry h2 { margin: 0 0 .6rem; font-size: 1.5rem; }
.entry p { margin: 0 0 .8rem; font-size: .98rem; color: var(--ink-2); max-width: 38rem; }
.entry p:last-child { margin-bottom: 0; }

.visit { margin-top: 1.1rem; }
.visit a {
  font-family: var(--font-mono);
  font-size: .8rem;
  letter-spacing: .04em;
  color: var(--orange-deep);
  text-underline-offset: 3px;
}

/* The one entry that is a place rather than a product, and the only one with no
   photograph. It gets a drawing instead of a gap, so the absence reads as a
   decision rather than as an image that failed to arrive. */
.coast { width: 100%; height: auto; }
.coast .l { fill: none; stroke: var(--graphite); stroke-linecap: round; }
.coast .sea { stroke: var(--rule); }
.coast .v { fill: var(--graphite); }
.coast .roof { fill: var(--orange-deep); }
.stand.drawn .coast { background: var(--paper-2); border: 1px solid var(--rule); }

/* --- narrower windows --------------------------------------------------- */

@media (max-width: 46rem) {
  /* One height sizes the whole rail, so a narrower window is one smaller
     number rather than seventeen. Widths follow it, so the objects stay in
     proportion and stay uniform. */
  .the-shelf { --stand-h: 7.2rem; }
  .track { gap: 1.9rem; }
  .entry.is-open,
  .entry.is-pair.is-open { grid-template-columns: 1fr; gap: 1.1rem; }
  /* The stage hugs the picture rather than centring it in a wider box, so its
     left edge lines up with the writing that now sits underneath it. It also
     gives up its square minimum height here. Beside the writing that floor is
     what stops the note changing height from one object to the next, but stacked
     above it the floor is just paper, and a different amount of it each time:
     117px under the Stripe Press screenshot against 54px under the mugs. */
  .entry .shot { max-width: var(--shot-c); min-height: 0; }
  .entry .shot.two { max-width: 100%; }
}

@media (max-width: 34rem) {
  .rail { margin: 0 -1.15rem; padding: .75rem 1.15rem 1.2rem; }
  .entry h2 { font-size: 1.32rem; }
  /* A phone is swiped, not arrowed, so the hint advertises a key the reader
     does not have. The whole strip goes rather than only the span inside it,
     because the arrow hint is now its only content and hiding just that would
     leave an empty paragraph still holding its top margin. */
  .rail-strip { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .stand { transition: none; }
  .stand:hover { transform: none; }
}
