/* The sketchbook: a table you deal cards onto, and the catalogue below it.
   ---------------------------------------------------------------------------
   Colour comes entirely from site.css's tokens, so both themes are already
   handled and only the three surfaces this page invents — the grass mat, the
   card stock and the card's edge — are defined here, once per palette.

   One rule about the card: .card-face is the whole of a card's appearance, and
   nothing outside it should need to change to restyle one. Position, rotation,
   flipping and stacking live on .card and .card-inner above it; the face is
   just a picture with a name on it. The fan, the table and the grid all use the
   same face, which is why it is worth keeping that line clean. */

:root {
  /* Casino green, sampled off the grass albedo so the chips' punched centres
     still read as holes in the mat rather than as a second colour. The mat
     itself is the same in both themes: a playing mat does not go grey at night. */
  --felt: #2a4528;
  --felt-line: rgba(0, 0, 0, 0.18);
  --stock: #ffffff;        /* the card itself */
  /* The back is its own surface. Light keeps a near-white stock so the brand
     blue reads as ink on paper; dark uses the deck's grey, which is lighter
     than the table, so a face-down card still separates from it. */
  --back-stock: #F7F5F5;
  --stock-edge: rgba(0, 0, 0, 0.12);
  --card-shadow: 0 2px 6px rgba(0, 0, 0, 0.10), 0 12px 28px rgba(0, 0, 0, 0.10);
  --card-shadow-lift: 0 4px 10px rgba(0, 0, 0, 0.12), 0 22px 48px rgba(0, 0, 0, 0.18);
  /* What dice and chips cast on the felt. The mat is the same dark grass
     in both themes, so the shadow has to read on green, not on a light
     page: 0.26 vanishes into the nap. */
  --cast: rgba(0, 0, 0, 0.55);
  --cast-glow: 0 0 7px 2px rgba(0, 0, 0, 0.34);

  /* A playing card is 2.5 by 3.5 inches. Every card on this page — in the fan,
     on the table, in the grid — is that shape, so the works crop into it
     rather than each tile taking its own proportions. */
  --card-ar: 5 / 7;
  --card-w: 172px;         /* the hand; the grid sizes its own */
  /* A card on the table is smaller than one you are holding. That is partly
     perspective — the hand is nearer than the table — and mostly room: at the
     hand's size only three or four fit before they start burying each other. */
  --card-w-played: 118px;
  /* The card in the modal is the one card that is not 5:7 — in there it takes
     the proportions of the work it is showing — so it is not given a width but
     a box to fit inside, and a width alone could not have done the job: at one
     fixed width a 16:9 gif is a sliver and a 963x2400 painting runs off the
     bottom of a short window. This is the width the 5:7 card had, and --card-box
     is the square that card's own height describes, so no card in the modal is
     ever bigger in any direction than the one that used to be there was tall —
     and the 5:7 card, fitted into that square, comes out at exactly this width
     again, at every window size, which is why it is still written here. */
  --card-w-modal: min(320px, 72vw);
  --card-box: calc(var(--card-w-modal) / (5 / 7));
  /* The room the modal keeps around whatever it is holding. Named rather than
     written into the padding alone, because the fit has to subtract it from the
     window twice: a card sized to the whole window is a card that overflows it
     by exactly this, twice. */
  --modal-pad: 24px;
  /* The corner control's box - the link on a work that has one. Smaller than
     the live site's 36px because the card it sits on is smaller, and because
     it has to stay in the corner of a card that is tilted: the further it is
     from the corner the further it swings in as the card turns. */
  --corner-btn: 28px;
  /* site.css has no readable-on-the-accent token yet; style.css's two values
     are repeated here so the one control that fills with the accent has ink
     that passes on both palettes. */
  --accent-ink: #10131C;
  /* How far the cloth has been pulled open over the window. 0 is the
     ordinary table strip; 1 is the whole viewport. Driven by scroll-up
     at the top of the page, in sketchbook.js. */
  --felt-expand: 0;
}

/* So --felt-expand can ease. Without this the number jumps and the cloth
   pops; with it, scroll-up and Escape both interpolate the same value that
   drives the height, the overlap of the bar, and the chrome fade. */
@property --felt-expand {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

:root[data-theme="dark"] {
  --accent-ink: #16160A;   /* the yellow wants its own, warmer */
  --stock: #202020;
  --back-stock: #373737;
  --stock-edge: rgba(255, 255, 255, 0.14);
  /* Shadow does almost no work on a dark ground, so the card is separated by a
     lighter stock and a visible edge instead; the shadow only stops it looking
     pasted on. */
  --card-shadow: 0 2px 6px rgba(0, 0, 0, 0.4), 0 12px 28px rgba(0, 0, 0, 0.4);
  --card-shadow-lift: 0 4px 10px rgba(0, 0, 0, 0.55), 0 22px 48px rgba(0, 0, 0, 0.6);
  --cast: rgba(0, 0, 0, 0.62);
  --cast-glow: 0 0 7px 2px rgba(0, 0, 0, 0.4);
}

.sketchbook {
  --col: 1180px;
  --gutter: 24px;
  /* No column here: the table is meant to run the full width of the window, so
     the measure is put on the parts that want one instead of being put here and
     then fought by the one part that does not. */
  padding: 0 0 96px;
}

/* Everything except the table itself keeps the site's measure. */
#upcoming {
  max-width: var(--col);
  margin-left: auto;
  margin-right: auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* ---------- the table ---------- */

/* No padding-top on #table. The bar is already 63px (site.css), and that is
   the whole gap from the top of the window to the canvas — the 20px that used
   to sit here made the canvas start 83px down instead of 63. The bar itself
   is left alone. */

#felt {
  position: relative;
  /* Tall enough that a card can land well clear of the fan and still have the
     grid below it visible on a laptop. The floor keeps that true when the
     viewport is short — below it there is no room to play a card into.
     --felt-expand interpolates that strip toward the window: more scroll-up
     at the top of the page opens the cloth until it is the whole view.

     The pair, here as below: a phone held in landscape is wider than the
     narrow breakpoint and lands on this rule, and on such a browser vh is the
     window as it would be with the URL bar hidden whether or not it is. On vh
     alone the opening is taller than the view, and the name printed at its
     middle sits below the middle of what can be seen. */
  --felt-base: max(64vh, 460px);
  --felt-base: max(64dvh, 460px);
  height: calc(var(--felt-base) + (100vh - var(--felt-base)) * var(--felt-expand));
  height: calc(var(--felt-base) + (100dvh - var(--felt-base)) * var(--felt-expand));
  /* At rest the cloth is a card on the page: inset from the window's edges
     and the bar, with rounded corners. Opening it (--felt-expand toward 1)
     takes the inset and the rounding away together, and it also grows up
     over the 63px bar, so 1 is genuinely fullscreen and not a 100vh block
     that still sits under the crumb. */
  --felt-inset: 16px;
  --felt-radius: 20px;
  --felt-rest: calc(1 - var(--felt-expand));
  margin-top: calc(var(--felt-inset) * var(--felt-rest) - 63px * var(--felt-expand));
  margin-left: calc(var(--felt-inset) * var(--felt-rest));
  margin-right: calc(var(--felt-inset) * var(--felt-rest));
  border-radius: calc(var(--felt-radius) * var(--felt-rest));
  background-color: var(--felt);
  box-shadow: inset 0 0 0 1px var(--rule);
  overflow: hidden;
  /* The cards inside are dragged with the pointer; without this a drag on a
     touch screen scrolls the page out from under the card instead. */
  touch-action: none;
  /* Follow the custom property only. Transitioning height as well as the
     variable that drives it double-eases the box, and the lag at the
     bottom is an empty strip of --felt before the nap is there. */
  transition: --felt-expand 90ms linear;
}

/* The bar is sticky above the cloth. While the cloth is opening it has to
   paint over that bar and the bar has to stop catching clicks, or the top
   63px of the mat would be an invisible hole. */
html.felt-covering {
  overscroll-behavior-y: none;
}
html.felt-covering #felt {
  z-index: 11;
  will-change: height;
}
html.felt-covering #topbar {
  opacity: calc(1 - var(--felt-expand));
  transform: translateY(calc(-10px * var(--felt-expand)));
  pointer-events: none;
  /* The bar's own hide-on-scroll transition would trail the gesture. */
  transition: none;
}
html.felt-covering #upcoming {
  opacity: calc(1 - var(--felt-expand));
  pointer-events: none;
}
html.felt-fullscreen { overflow: hidden; }
html.felt-snapping #felt {
  transition: --felt-expand 480ms cubic-bezier(0.22, 1, 0.36, 1);
}
html.felt-covering.felt-snapping #topbar {
  transition: opacity 480ms cubic-bezier(0.22, 1, 0.36, 1),
              transform 480ms cubic-bezier(0.22, 1, 0.36, 1);
}
html.felt-snapping #upcoming {
  transition: opacity 480ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Phones never open the cloth over the window: there is not enough window to
   be worth covering, and the gesture is a scroll-up, which a finger dragging
   the table is not.

   Width only. This used to opt out on a coarse pointer or no hover as well,
   which reads as "a phone" and is not: a laptop with a touchscreen matches
   both, so does an embedded webview, and on those machines the cloth would be
   pinned shut here while sketchbook.js — which no longer asks either question —
   opened it anyway. That combination is the worst of the three outcomes: the
   page locks its overflow for a cloth that never appears. The two guards have
   to agree, and this is the one they can both see. */
@media (max-width: 700px) {
  :root { --felt-expand: 0 !important; }
  html.felt-covering #felt { z-index: auto; }
  html.felt-covering #topbar {
    opacity: 1;
    transform: none;
    pointer-events: auto;
    transition: transform 0.35s ease, opacity 0.35s ease;
  }
  html.felt-covering #upcoming { opacity: 1; pointer-events: auto; }
  html.felt-fullscreen { overflow: visible; }
}

/* The table itself, which is larger than the opening you see it through and
   slides under it. Its transform makes it a stacking context, which is what
   keeps every played card's z-index inside it and below the hand's - without
   that the tenth card played would climb over the cards you are holding. */
#surface {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  /* Chips and dice sit above every card on the table, picked up or not.
     Counted in the tens of thousands so a long session's topZ, and the
     shuffle's 1000-band, cannot climb over them. */
  --die-z: 40000;
  /* Half again as big as the window onto it — or a laptop-sized cloth,
     whichever is larger. A phone's window is small; the table behind it is
     not, so there is the same room to push a card into. sketchbook.js will
     grow this to match a wider window and will not shrink it, so a resize
     cannot drop a card off the edge of the table. The floors match TABLE_MIN
     in that file. */
  width: 150%;
  height: 150%;
  min-width: 1920px;
  min-height: 720px;
  min-height: max(720px, 150vh);
  --px: 0px;
  --py: 0px;
  transform: translate3d(var(--px), var(--py), 0);
  /* The grass, tiled, which is what you see if WebGL never arrives. felt.js
     paints the same maps with lighting over the top; both live on the surface
     so the nap moves with the cards when the table is dragged. */
  background-color: var(--felt);
  background-image: url(assets/sketchbook/felt/albedo.webp);
  background-size: 520px 520px;
  cursor: grab;
}
#felt.is-panning #surface { cursor: grabbing; }
/* The mat is the one place the native cursor is allowed: site.css hides it
   everywhere with !important, and these win on specificity. #played covers
   the whole surface, so it wears the hand too; cards inside it match only
   the site-wide rule and keep the dot. */
body.has-dot #surface,
body.has-dot #played { cursor: grab !important; }
body.has-dot #felt.is-panning #surface,
body.has-dot #felt.is-panning #played { cursor: grabbing !important; }

/* The WebGL mat. It fills the surface and sits under the chips, the dice and
   the cards. pointer-events are off: a pan belongs to #felt, a drag belongs
   to the card or the chip, and a canvas that ate either would be a hole in
   the table. */
#mat {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.45s ease;
}
#mat.is-lit { opacity: 1; }

/* The desk's printed name. It lives on #surface so it pans with the cloth.
   White ink at half strength, sitting on the grass — not a multiply stain
   and not an overlay wash (that just lightened the nap and hid the word).
   pointer-events are off: a pan through the name is still a pan. Hidden
   until sketchbook.js has sat it in the opening. */
#table-heading {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
  width: max-content;
  margin: 0;
  padding: 0;
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
  text-align: center;
  visibility: hidden;
  font-weight: 400;
  line-height: 1;
  color: transparent;
}
#table-heading.is-placed { visibility: visible; }
/* Once the name is in the grass shader it takes the lamp with the nap.
   The HTML heading stays for a11y and as the no-WebGL fallback. */
html.mat-ink #mat.is-lit ~ #table-heading { opacity: 0; }
#table-heading .mat-title,
#table-heading .mat-tag {
  /* Fallback print if WebGL never arrives: white at half strength. */
  background-image: none;
  -webkit-text-fill-color: rgba(255, 255, 255, 0.5);
  color: rgba(255, 255, 255, 0.5);
}
/* The drag hint under the name. Same white-on-grass ink as the fallback
   print, smaller and in the UI face, since it is an instruction rather than
   part of the name. Placed by sketchbook.js; hidden until then. */
#table-hint {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  width: max-content;
  margin: 0;
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
  visibility: hidden;
  font-family: "DM Sans", sans-serif;
  font-size: 14px;
  line-height: 1;
  color: rgba(255, 255, 255, 0.55);
}
#table-hint.is-placed { visibility: visible; }
#table-hint svg { flex: none; }

#table-heading .mat-title {
  display: block;
  font-family: "DM Sans", sans-serif;
  font-weight: 400;
  font-size: clamp(42px, 10.5vw, 104px);
  letter-spacing: -0.02em;
  line-height: 1.05;
}
#table-heading .mat-tag {
  display: block;
  margin-top: 0.18em;
  font-family: "Mynerve", cursive;
  font-weight: 400;
  font-size: clamp(18px, 3.5vw, 38px);
  letter-spacing: 0.01em;
  line-height: 1.2;
}

/* A chip on the table. The shadow is the same disc as the chip, a hair
   down and to the right — light from above-left — so the chip itself
   covers all of it but a sliver, and that sliver is a crescent on the
   far rim. Size is --chip-w, so the crescent scales with the chip; the
   offset is a fraction of that width, so it stays a thin rim at any
   size. It is not a cube, not a thick puck, and not a second full disc
   sitting beside the chip.

   Draggable, the same way a card on the table is: the press is the chip's,
   not the mat's, and left/top follow the pointer with no easing. The
   scale is the pickup — it changes twice in a gesture and wants easing,
   which is why it is on transform and the position is not. */
.chip {
  position: absolute;
  width: var(--chip-w, 52px);
  height: var(--chip-w, 52px);
  border-radius: 50%;
  overflow: visible;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  /* Same band as the dice: a card on the table, even one being dragged,
     always slides under a chip. A chip being dragged climbs above its
     neighbour in script, still in this band, so it cannot bury a card. */
  z-index: var(--die-z, 40000);
  transition: transform 0.18s cubic-bezier(0.2, 0.75, 0.3, 1);
}
.chip.is-dragging { transform: scale(1.08); }
.chip svg {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  transform: rotate(var(--chip-rot, 0deg));
}
.chip-cast {
  position: absolute;
  inset: 0;
  pointer-events: none;
  transform: translate3d(
      var(--cast-dx, calc(var(--chip-w, 52px) * 0.12)),
      var(--cast-dy, calc(var(--chip-w, 52px) * 0.16)),
      0
    );
  transition: transform 0.18s cubic-bezier(0.2, 0.75, 0.3, 1);
}
.chip.is-dragging .chip-cast {
  transform: translate3d(
      calc(var(--cast-dx, calc(var(--chip-w, 52px) * 0.12)) * 1.5),
      calc(var(--cast-dy, calc(var(--chip-w, 52px) * 0.16)) * 1.5),
      0
    );
}
.chip-cast-disc {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--cast);
  filter: blur(0.7px);
}

#played { position: absolute; inset: 0; }
/* The riffle has to pass over the chips and dice (--die-z is 40000).
   #played is not a stacking context the rest of the time, so idle cards
   still slide under a chip; only the gather climbs. */
#played.is-shuffling { z-index: 50000; }

/* The fan sits along the bottom edge, half off it: a hand you are holding up
   to the table rather than five cards lying on it. */
#fan {
  position: absolute;
  left: 50%;
  bottom: 0;
  /* Above the table, always: the five cards you are holding are never buried
     by one you have put down. */
  z-index: 2;
  /* Centred with a margin, not translateX(-50%). The felt-arrive animation
     writes transform, and that used to wipe the centering for 480ms and
     snap the hand back when the class came off. */
  margin-left: calc(var(--card-w) / -2);
  /* Its own height is the card's; the cards inside are absolutely placed
     against its centre, which is what makes the arc easy to state. */
  width: var(--card-w);
  height: calc(var(--card-w) / (5 / 7));
  /* Pushed down so only the top two thirds of the hand shows. */
  margin-bottom: calc(var(--card-w) / (5 / 7) * -0.34);
}

/* ---------- card → table transition ---------- */

/* Same overlay the case pages use. The flight is measured in
   card-transition.js; these rules only give it a place to sit and hold
   the real cloth back until the stand-in has become it. */
#card-transition {
  position: fixed;
  inset: 0;
  z-index: 9;
  pointer-events: none;
}
#card-transition > * {
  position: fixed;
  margin: 0;
  transform-origin: 0 0;
  will-change: transform, opacity;
}
.ct-media {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  overflow: hidden;
}

/* Arriving from the home card, the cloth must not be seen at full size before
   the flight has shrunk it into the card: that reads as the table appearing,
   jumping back into the card, and opening out a second time. Until
   card-transition.js starts the flight (felt-flying) the opening shimmers the
   way an image does while it loads, and the cloth waits behind it. The shimmer
   is skeleton.css's, so it matches every other loading picture on the site. */
:root.card-transition:not(.felt-flying) #felt { visibility: hidden; }
:root.card-transition:not(.felt-flying) #table {
  background-image: linear-gradient(100deg,
    var(--skel-base) 30%, var(--skel-sheen) 50%, var(--skel-base) 70%);
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: skel-shimmer 1.5s ease-in-out infinite;
}

/* The felt itself is the flight — grass, name and rim pieces stay on
   it. The hand, the bar, the rack and any cards already played wait. */
:root.card-transition #felt > #fan,
:root.card-transition #felt #played {
  opacity: 0;
}
:root.card-transition #topbar,
:root.card-transition #upcoming {
  opacity: 0;
  pointer-events: none;
}

/* After the cloth has opened: chrome comes in, not the table. */
:root.felt-arrive #played,
:root.felt-arrive #fan,
:root.felt-arrive #upcoming,
:root.felt-arrive #topbar {
  animation: felt-piece-in 480ms cubic-bezier(.22, .61, .36, 1) both;
}
:root.felt-arrive #topbar { animation-delay: 40ms; }
:root.felt-arrive #played { animation-delay: 70ms; }
:root.felt-arrive #fan { animation-delay: 130ms; }
:root.felt-arrive #upcoming { animation-delay: 180ms; }

@keyframes felt-piece-in {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: none; }
}
@keyframes felt-piece-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* ---------- a card ---------- */

.card {
  position: absolute;
  width: var(--card-w);
  aspect-ratio: var(--card-ar);
  /* Every measurement on the face below is taken off the card's own width, and
     this is what makes that possible: 1cqw is one per cent of this element's
     width, wherever the card happens to be and whatever size it happens to be
     at. It has to be here rather than on the face because an element cannot
     query itself, and a card is four different sizes on this page — 172 in the
     hand, 118 on the table, whatever its column gives it in the rack, and in
     the modal whatever shape its own work is.
     Nothing else changes: .card is already positioned and already carries a
     transform, so it was a containing block and a stacking context before the
     containment inline-size implies made it one. */
  container-type: inline-size;
  /* The border the work is inset by, ported from the about-me card's photo
     silhouette: 21px of a 1059px-wide design frame, which is 1.98% of the
     card's width wherever it is drawn.

     It scales rather than sitting at a fixed weight, because the index in the
     corner and the fold it sits in are the same geometry and those cannot be
     fixed — a border held at 3px on the modal's 320px card while the fold
     shrank with the card would stop being the about-me layout. The floor is
     there for one card only: at the table's 118px the ratio gives 2.3px, and on
     a phone's 84px card 1.7px, which straddles a device pixel and reads as a
     smudge along the edge rather than as a border. Two pixels is the least a
     stock edge can be and still be one. */
  --frame: max(2px, 1.98cqw);
  /* 8px on the 172px hand. cqw, not a length, so a card on the table
     and one in the modal keep the same corner as the one you are holding. */
  --card-r: 4.65cqw;
  /* Concentric with the card's own corner. A picture that kept the card's full
     radius inside a border would look rounder than the stock around it, and one
     given no radius at all would show four square corners inside a rounded
     card, which is the tell that the border is a rectangle laid on top rather
     than the paper the work is printed on. */
  --well-r: max(2px, calc(var(--card-r) - var(--frame)));
  /* Every card's own position and tilt are set as custom properties by
     sketchbook.js and composed here, so the script never writes a transform
     string and the two cannot disagree about the order of operations. */
  --x: 0px;
  --y: 0px;
  --rot: 0deg;
  --lift: 0px;
  /* Where a card arriving in the hand starts from. Separate from --lift, which
     is hover's to write, and from --y, which is the slot's: three things move a
     card vertically and none of them should have to know about the others. */
  --enter: 0px;
  /* Invert, after a shuffle: the opening five start on the pack and
     deal into the fan. Composed into translate the same way --enter is,
     so the existing transition on that property can carry them. */
  --from-x: 0px;
  --from-y: 0px;
  /* An extra few degrees while a card is being dragged, so picking one up is
     visible even before it has moved far. Composed into the same rotate as the
     card's own tilt rather than added as a second transform: two transforms on
     one element cannot both be written without one clobbering the other. */
  --tilt: 0deg;
  /* 1 until a hold or a drag picks the card up, then a hair under it so
     the thing in the hand is visibly smaller than the one on the table. */
  --pick: 1;
  left: 0;
  top: 0;
  /* Where the card is and which way it is leaning are put on two different
     properties, and that is the whole reason the lean can be animated. You
     cannot transition half of a transform, and these two halves want opposite
     things: the position is rewritten on every pointermove and must never be
     eased, because anything between the pointer and the card reads as the card
     lagging behind the hand holding it, while the tilt changes exactly twice in
     a gesture and wants easing badly.

     The individual translate/rotate/scale properties are applied BEFORE the
     transform shorthand, so `translate: x y` followed by `transform: rotate(a)`
     multiplies out to precisely the translate3d(...) rotate(...) this was
     written as, with the same transform-origin — the card is where it always
     was, and only the rotation is now transitionable on its own.

     The order is not a free choice and cannot be swapped round. Putting the
     rotation on the `rotate` property and the position in `transform` would
     rotate the offset vector before applying it, which does not lean the card:
     it carries it off across the table. */
  translate: calc(var(--x) + var(--from-x)) calc(var(--y) + var(--lift) + var(--enter) + var(--from-y)) 0px;
  transform: rotate(calc(var(--rot) + var(--tilt))) scale(var(--pick));
  /* The card is a 3D box so the flip has a back to turn to. */
  perspective: 1000px;
  border: 0;
  padding: 0;
  /* A grid tile is a <figure>, which the UA sheet gives 1em 40px of margin —
     enough to push every tile out of its column and the last one off the page. */
  margin: 0;
  background: none;
  /* A card mid-drag must not select the page text under it. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* An interactive card is a wrapper holding two controls: the card itself, which
   turns over, and the expand control in its corner. They are separate buttons
   because they do separate things — a button inside a button is not allowed,
   and putting the expand on the card's own click would make "turn it over" and
   "open it" the same gesture. .card-flip is the card's whole face and carries
   the pressed state; the wrapper only positions them. */
.card-flip {
  position: absolute;
  inset: 0;
  z-index: 1;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: inherit;
  /* The 3D box the flip turns inside. It has to be on the direct parent of
     .card-inner, which is this rather than .card now. */
  perspective: 1000px;
  cursor: inherit;
  -webkit-tap-highlight-color: transparent;
}
.card-flip:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

.card-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.card.is-flipped .card-inner { transform: rotateY(180deg); }

/* The turn is a lift, not a spin on the table. Scale and a little Z live on
   their own properties so they can peak in the middle of a rotation whose
   start and end are the same size — a transform transition cannot do that,
   and putting them on `transform` would fight the rotateY. The hold through
   the middle is the card in the air; the settle at either end is it landing. */
.card.is-flipping .card-inner {
  animation: card-lift 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
}
@keyframes card-lift {
  0%, 100% { scale: 1;    translate: 0 0 0; }
  32%, 58% { scale: 1.1;  translate: 0 0 28px; }
}
/* The link sits on the wrapper, not on the turning inner, so it would hang in
   the original corner while the card left it. Hidden while a card is turned:
   it belongs to the picture, and the back has none. */
.card.is-flipping > .card-link,
.card.is-flipping:hover > .card-link,
.card.is-flipped > .card-link,
.card.is-flipped:hover > .card-link,
.card.is-flipped > .card-link:focus,
.card.is-flipped > .card-link:focus-visible {
  opacity: 0;
  pointer-events: none;
}

.card-face {
  position: absolute;
  inset: 0;
  border-radius: var(--card-r);
  overflow: hidden;
  background: var(--stock);
  box-shadow: var(--card-shadow), inset 0 0 0 1px var(--stock-edge);
  /* The half turned away must not paint through the half facing you. */
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.card-face--back {
  transform: rotateY(180deg);
  background: var(--back-stock);
}

/* The back is drawn, not photographed. Cards/back.jpg was one fixed yellow
   whatever the page was set to; this is the same frame and mark built out of
   the theme's own tokens, so the blue back and the yellow-on-grey one are one
   piece of markup rather than two exports that have to be kept in step. The
   geometry is the deck's: a 25-wide accent border inset 35.5 in a 1059x1449
   frame, and a mark 437 across in the middle. The SVG scales that frame rather
   than a percentage inset, because a percentage inset draws unequal margins on
   a card that is taller than it is wide. */
.card-back-frame {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  color: var(--accent);
}
.card-back-mark {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 41.27%;             /* 437 / 1059 */
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background-color: var(--accent);
  -webkit-mask: url(assets/hfyj-mark.svg) center / contain no-repeat;
  mask: url(assets/hfyj-mark.svg) center / contain no-repeat;
}

/* ---------- the front of a card, which is a playing card ---------- */

/* Ported from the about-me card, which is drawn to a canvas in script.js in a
   1059x1449 design frame — the same frame the card back above already uses, so
   its numbers come across as they are, divided by 10.59 to read as a percentage
   of the card's width. The stock border came over, and so did the folded
   corner cut out of the picture — once at the top left, and again mirrored at
   the bottom right, which is how a playing card gives both indices a patch of
   paper to sit on. The shelf the about-me card's tag pills stand on did not.

   Everything here is measured in cqw and not in per cent, and that is the whole
   trick. A per cent is relative to whichever axis the property happens to use —
   the height for a `top`, the width for a `left` — and the card in the modal is
   now whatever shape its work is, anywhere from 0.40 to 1.78. One unit taken
   off the card's width instead puts the index the same distance in from the
   corner of a landscape gif and of a tall painting alike, and leaves the pip
   the shape it was drawn as rather than stretching it to the card. */

.card-face--front {
  /* Padding, so the work's own box is the content box and there is nothing to
     keep in step: the border is whatever is left over. Equal on all four sides
     for the same reason the back's frame is stretched rather than inset by a
     percentage — the stock is meant to show the same distance in from every
     edge, not a proportion of each one. */
  padding: var(--frame);
  /* The deck's paper. The front used to be --stock and it never showed, because
     the picture covered the card corner to corner; now that there is a border it
     is the surface you actually see, and the about-me card and the card back are
     both drawn on this one. */
  background: var(--back-stock);
}

.card-well {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: var(--well-r);
  /* Rounds the picture, and crops the name's scrim to the same corners. */
  overflow: hidden;
  /* The folded corners, off ABOUTME_PHOTO_MASK in script.js, in the design's own
     coordinates less the 21px inset the padding above already carries. The two
     curves in the original are sampled rather than replaced by their endpoints:
     the long one sweeps a third of the fold's height, and a straight line drawn
     across it cuts visibly into the very corner the index has to sit in.
     The fold is not decoration — it is what puts each index on paper instead of
     on the photograph. The bottom-right points are the top-left ones turned
     around the well's centre, so both notches stay one piece of geometry. */
  clip-path: polygon(
    19.83cqw 0,
    100% 0,
    100% calc(100% - 10.48cqw),
    calc(100% - 0.62cqw) calc(100% - 7.77cqw),
    calc(100% - 2.30cqw) calc(100% - 6.10cqw),
    calc(100% - 4.86cqw) calc(100% - 5.52cqw),
    calc(100% - 11.14cqw) calc(100% - 5.52cqw),
    calc(100% - 13.84cqw) calc(100% - 4.24cqw),
    calc(100% - 15.80cqw) calc(100% - 1.84cqw),
    calc(100% - 16.82cqw) calc(100% - 0.92cqw),
    calc(100% - 19.83cqw) 100%,
    0 100%,
    0 10.48cqw,
    0.62cqw 7.77cqw,
    2.30cqw 6.10cqw,
    4.86cqw 5.52cqw,
    11.14cqw 5.52cqw,
    13.84cqw 4.24cqw,
    15.80cqw 1.84cqw,
    16.82cqw 0.92cqw);
}

/* The index: the rank and then the house, in the fold at the top left and again
   upside down in the opposite corner, which is how a card tells you what it is
   whichever way up you are holding it.

   Sized off the card's width like the frame, and with no floor of its own. A pip
   held at a legible size while the fold around it went on shrinking would climb
   straight out of the fold and onto the picture, which is the one thing the fold
   exists to prevent; the index and the notch are one piece of geometry and
   either both scale or neither does. */
.card-pip {
  position: absolute;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 0.7cqw;
  font-family: "DM Sans", "Figtree", system-ui, sans-serif;
  font-size: 4.5cqw;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink);
  /* It sits over the card's own controls; nothing about it is clickable. */
  pointer-events: none;
}
/* Hearts and diamonds are red on a real card and the theme colour here. There
   is no red anywhere else on this site, and one appearing on a card would read
   as something having gone wrong rather than as a suit. */
.card-pip.is-accent { color: var(--accent); }
.pip-suit {
  display: block;
  flex: none;
  width: 0.82em;
  height: 0.82em;
}

.card-pip--tl { left: 4.15cqw; top: 1.9cqw; }

/* Turned the whole way round rather than built mirrored: on a real card the two
   indices are one mark printed twice, and the rotation is what lets you read the
   card upside down. It sits in the same fold the top-left one does, just the
   other way up. */
.card-pip--br {
  right: 4.15cqw;
  bottom: 1.9cqw;
  transform: rotate(180deg);
}

.card-face img {
  width: 100%;
  height: 100%;
  /* The works run from 426x240 to 963x2400; the card is one shape, so they
     crop into it rather than letterbox. */
  object-fit: cover;
}
/* Unless the work asks otherwise: see work.fit in sketchbook.js. */
.card-well.is-contain { background-color: var(--fit-bg, #000); }
.card-well.is-contain img { object-fit: contain; }

/* The opened YouTube work. The player fills the well; the poster sits
   behind it in case the iframe is slow or blocked, and the name is off —
   YouTube already writes a title across that edge and a second one would
   cover the controls. */
.card-embed {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: #000;
}
.card-embed iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
.card-well.is-embed .card-name { display: none; }

/* The name, on the work. It sits inside the well now rather than on the face,
   so it reaches the picture's edges and not the card's and the well's corners
   crop it — a scrim run out over the stock would draw a dark bar across the
   bottom of the border.

   The right-hand padding is what keeps it out of the bottom-right fold, which
   is the same notch the index sits in. It is the fold's own reach, in the
   fold's own unit, so the two cannot drift apart; the floor is the padding this
   side had before, for the card small enough that the notch is narrower than
   that. */
.card-name {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 22px max(10px, 21cqw) 9px 10px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.68), rgba(0, 0, 0, 0));
  color: #fff;
  text-align: left;
  pointer-events: none;
}
.card-name .cat {
  display: block;
  font-size: 8px;
  letter-spacing: 0.14em;
  /* The source's own casing is inconsistent — "installation" next to
     "Installation" — and an eyebrow in caps hides that without editing what
     the site actually says. The title below is left exactly as written. */
  text-transform: uppercase;
  opacity: 0.78;
  margin-bottom: 1px;
}
.card-name .name {
  display: block;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.2;
}

/* A work's external link, in the card's top-right corner, out of the way until
   you want it. It is in the document the whole time rather than mounted on
   hover, so it can be tabbed to — and it is hidden with opacity alone for
   exactly that reason: visibility: hidden and display: none both take an
   element out of the tab order. pointer-events does the part visibility was
   there for. Moved in by the width of the border, so it lands on the picture
   rather than straddling the stock edge. In the modal it stays on. */
.card-link {
  position: absolute;
  top: calc(var(--frame) + 3px);
  right: calc(var(--frame) + 3px);
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--corner-btn);
  height: var(--corner-btn);
  border-radius: 8px;
  background: rgba(20, 20, 20, 0.55);
  color: #fff;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease, background-color 0.18s ease;
}
.card-link svg { width: calc(var(--corner-btn) * 0.5); height: calc(var(--corner-btn) * 0.5); display: block; }
.card-link:hover { background: rgba(20, 20, 20, 0.78); }
.card:hover > .card-link,
.card-link:focus,
.card-link:focus-visible {
  opacity: 1;
  pointer-events: auto;
}
.card-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
#modal-card .card-link {
  opacity: 1;
  pointer-events: auto;
}

/* ---------- cards in the fan ---------- */

/* Only the top card of the fan responds; the rest are behind it. The lift is a
   separate custom property from --y so hovering never disturbs the arc. Two
   transitions where there was one, for the same reason .card now has two
   properties: a hand closing up slides its cards and turns them at the same
   time, and those are `translate` and `transform` now. */
#fan .card { transition: translate 0.22s ease, transform 0.22s ease, opacity 0.22s ease; }
/* The opening deal writes every slot at once. Without this the cards start
   stacked on --x: 0 and ease out, which reads as the hand landing off-centre
   and snapping into place. */
#fan.is-placing .card { transition: none; }
/* A card played leaves the hand and the rest close up; the new one comes in at
   the end, rising into the gap rather than appearing in it. */
#fan .card.is-entering { --enter: 54px; opacity: 0; }
/* Same invert as the rack tiles: from the pack, at the table's size, into
   the fan. Longer than the hand's ordinary close-up so the throw reads. */
#fan .card.is-dealing-in {
  transition: translate 0.52s cubic-bezier(0.2, 0.75, 0.3, 1),
              transform 0.52s cubic-bezier(0.2, 0.75, 0.3, 1);
}
/* :focus-within, not :focus-visible — the thing that takes focus is the button
   inside the card, and the card is what lifts. */
#fan .card:hover, #fan .card:focus-within { --lift: -16px; }

/* ---------- cards on the table ---------- */

#played .card {
  width: var(--card-w-played);
  box-shadow: none;
  /* A long-press on a phone is how you pick a card up. The callout is the
     browser offering to save the picture instead, and it steals the hold. */
  -webkit-touch-callout: none;
}
#played .card.is-dragging { --pick: 0.92; }
#played .card.is-dragging .card-face { box-shadow: var(--card-shadow-lift), inset 0 0 0 1px var(--stock-edge); }

/* The flight out of the fan. sketchbook.js puts the card at its landing spot
   first and offsets it back to where it was in the hand, then takes the offset
   away — so this transition runs on real coordinates and the card is already
   where it belongs if it never gets to animate. */
#played .card.is-dealing {
  /* Width too: the card leaves the hand at the hand's size and arrives at the
     table's, so the shrink is part of the throw rather than a snap at the
     moment it is let go of. */
  transition: translate 0.52s cubic-bezier(0.2, 0.75, 0.3, 1),
              transform 0.52s cubic-bezier(0.2, 0.75, 0.3, 1),
              width 0.52s cubic-bezier(0.2, 0.75, 0.3, 1);
}

/* A card being dragged leans four degrees the way it is being pulled, and tips
   into it rather than snapping. */
#played .card {
  /* The lean, tipping in and settling back out. Only `transform` is named here,
     which on a card is now only its rotation — the position is on `translate`
     and is deliberately left with no transition at all, because it is following
     a finger. That split is what makes this line safe: a card being dragged has
     its position rewritten every pointermove and its tilt written twice in the
     whole gesture, on the frame the drag begins and on the frame it is dropped,
     so easing everything the card does would have eased the wrong one. Kept
     short and front-loaded: the tilt is a reply to a gesture that has already
     started, and a slow one would arrive after the card had moved. */
  transition: transform 0.2s cubic-bezier(0.2, 0.75, 0.3, 1),
              scale 0.18s cubic-bezier(0.2, 0.75, 0.3, 1);
}

/* The end of a round: every card comes together, the pack riffles three
   times, and the whole lot is dealt back into the rack. Like the flight
   above, both of these move a card and turn it at the same time, so both
   name the two properties that now share that work. */
#played .card.is-gathering {
  transition: translate 0.5s cubic-bezier(0.3, 0.8, 0.3, 1),
              transform 0.5s cubic-bezier(0.3, 0.8, 0.3, 1);
}
#played .card.is-riffling {
  transition: translate 0.22s ease, transform 0.22s ease;
}

/* Every card in the pack drops its shadow while the round is being reshuffled.
   Stacked on one spot, each card's shadow falls on the card below rather than
   on the table, and twenty-one of them add up to a smudge. The hairline edge
   stays: it is what keeps the pile reading as paper. */
#played.is-shuffling .card-face { box-shadow: inset 0 0 0 1px var(--stock-edge); }

/* ---------- the catalogue ---------- */

#upcoming { padding-top: 24px; }

#upcoming h2 {
  font-family: "DM Sans", "Figtree", system-ui, sans-serif;
  font-size: 22px;
  font-weight: 500;
  color: var(--head);
  margin: 0 0 20px;
}
#upcoming h2 .count { color: var(--num); font-variant-numeric: tabular-nums; }

#grid {
  position: relative;        /* a card on its way out is pinned over this */
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 18px;
}

/* A fixed rack. One slot per work the sketchbook started with, made once and
   never taken away: cards advance through them as they are played, so the block
   keeps its footprint and the empties collect at the end instead of the whole
   grid reflowing shorter every time a card goes down. */
.slot {
  position: relative;
  aspect-ratio: var(--card-ar);
  /* Same 4.65% of width as .card. Percentages on border-radius are
     per-axis, so the y value is scaled by the 5:7 so the corner stays
     a circle — a slot cannot query its own width. */
  border-radius: 4.65% / calc(4.65% * 5 / 7);
}
/* An empty slot is a place a card is missing from, so it is drawn as one: a
   faint fill as well as the hairline, or on a white page it reads as nothing
   at all rather than as a gap in the rack. */
.slot:empty {
  box-shadow: inset 0 0 0 1px var(--rule);
  background: rgba(0, 0, 0, 0.028);
}
:root[data-theme="dark"] .slot:empty { background: rgba(255, 255, 255, 0.035); }

/* The mark, watermarked into the gap, so the empty slot says whose card is
   missing rather than just that one is. Painted as a mask over a colour for
   the same reason .card-back-mark is: one file that takes whatever grey the
   theme asks for, instead of a second export for dark.

   Smaller than the deck's own 41.27% and much fainter than the accent it is
   drawn in there. A real card face is sitting in the next slot along, and this
   has to read as the absence of one — a placeholder — not as a second thing
   competing for the eye. */
.slot:empty::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 26%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  /* A step further from the page than the slot's own fill rather than a step
     back toward it: darker in light, lighter in dark. Either way it stays
     under --rule, so the hairline still reads as the edge of the slot and the
     mark as something inside it. */
  background-color: rgba(0, 0, 0, 0.07);
  -webkit-mask: url(assets/hfyj-mark.svg) center / contain no-repeat;
  mask: url(assets/hfyj-mark.svg) center / contain no-repeat;
}
:root[data-theme="dark"] .slot:empty::before { background-color: rgba(255, 255, 255, 0.09); }

/* A grid tile is the same card face at a different size, so it inherits the
   shape and the stock and only overrides what a tile does differently: it fills
   its column instead of being a fixed width, and it does not move. */
#grid .card {
  position: absolute;
  inset: 0;
  width: auto;
  height: auto;
  aspect-ratio: auto;        /* the slot holds the shape now */
  transform: none;
}

/* FLIP. The script puts a card back where it was with an inline transform, then
   clears it with this on, so the advance is animated without anything having to
   know the grid's geometry. */
#grid .card.is-moving { transition: transform 0.42s cubic-bezier(0.2, 0.75, 0.3, 1); }

/* A card that has been played shrinks away where it stood, pinned over the grid
   so its slot is already free for the card advancing into it. */
#grid .card.is-leaving {
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.9, 0.4), opacity 0.3s ease;
  transform: scale(0.14);
  opacity: 0;
  pointer-events: none;
  /* Both halves of .card's placement, cleared. A tile is placed by its slot and
     the script drives its FLIP with an inline transform, so the custom
     properties above have nothing to say here — and leaving `translate` at a
     resolved zero rather than at none would make every tile a containing block
     and a stacking context it was not before. */
  translate: none;
}

/* A rack card can be picked up and dropped into another slot: what is upcoming
   is a running order, not a fixed list. touch-action, because a finger holding
   a card must not also be scrolling the page out from under it — and the
   gesture is pointer events rather than the browser's own drag-and-drop
   precisely so that a finger and a mouse are the same code. */
/* Nor any other cursor. The dot is what is over a card — a pointing hand under
   it was two cursors saying the same thing, and on a card being dragged it was
   the wrong one. Scoped to body.has-dot so a coarse pointer, which has no dot,
   is left with the browser's own. */
body.has-dot .card,
body.has-dot #grid .card,
body.has-dot .card-flip,
body.has-dot .card-link,
body.has-dot .chip { cursor: none; }

/* No grab hand on a card, here or on the table. The hand belongs to the mat,
   which is the thing you pan; over a card the dot is the cursor, picked up or
   not, so the two gestures stay told apart by what is under the pointer rather
   than by two hands that look alike. */
#grid .slot > .card {
  /* pan-y until a hold has claimed the finger — touch-action: none here is
     how a scroll that started on a tile used to stall the whole rack. */
  touch-action: pan-y;
  -webkit-touch-callout: none;
}
#grid .slot > .card:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

/* Out of its slot and pinned over #grid, riding above the tiles it is being
   carried across — z-index and not document order, because the slots after it
   are positioned too and would otherwise paint over it. No transition on the
   transform: it is following a finger, and anything between the two reads as
   the card lagging behind the hand holding it. */
#grid .card.is-lifted {
  touch-action: none;
  z-index: 5;
  /* Position still has no transition — it is following a finger. The
     shrink is the hold landing, and that one is allowed to ease. */
  transition: scale 0.18s cubic-bezier(0.2, 0.75, 0.3, 1);
  scale: 0.92;
}
#grid .card.is-lifted .card-face {
  box-shadow: var(--card-shadow-lift), inset 0 0 0 1px var(--stock-edge);
}

#grid .card-face { box-shadow: inset 0 0 0 1px var(--stock-edge); }
#grid .card-face img { transition: transform 0.4s ease; }
#grid .card:hover .card-face img { transform: scale(1.04); }

/* The name is hidden until hover, the way the live sketchbook does it — but it
   is in the document the whole time rather than mounted on hover, so a screen
   reader and a keyboard get the name too. */
#grid .card-name {
  opacity: 0;
  transition: opacity 0.22s ease;
}
#grid .card:hover .card-name,
#grid .card:focus-within .card-name { opacity: 1; }

#grid-empty { color: var(--caption); font-size: 15px; }
/* Same reason as #card-modal below: an author display rule outranks the
   UA's [hidden] { display: none }, and this line has to stay off the page
   until the catalogue has actually failed. */
#grid-empty[hidden] { display: none !important; }

/* ---------- the card, opened ---------- */

/* A modal, and the page behind it is genuinely behind it: blurred, unscrollable
   and unreachable by the pointer. #card-modal is the overlay; .modal-panel is
   the dialog proper, which is what carries the role and the name. */
#card-modal {
  position: fixed;
  inset: 0;
  /* Above the sticky top bar (10) and below the cursor dot (100), which should
     keep following the pointer over the modal like it does everywhere else. */
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--modal-pad);
}
/* The UA's [hidden] rule is display:none, which the flex above outranks; this
   puts it back. */
#card-modal[hidden] { display: none; }

/* The page behind a modal does not scroll. Set on <body>, which propagates to
   the viewport, so the scrollbar goes with it and the scrim measures exactly
   the window. */
body.modal-open { overflow: hidden; }

.modal-scrim {
  position: fixed;
  inset: 0;
  background: rgba(20, 20, 22, 0.42);
  animation: modal-fade 0.28s ease both;
}
:root[data-theme="dark"] .modal-scrim { background: rgba(0, 0, 0, 0.6); }
/* Only while it is open. A backdrop-filter that is merely display:none still
   reports itself in the computed style, and "is the page blurred right now" is
   a question about the open state, not about the rule existing. */
#card-modal:not([hidden]) .modal-scrim {
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

.modal-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-height: 100%;
}

#modal-card { display: flex; }

/* The card in the modal sits in the flow rather than being placed on a table,
   and it arrives by rising into place. The rise is an animation and not a
   transition because it plays once, on a card that was created a frame ago —
   there is no previous value for a transition to run from.

   It is also the one card cut to the shape of its own work rather than to 5:7:
   sketchbook.js writes the work's width and height onto this element as
   --card-ar, which is the same property every card's aspect-ratio already reads,
   so nothing here has to learn a second way of being a shape and no card outside
   this one is touched. A work whose manifest entry is missing either number
   never gets that override and stays 5:7.

   The two constraints below are what make an arbitrary shape safe. The card is
   *contained* in the box, so whichever of the two binds first wins: a wide work
   is stopped by the box's width, a tall one by its height, and on a short window
   the height is stopped by the window rather than by the box. Fitting to width
   alone is the version of this that looks right on a desk and pushes the card
   through the floor on a laptop turned sideways. */
/* A YouTube work needs a bigger box than the stills. 320px is a playing
   card; a player that size is a postage stamp. The square is still a square
   — the 16:9 ratio is written on the card as --card-ar — so this only
   raises the ceiling the fit reads. */
#modal-card > .card.is-embed {
  --card-box: min(960px, calc(100vw - var(--modal-pad) * 2),
                  calc((100svh - var(--modal-pad) * 2) * 16 / 9));
}

#modal-card > .card {
  position: relative;
  --fit-w: min(var(--card-box), calc(100vw - var(--modal-pad) * 2));
  --fit-h: min(var(--card-box), calc(100vh - var(--modal-pad) * 2));
  /* And again in the small viewport height wherever that is understood: 100vh
     on a phone is the window with the browser's chrome retracted, which is more
     than you can see, so a card fitted to it hangs under the toolbar. */
  --fit-h: min(var(--card-box), calc(100svh - var(--modal-pad) * 2));
  /* Contain, written out: the widest the card may be, or the width its own
     shape gives it at the tallest it may be, whichever is smaller. --card-ar is
     a `w / h` pair, so multiplying a length by it is a division as well and the
     unit survives. */
  --modal-card-w: min(var(--fit-w), calc(var(--fit-h) * var(--card-ar)));
  /* The height that works out to, kept as a property because the detail side
     printed on the back has to know how short a card it is on. */
  --modal-card-h: calc(var(--modal-card-w) / (var(--card-ar)));
  width: var(--modal-card-w);
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  perspective: 1100px;
  animation: card-rise 0.62s cubic-bezier(0.2, 0.75, 0.28, 1) both;
}
/* Lean lives on the inner so the rise (on the wrapper) can finish first.
   A few degrees only — enough to catch the light, not enough to read as a
   turn. */
#modal-card > .card:not(.is-flipped) .card-inner {
  transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transition: transform 140ms ease-out;
}
/* Expanded: the stock frame only. The fold and the indices stay on the
   table cards; in here they just ate the picture. */
#modal-card .card-pip { display: none; }
#modal-card .card-well { clip-path: none; }
#modal-card .card-name { padding: 22px 10px 9px; }
#modal-card > .card .card-face { box-shadow: var(--card-shadow-lift), inset 0 0 0 1px var(--stock-edge); }

@keyframes card-rise {
  from { transform: translate3d(0, 88px, 0); opacity: 0; }
  to   { transform: translate3d(0, 0, 0);    opacity: 1; }
}
@keyframes modal-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Closing is the rise played back: the card sinks the way it came and the
   scrim clears with it. sketchbook.js puts .is-closing on the overlay, waits
   for card-sink to end, and only then hides the modal and throws the card
   away. The curve is the rise's mirrored (slow start, fast finish) and a
   little quicker, because leaving should not keep you waiting. */
#card-modal.is-closing #modal-card > .card {
  animation: card-sink 0.42s cubic-bezier(0.72, 0, 0.8, 0.25) both;
}
#card-modal.is-closing .modal-scrim {
  animation: modal-fade-out 0.42s ease both;
}
@keyframes card-sink {
  from { transform: translate3d(0, 0, 0);     opacity: 1; }
  to   { transform: translate3d(0, 120px, 0); opacity: 0; }
}
@keyframes modal-fade-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* The information side. It replaces the card back on this one card: turning the
   card over in the modal is how you read about the work. */
#modal-card .card-face--back {
  display: flex;
  background: var(--stock);
}
.card-detail {
  display: flex;
  flex-direction: column;
  width: 100%;
  /* The margin gives way before the words do. This side is printed on the back
     of a card that is now whatever shape its work is, and on a 16:9 one that is
     197px tall on a phone — where a fixed 20px top and bottom is a fifth of the
     whole side, and the note ran two pixels past the bottom edge.

     A fifteenth of the card's height and a twelfth of its width are what these
     two values already come to on the 5:7 card, near enough, so on that card the
     cap is what binds and its padding is exactly what it always was; the
     fraction only takes over on a card smaller than it in that direction. */
  --pad-y: 26px;
  --pad-x: 22px;
  padding: min(var(--pad-y), calc(var(--modal-card-h) / 15))
           min(var(--pad-x), calc(var(--modal-card-w) / 12));
  text-align: left;
  color: var(--ink);
  overflow: auto;
}
.card-detail .detail-cat {
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 8px;
}
.card-detail .detail-title {
  font-family: "DM Sans", "Figtree", system-ui, sans-serif;
  font-size: 21px;
  font-weight: 500;
  line-height: 1.2;
  margin: 0 0 16px;
  color: var(--head);
}
.card-detail .detail-copy {
  margin: 0 0 16px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
}
.card-detail dd a {
  color: var(--accent);
  text-decoration: none;
}
.card-detail dd a:hover { text-decoration: underline; }
.card-detail dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 7px 14px;
  margin: 0;
  padding-top: 14px;
  border-top: 1px solid var(--rule);
  font-size: 12px;
}
.card-detail dt { color: var(--caption); }
.card-detail dd { margin: 0; color: var(--ink); font-variant-numeric: tabular-nums; }
/* Pushed to the foot of the card: the fields are a short list and the card is
   5:7, so without this the whole side sits in its top third. */
.card-detail .detail-note {
  margin: auto 0 0;
  padding-top: 16px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--caption);
}

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

@media (max-width: 900px) {
  #grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 700px) {
  /* The gutter narrows; the table still runs the full width, so the padding
     stays off .sketchbook itself. */
  .sketchbook { --gutter: 16px; padding: 0 0 64px; }
  :root {
    --card-w: 124px;
    --card-w-played: 84px;
    --corner-btn: 24px;
    /* The modal's own padding, which used to be set on #card-modal here. It is
       a token because the card's fit has to subtract it from the window, and
       two places saying how much room the modal keeps is two places to drift. */
    --modal-pad: 16px;
  }
  /* Seventy percent of the window, not a floor that ate most of a short
     phone. dvh is the visible window once the browser chrome is counted;
     vh is the fallback for a browser that has not heard of it — and it has to
     be said, not merely described. On a phone vh is the window with the URL
     bar hidden, whether or not it is, so on vh alone the opening is taller
     than the view and its middle — where the name is printed — sits below the
     middle of what the visitor can see. */
  #felt {
    --felt-base: 70vh;
    --felt-base: 70dvh;
    --felt-inset: 10px;
    --felt-radius: 16px;
  }
  #table-heading .mat-title { font-size: clamp(36px, 12vw, 72px); }
  #table-heading .mat-tag { font-size: clamp(16px, 4.4vw, 28px); }
  #table-hint { font-size: 13px; gap: 6px; }
  /* The caps, not the padding: the shorthand above is a min() against the card's
     own size, and restating it here as two lengths would throw that away on
     exactly the narrow window where it does the most work. */
  .card-detail { --pad-y: 20px; --pad-x: 18px; }
  .card-detail .detail-title { font-size: 18px; }
  #grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  #upcoming h2 { font-size: 19px; }
  /* Hover is not available here, so the name that would be revealed by it has
     to be on already or it can never be read. */
  #grid .card-name { opacity: 1; }
}

/* The deal, the flip and the tile's zoom are all decoration over a page that
   works without them; the drag is not, and is untouched — the card still goes
   exactly where the pointer takes it, at the moment the pointer takes it there.
   Only the lean it tips into is shortened, which leaves the signal that a card
   has been picked up and takes away the movement in it. */
@media (prefers-reduced-motion: reduce) {
  .card-inner,
  #fan .card,
  #fan .card.is-dealing-in,
  #played .card,
  #grid .card.is-moving,
  #grid .card.is-leaving,
  #played .card.is-gathering,
  #played .card.is-riffling,
  #played .card.is-dealing,
  #grid .card-face img,
  .card-link,
  #grid .card-name { transition-duration: 0.001s; }
  .card.is-flipping .card-inner { animation-duration: 0.001s; }
  /* The rise and the scrim's fade are decoration over a modal that is just as
     usable arriving all at once. */
  #modal-card > .card,
  .modal-scrim,
  #card-modal.is-closing #modal-card > .card,
  #card-modal.is-closing .modal-scrim { animation-duration: 0.001s; }
  #modal-card > .card .card-inner { transition-duration: 0.001s; }
  #mat { transition-duration: 0.001s; }
  #felt,
  html.felt-snapping #felt { transition-duration: 0.001s; }
}

@supports (height: 1dvh) {
  @media (max-width: 700px) {
    #felt { --felt-base: 70dvh; }
  }
}

/* Playground cards: just the work (Jen, Oct 28). The rank + suit indices in
   the corners are gone, and so are the folded notches that only existed to
   give them paper to sit on: the picture fills the whole rounded well. The
   indices are still built in sketchbook.js, only hidden; delete this block to
   bring both back. */
.card-pip { display: none; }
.card-well { clip-path: none; }
