/* The Book Garden — quaint & clean */

:root {
  --paper:      #fbf8f1;
  --paper-warm: #f3ecdd;
  --ink:        #26332b;
  --ink-soft:   #56645a;
  --leaf:       #3f6149;      /* ivy in the logo border   */
  --leaf-deep:  #223528;      /* the dark green sign      */
  --berry:      #a8382c;      /* dragon / illuminated "B" */
  --gold:       #d8bd77;      /* the lettering            */
  --line:       #dcd2bf;

  --serif: "Fraunces", Georgia, "Times New Roman", serif;
  --sans:  "Karla", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, sans-serif;

  --measure: 62ch;
  --wrap: 62rem;
}

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background-color: var(--paper);
  background-image:
    radial-gradient(circle at 1px 1px, rgba(74,107,82,.055) 1px, transparent 0);
  background-size: 22px 22px;
  color: var(--ink);
  font-family: var(--sans);
  font-size: 1.0625rem;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--leaf-deep); text-underline-offset: 3px; text-decoration-thickness: 1px; }
a:hover { color: var(--berry); }

h1, h2, h3 {
  font-family: var(--serif);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.01em;
  margin: 0;
}

/* ---------- header ---------- */

.site-header {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 1.75rem 1.5rem 0;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  align-items: baseline;
  justify-content: space-between;
}

.wordmark {
  display: inline-flex;
  align-items: baseline;
  gap: .5rem;
  font-family: var(--serif);
  font-size: 1.4rem;
  color: var(--ink);
  text-decoration: none;
}
.wordmark-leaf { color: var(--berry); font-size: 1.05rem; }
.wordmark:hover { color: var(--leaf-deep); }

.site-nav {
  display: flex;
  gap: 1.75rem;
  font-size: .95rem;
  letter-spacing: .02em;
}
.site-nav a {
  color: var(--ink-soft);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
}
.site-nav a:hover { color: var(--leaf-deep); border-bottom-color: var(--leaf); }

/* ---------- shared layout ---------- */

main { max-width: var(--wrap); margin: 0 auto; padding: 0 1.5rem; }

section { padding: 4.5rem 0; }

section h2 {
  font-size: clamp(1.6rem, 3.5vw, 2.1rem);
  text-align: center;
  margin-bottom: 2.5rem;
}

.lede {
  max-width: var(--measure);
  color: var(--ink-soft);
  font-size: 1.125rem;
}
.lede-center { margin-inline: auto; text-align: center; margin-top: -1.5rem; margin-bottom: 2.5rem; }

/* ---------- hero ---------- */

.hero { padding: 3rem 0 4rem; text-align: center; }

.hero-logo {
  margin: 0 auto 1.75rem;
  max-width: 40rem;
}
.hero-logo img {
  display: block;
  width: 100%;
  height: auto;
  /* the artwork ships on a white field — multiply drops it onto the paper */
  mix-blend-mode: multiply;
}

.eyebrow {
  margin: 0 0 1.75rem;
  font-size: .8rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--leaf);
}

.hero h1 {
  font-size: clamp(2.5rem, 7vw, 4rem);
  margin-bottom: 1.5rem;
}

/* A shade wider than the 62ch body measure: at 62ch the closing "2002." was
   left alone on a third line, with only 28px of slack on the line above and
   the year needing about 50. The extra few characters pull the whole phrase
   up. The nbsp in the markup keeps "since 2002." together at any width, so a
   narrower viewport orphans neither the year nor the word before it. */
.hero .lede { margin-inline: auto; max-width: 67ch; }

.hero-actions {
  margin-top: 2.25rem;
  display: flex;
  gap: .75rem;
  justify-content: center;
  flex-wrap: wrap;
}

.button {
  display: inline-block;
  padding: .7rem 1.6rem;
  border-radius: 999px;
  background: var(--leaf-deep);
  border: 1px solid var(--leaf-deep);
  box-shadow: inset 0 0 0 1px rgba(216,189,119,.35);
  color: var(--gold);
  font-size: .95rem;
  font-weight: 500;
  letter-spacing: .02em;
  text-decoration: none;
  transition: background-color .18s ease, color .18s ease, border-color .18s ease;
}
.button:hover { background: var(--berry); border-color: var(--berry); color: #fdf6e6; }

.button-quiet {
  background: transparent;
  box-shadow: none;
  color: var(--leaf-deep);
  border-color: var(--line);
}
.button-quiet:hover { background: transparent; color: var(--berry); border-color: var(--berry); }

/* ---------- divider ---------- */

.rule {
  display: flex;
  align-items: center;
  gap: 1rem;
  color: var(--leaf);
  opacity: .55;
}
.rule::before, .rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}
.rule span { font-size: .8rem; }
.rule::before, .rule::after { background: linear-gradient(90deg, transparent, var(--line), transparent); }

/* ---------- shop ---------- */

.cards {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  /* Grid sizes each row independently, so the row holding Pikachu's tile came
     out taller than the other. 1fr makes every row match the tallest. */
  grid-auto-rows: 1fr;
}

.card {
  background: var(--paper-warm);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 1.75rem 1.6rem;
  /* A floor, not a fixed height: the copy in these tiles varies a lot, and
     most of them host artwork that needs the vertical room regardless.
     13rem for a long time, which left the prince's 134px sitting under the
     last two lines of the Books copy -- at 21rem his top clears where the
     copy ends. grid-auto-rows: 1fr carries the extra height to every tile in
     the row, so the grid stays even. */
  min-height: 21rem;
}
.card h3 {
  font-size: 1.25rem;
  margin-bottom: .6rem;
  color: var(--leaf-deep);
}
.card p { margin: 0; color: var(--ink-soft); font-size: 1rem; }

.note {
  max-width: var(--measure);
  margin: 2.5rem auto 0;
  text-align: center;
  font-size: .975rem;
  color: var(--ink-soft);
  font-style: italic;
}

/* ---------- events ---------- */

.events-placeholder {
  max-width: 34rem;
  margin: 0 auto;
  padding: 2.5rem 1.5rem;
  text-align: center;
  border: 1px dashed var(--line);
  border-radius: 6px;
  background: var(--paper-warm);
}
.events-placeholder p { margin: 0 0 1.25rem; color: var(--ink-soft); }
.events-placeholder p:last-child { margin-bottom: 0; }

.fb-page { display: flex; justify-content: center; }

/* ---------- visit ---------- */

.visit-grid {
  display: grid;
  gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  text-align: center;
}

.visit-block h3 {
  font-size: .8rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  font-family: var(--sans);
  font-weight: 600;
  color: var(--leaf);
  margin-bottom: .9rem;
}
.visit-block p { margin: 0 0 .5rem; }

.hours { margin: 0; }
.hours > div {
  display: flex;
  justify-content: center;
  gap: .5rem;
}
.hours dt { color: var(--ink-soft); }
.hours dt::after { content: ":"; }
.hours dd { margin: 0; }

/* ---------- footer ---------- */

.site-footer {
  margin-top: 2rem;
  border-top: 1px solid var(--line);
  padding: 2.5rem 1.5rem 3.5rem;
  text-align: center;
}
.footer-mark {
  font-family: var(--serif);
  font-size: 1.1rem;
  margin: 0 0 .4rem;
}
.footer-mark span { color: var(--leaf); }
.footer-fine {
  margin: 0;
  font-size: .875rem;
  color: var(--ink-soft);
}

/* ---------- small screens ---------- */

@media (max-width: 34rem) {
  .site-header { flex-direction: column; align-items: flex-start; gap: .75rem; }
  .site-nav { gap: 1.25rem; }
  section { padding: 3.25rem 0; }
  .hero { padding-top: 1.5rem; }
  .hero-logo { margin-bottom: 1.25rem; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition: none !important; }
}

/* ---------- interior pages ---------- */

.page section { padding: 3.5rem 0; }

.page-head { padding-top: 3.5rem; text-align: center; }
.page-head h1 {
  font-size: clamp(2.1rem, 5.5vw, 3rem);
  margin-bottom: 1.25rem;
}
.page-head .lede-center { margin-top: 0; margin-bottom: 0; }

.portrait { margin: 1rem 0 0; }
.portrait img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  border: 1px solid var(--line);
}
.portrait figcaption {
  margin-top: .7rem;
  text-align: center;
  font-size: .875rem;
  color: var(--ink-soft);
  font-style: italic;
}

.prose { max-width: var(--measure); margin-inline: auto; }
.prose h2 {
  text-align: left;
  font-size: 1.5rem;
  margin-bottom: 1.25rem;
}
.prose p { margin: 0 0 1.2rem; }
.prose p:last-child { margin-bottom: 0; }

.prose blockquote {
  margin: 1.75rem 0;
  padding: .25rem 0 .25rem 1.5rem;
  border-left: 2px solid var(--leaf);
}
.prose blockquote p {
  font-family: var(--serif);
  font-size: 1.2rem;
  line-height: 1.5;
  color: var(--leaf-deep);
  margin-bottom: .6rem;
}
.prose blockquote cite {
  font-style: normal;
  font-size: .8rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.page-cta { text-align: center; }
.page-cta .lede-center { margin-bottom: 1.75rem; }

.site-nav a[aria-current="page"] {
  color: var(--leaf-deep);
  border-bottom-color: var(--leaf);
}

/* ---------- facebook events embed ---------- */

/*
  Two third-party embeds, framed the same way: the Facebook calendar and the
  Starwall reviews. Both drop an iframe of their own making into the page, so
  all we do is bound the width, centre it, and hold a min-height -- without
  that the page jumps as each one loads.
*/
.fb-events,
.reviews-embed {
  max-width: 46rem;
  margin: 0 auto;
  min-height: 12rem;
}

.fb-events iframe,
.reviews-embed iframe { display: block; max-width: 100%; border: 0; }

/* The widget styles itself; this is only so anything it renders inherits the
   page's type rather than the browser default. */
.reviews-embed { font-family: var(--sans); color: var(--ink); }

/*
  Every other section runs its h2 into a lede, and .lede-center pulls itself
  up 1.5rem to close the gap. This one lost its subtitle, so the heading was
  left holding the full 2.5rem against the widget with nothing in between --
  which read as a hole rather than as spacing. 1rem here is the same distance
  the other headings actually end up with once their lede has pulled up.
*/
.reviews h2 { margin-bottom: 1rem; }

/*
  The SendFox signup is the odd one out: it injects a plain div rather than an
  iframe, so it inherits page styles and needs narrower framing -- a signup
  form is one field and a button, not a feed.
*/
.signup-embed {
  max-width: 32rem;
  margin: 0 auto;
  min-height: 7rem;
  font-family: var(--sans);
  color: var(--ink);
}

/* The script wraps the form in its own div carrying an inline max-width, so
   that wrapper is what needs centring. */
.signup-embed .sendfox-embed { margin: 0 auto; }
.signup-embed form { display: block; max-width: 100%; margin: 0 auto; }

/* ---------- lotus in the Magic tile ---------- */

/*
  lotus.png is assets/source/lotus.jpg with its grey backdrop keyed out by
  tools/make-lotus.py.
  Sits in the bottom-right corner, behind the copy like the spider does, so
  the tile's text stays first in the reading order.
*/
.card-lotus {
  position: relative;
  /* The glow is wider than the flower and hangs off the corner; without this
     it spills past the tile onto the page. Clips to the 6px radius too. */
  overflow: hidden;
}

.card-lotus .lotus {
  --lotus-w: 105px;
  --lotus-opacity: 0.35;

  position: absolute;
  right: 0.5rem;
  bottom: 0.5rem;
  width: var(--lotus-w);
  height: auto;
  opacity: var(--lotus-opacity);
  z-index: 0;
  pointer-events: none;
}

.card-lotus h3,
.card-lotus p {
  position: relative;
  z-index: 1;
}


/* ---------- the little prince in the Books tile ---------- */

/*
  prince.png is assets/source/littleprince.png with its purple backdrop keyed out to real
  transparency by make-prince.py, so it needs no blend mode.
*/
.card-prince {
  position: relative;
}

.card-prince .prince {
  --prince-w: 108px;
  --prince-opacity: 0.37;

  position: absolute;
  /* 12px minus the 5px nudge left; the PNG is cropped flush to him, so bottom:0
     seats him exactly on the tile's edge with no baked-in margin. */
  left: 7px;
  bottom: 0;
  width: var(--prince-w);
  height: auto;
  opacity: var(--prince-opacity);
  z-index: 0;
  pointer-events: none;
}

.card-prince h3,
.card-prince p {
  position: relative;
  z-index: 1;
}

/* ---------- the lotus's glow and pollen ---------- */

/*
  Hovering the Magic tile brings the flower to life: a slow pink glow breathes
  behind her, and pollen lifts off and drifts away to the left.

  Both are pure CSS -- a radial gradient and seven 3-5px dots -- so there is no
  asset to load and the whole thing costs one repaint. The pink is sampled from
  lotus.png's own highlights (#fa67cf, #e856b7), pulled down to something that
  reads as a glow on cream rather than a highlighter.

  Everything is scoped to (hover: hover) and starts from opacity 0, so nothing
  animates, or is even visible, until the pointer is on the tile.
*/

.card-lotus .lotus-glow {
  /* Centred on the flower, which sits 8px in from the bottom-right corner and
     is about 105x101. Half of this box hangs off the tile; the tile clips it. */
  --glow-size: 230px;

  position: absolute;
  right: calc(0.5rem + var(--lotus-w, 105px) / 2 - var(--glow-size) / 2);
  bottom: calc(0.5rem + 50px - var(--glow-size) / 2);
  width: var(--glow-size);
  height: var(--glow-size);
  opacity: 0;
  z-index: 0;
  pointer-events: none;
}

/*
  The fade and the pulse are split across two elements on purpose: the parent
  transitions its opacity in and out with the pointer, and the child carries
  the looping breath. One element cannot do both -- a keyframe animation on
  opacity would fight the transition, and the glow would snap off rather than
  fade when the pointer leaves.
*/
.card-lotus .lotus-glow::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(closest-side,
                              rgba(232, 90, 180, 0.26),
                              rgba(232, 90, 180, 0.11) 55%,
                              rgba(232, 90, 180, 0) 100%);
}

.card-lotus .pollen {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.card-lotus .pollen span {
  --mote: 5px;
  --mote-opacity: 0.8;
  --dx: -130px;               /* leftwards, away from the flower */
  --dy: -26px;                /* and drifting up as it goes */
  --wobble: 10px;             /* how far the arc bows mid-flight */

  position: absolute;
  width: var(--mote);
  height: var(--mote);
  border-radius: 50%;
  /* Deeper than the glow's pink: a 5px dot on cream needs the extra weight to
     read at all, where the 230px wash does not. */
  background: #e07cbc;
  box-shadow: 0 0 6px 2px rgba(232, 130, 195, 0.5);
  opacity: 0;
}

/* Each mote leaves from its own point on the flower and takes its own path.
   The spread in durations and delays is what keeps it from reading as seven
   dots released by a metronome. */
.card-lotus .pollen span:nth-child(1) { --mote: 6px; --dx: -150px; --dy: -34px; --wobble:  12px; right: 22%; bottom: 30%; animation-duration: 4.6s; animation-delay: 0s;    }
.card-lotus .pollen span:nth-child(2) { --mote: 4px; --dx: -110px; --dy: -18px; --wobble:  -9px; right: 16%; bottom: 44%; animation-duration: 3.6s; animation-delay: .7s;  }
.card-lotus .pollen span:nth-child(3) { --mote: 5px; --dx: -168px; --dy: -12px; --wobble:  14px; right: 26%; bottom: 22%; animation-duration: 5.4s; animation-delay: 1.4s; }
.card-lotus .pollen span:nth-child(4) { --mote: 4px; --dx: -128px; --dy: -40px; --wobble:   8px; right: 12%; bottom: 36%; animation-duration: 4.1s; animation-delay: 2.1s; }
.card-lotus .pollen span:nth-child(5) { --mote: 7px; --dx: -142px; --dy:  -8px; --wobble: -12px; right: 20%; bottom: 52%; animation-duration: 5.0s; animation-delay: 2.8s; }
.card-lotus .pollen span:nth-child(6) { --mote: 4px; --dx:  -96px; --dy: -30px; --wobble:   7px; right: 28%; bottom: 40%; animation-duration: 3.9s; animation-delay: 3.4s; }
.card-lotus .pollen span:nth-child(7) { --mote: 5px; --dx: -158px; --dy: -22px; --wobble: -10px; right: 18%; bottom: 26%; animation-duration: 4.8s; animation-delay: 4.2s; }

/* Out from the flower, bowing through the middle of the flight, fading in at
   the start and out at the end so motes are never seen popping in or out. */
@keyframes pollen-drift {
  0% {
    transform: translate(0, 0) scale(.4);
    opacity: 0;
  }
  14% {
    opacity: var(--mote-opacity);
  }
  50% {
    transform: translate(calc(var(--dx) * .5),
                         calc(var(--dy) * .5 + var(--wobble))) scale(1);
  }
  72% {
    opacity: var(--mote-opacity);
  }
  100% {
    transform: translate(var(--dx), var(--dy)) scale(.85);
    opacity: 0;
  }
}

@keyframes lotus-breathe {
  0%, 100% { opacity: .7; transform: scale(1);    }
  50%      { opacity:  1; transform: scale(1.09); }
}

@media (hover: hover) {
  .card-lotus .lotus-glow {
    transition: opacity .5s ease;
  }

  .card-lotus:hover .lotus-glow {
    opacity: 1;
  }

  .card-lotus:hover .lotus-glow::before {
    animation: lotus-breathe 3.6s ease-in-out infinite;
  }

  /* The motes only exist while the pointer is here: no animation, no mote.
     Durations and delays come from the per-child rules above. */
  .card-lotus:hover .pollen span {
    animation-name: pollen-drift;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
  }
}

/*
  Reduced motion drops the drift and the breath entirely rather than slowing
  them: drifting particles are exactly what that setting is asking us not to
  do. The glow stays, as a still wash of colour.
*/
@media (hover: hover) and (prefers-reduced-motion: reduce) {
  .card-lotus:hover .lotus-glow::before { animation: none; }
  .card-lotus:hover .pollen span { animation: none; }
}

/* ---------- the prince's stars ---------- */

/*
  Hovering the Books tile winks a handful of stars into the space around the
  prince -- he is reading on his asteroid, so the sky arrives with him.

  The star is a data-URI SVG rather than another image file: it is one path, it
  scales without a second asset for retina, and six of them cost nothing. Its
  fill is baked into the URI, so --gold is duplicated there; change one, change
  the other.

  On strength: the first pass drew these at 0.15 to match the pack, and they
  were effectively invisible -- gold on cream at 15% is nothing, and 10px of it
  is less. They are bigger now and sit at 0.45, which is well above the prince
  himself on purpose. He is a solid, detailed figure and reads at 0.32; a 16px
  outline needs far more to register at all.
*/
.card-prince .stars {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.card-prince .star {
  --star-size: 18px;
  --star-opacity: 0.45;

  position: absolute;
  width: var(--star-size);
  height: var(--star-size);
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0C12.6 6.4 17.6 11.4 24 12C17.6 12.6 12.6 17.6 12 24C11.4 17.6 6.4 12.6 0 12C6.4 11.4 11.4 6.4 12 0Z' fill='%23d8bd77'/%3E%3C/svg%3E")
              center / contain no-repeat;
  /* Resting state is no star at all: they exist only while the tile is
     hovered, which also means they never appear on touch, where :hover can
     latch on after a tap. */
  opacity: 0;
  transform: scale(.3);
}

/* Scattered around the prince, not on him: he holds the bottom-left corner,
   so these keep to the sky above and to his right. Percentages, so they stay
   spread however tall the tile grows. */
.card-prince .star:nth-child(1) { --star-size: 21px; left: 61%; top: 11%; }
.card-prince .star:nth-child(2) { --star-size: 14px; left: 81%; top: 33%; }
.card-prince .star:nth-child(3) { --star-size: 17px; left: 44%; top: 29%; }
.card-prince .star:nth-child(4) { --star-size: 19px; left: 87%; top: 65%; }
.card-prince .star:nth-child(5) { --star-size: 13px; left: 47%; top: 71%; }
.card-prince .star:nth-child(6) { --star-size: 16px; left: 27%; top: 15%; }

@media (hover: hover) {
  /*
    The wink: fade up while springing from 30% to full size. The overshoot in
    the easing is what makes it a wink rather than a fade -- each star pops
    just past its size and settles back. Transitions rather than keyframes so
    the same curve runs backwards when the pointer leaves and the stars
    shrink away again.
  */
  .card-prince .star {
    transition: opacity .35s ease,
                transform .35s cubic-bezier(.34, 1.56, .64, 1);
  }

  .card-prince:hover .star {
    opacity: var(--star-opacity);
    transform: scale(1);
  }

  /* Staggered, so they arrive one after another the way stars come out. */
  .card-prince .star:nth-child(2) { transition-delay: .05s; }
  .card-prince .star:nth-child(3) { transition-delay: .10s; }
  .card-prince .star:nth-child(4) { transition-delay: .15s; }
  .card-prince .star:nth-child(5) { transition-delay: .20s; }
  .card-prince .star:nth-child(6) { transition-delay: .25s; }
}

/* Reduced motion keeps the stars but drops the spring: they simply appear
   while the tile is hovered. */
@media (hover: hover) and (prefers-reduced-motion: reduce) {
  .card-prince .star {
    transition: none;
    transition-delay: 0s;
  }
  .card-prince:hover .star { transform: none; }
}

/* ---------- the booster pack in the Pokemon tile ---------- */

.card-pack {
  position: relative;
  overflow: hidden;
}

/*
  The pack, drawn rather than filmed. It was a 16-frame clip shipped twice
  (h264 and VP9) plus make-pack.py to build it; this is the same beat as one
  inline SVG and a few transitions. What that buys, beyond the two files: the
  colour key and the BG fudge in that script are gone -- h264's limited range
  would not carry --paper-warm, so the clip was composited onto a hand-picked
  #F5EEDF that happened to decode back to the tile colour -- and the ornament
  now sits on whatever the tile's background is, at any size, rather than
  being a 320px raster baked against one specific cream.

  What it gives up is the foil's soft shading and the little bends in the
  plastic as the pack flexes. Flat vector cannot do those.

  The card stack is drawn BEFORE the pack body, so document order alone does
  the hiding: below the pack's top edge the cards are simply behind an opaque
  shape. No clip-path, no masks.

  Everything is a transition rather than a keyframe animation, so leaving the
  tile plays the whole thing backwards -- cards slide home, torn ends swing
  back on -- instead of snapping shut.
*/
.card-pack .pack {
  --c-foil:   #2699cd;        /* the clip's own blue      */
  --c-shade:  #1f86b6;
  --c-sheen:  #63b6dd;
  --c-accent: #e9b93c;
  --c-mark:   #ffffff;

  --c-cardline:   #26332b;    /* the card backs           */
  --c-back:       #2e5a86;
  --c-backline:   #7fb2d8;
  --c-medal:      var(--paper);
  --c-petal:      #cf4436;
  --c-petal-line: #26332b;
  --c-eye:        #e9b93c;

  --crack-time: .55s;

  position: absolute;
  /* Bottom centre rather than tucked into the corner: the fan rises out of
     the pack's head, and against the right edge that put the cards under the
     last line of the copy. Both edges plus margin auto is what actually
     centres an absolutely positioned box. */
  left: 0;
  right: 0;
  /* Flush with the tile's floor rather than on the 0.5rem inset the corner
     ornaments use. The constraint on this one is the copy above it, not the
     border below: measured at a 301px tile the paragraph ends 138px off the
     floor, so every pixel the pack gives back at the bottom is a pixel it can
     have in height. Dropping the inset buys ~8px, which is what pays for 38%
     over 36% while the gap under the copy stays where it was.

     Not below the floor: the fan is meant to be cropped by the tile edge, the
     pack is a whole object and a clipped tail would read as a mistake.

     38% while the copy ran five lines; the rewrite took it to three and left
     166px under it, so this is back up to 43%. The ceiling is about 45% -- at
     that point the sealed pack's head touches the last line -- and the room is
     partly on loan: this row's height comes from the Community tile beside it,
     so if that copy ever shortens, re-measure before trusting the gap. */
  bottom: 0;
  width: 43%;
  margin-inline: auto;
  aspect-ratio: 320 / 372;
  opacity: 0.28;
  z-index: 0;
  pointer-events: none;       /* the tile takes the hover, not the art */
}

/*
  The art is sized off the tile's WIDTH, but what it has to fit inside is the
  room under the copy -- and those two move in opposite directions. Narrow the
  window and the grid drops to two columns: the tiles get thinner, the
  paragraph takes more lines, and the space under it shrinks while the pack,
  being a percentage, barely does. Drop to one column and it inverts -- the
  tile is suddenly 450px wide, so a percentage of it is enormous while the
  copy still needs its lines.

  CSS cannot measure the paragraph, so these are measured breakpoints rather
  than a clever rule. The numbers come from the worst case in each band: at
  two columns there is about 111px under the copy, at one column about 129px
  against a much wider tile.
*/
@media (max-width: 62rem) {
  .card-pack .pack { width: 32%; }
}

@media (max-width: 36rem) {
  .card-pack .pack { width: 21%; }
}

.card-pack .pack svg {
  display: block;
  width: 100%;
  height: 100%;
}

/*
  The cards fan about one pivot low in the pack rather than each turning on
  its own centre: with a shared origin the tops spread as they rise, which is
  what a stack coming out of a pack actually does.
*/
.card-pack .pack .cards use {
  transform-box: view-box;
  transform-origin: 160px 320px;
}

.card-pack .card1,
.card-pack .card2,
.card-pack .card3 {
  transition: transform var(--crack-time) cubic-bezier(.2, .7, .3, 1);
}
.card-pack .card1 { transition-delay: .10s; }
.card-pack .card2 { transition-delay: .15s; }
.card-pack .card3 { transition-delay: .20s; }

/* The torn ends swing off the point the tear opens from -- the middle of the
   pack's head -- rather than sliding sideways, and stay in shot at the
   corners the way the clip keeps them. */
.card-pack .flap-l,
.card-pack .flap-r {
  transform-box: view-box;
  transform-origin: 160px 120px;
  transition: transform calc(var(--crack-time) * .8) cubic-bezier(.3, .8, .4, 1),
              opacity calc(var(--crack-time) * .8) ease;
}

@media (hover: hover) {
  .card-pack .pack { transition: opacity .35s ease; }
  .card-pack:hover .pack { opacity: 0.42; }

  .card-pack:hover .card1 { transform: translateY(-92px) rotate(-6deg); }
  .card-pack:hover .card2 { transform: translateY(-104px) rotate(1deg); }
  .card-pack:hover .card3 { transform: translateY(-84px) rotate(7deg); }

  .card-pack:hover .flap-l { transform: translate(-46px, -40px) rotate(-34deg); opacity: .9; }
  .card-pack:hover .flap-r { transform: translate(46px, -36px) rotate(30deg);   opacity: .9; }
}

@media (hover: hover) and (prefers-reduced-motion: reduce) {
  .card-pack .card1,
  .card-pack .card2,
  .card-pack .card3,
  .card-pack .flap-l,
  .card-pack .flap-r { transition: none; }
}

.card-pack h3,
.card-pack p {
  position: relative;
  z-index: 1;
}

/* --- The artwork no longer brightens on hover. ---
   Both illustrated tiles used to bring their art up to full strength when
   pointed at. Both now answer a hover with something of their own instead --
   the prince with his stars, the lotus with her glow and pollen -- and having
   the artwork bloom at the same time made each tile busy. They hold their
   resting opacities (0.32 and 0.30) and let the animation do the talking.

   To bring the old behaviour back, add this beside the two hover blocks below:

     @media (hover: hover) {
       .card-lotus .lotus, .card-prince .prince { transition: opacity .25s ease; }
       .card-lotus:hover .lotus, .card-prince:hover .prince { opacity: 1; }
     }
*/

/* ---------- the tiles on a phone ---------- */

/*
  Single column makes each tile WIDER than it is on desktop (a full 342px on a
  390px phone, against ~301px in the three-up grid) while the copy stays short,
  so tiles end up wide and shallow. Art sized for the desktop tile then fills
  almost the whole height of one, hence the shallower floor and smaller art.
*/
@media (max-width: 34rem) {
  .card {
    min-height: 17rem;
  }

  .card-prince .prince { --prince-w: 88px; }
  .card-lotus .lotus   { --lotus-w: 68px; }
}

/* ---------- the prince's stars: the outer four, and their glow ----------
   Ten stars in all. The first six hold the corners; these four fill the
   middle band, which only opened up when .card's floor went from 13rem to
   21rem. Positions stay percentages, so they ride the tile height. */
.card-prince .star:nth-child(7)  { --star-size: 15px; left: 70%; top: 46%; }
.card-prince .star:nth-child(8)  { --star-size: 12px; left: 34%; top: 57%; }
.card-prince .star:nth-child(9)  { --star-size: 13px; left: 91%; top: 19%; }
.card-prince .star:nth-child(10) { --star-size: 18px; left: 57%; top: 83%; }

/*
  The glow. Two stacked drop-shadows rather than one: a tight bright core
  that hugs the four points, and a wide faint bloom that spreads past them.
  A single shadow at either radius reads as a blur or as a smudge; the pair
  reads as light. drop-shadow follows the shape's own alpha, so the halo is
  star-shaped instead of the box-shaped one box-shadow would give.
*/
.card-prince .star {
  filter: drop-shadow(0 0 4px rgba(216, 189, 119, 0.9))
          drop-shadow(0 0 11px rgba(216, 189, 119, 0.45));
}

@media (hover: hover) {
  /* Carry the stagger on to the new four. */
  .card-prince .star:nth-child(7)  { transition-delay: .30s; }
  .card-prince .star:nth-child(8)  { transition-delay: .35s; }
  .card-prince .star:nth-child(9)  { transition-delay: .40s; }
  .card-prince .star:nth-child(10) { transition-delay: .45s; }
}

/* ---------- the comics tile: a spotlight and a signal ----------
   The lamp sits in the bottom-left corner all the time, faint like the other
   tiles' art. Hovering throws a beam up across the tile and lights a bat on
   the far side.

   Both drawings are data-URI SVGs, same as the prince's star: the lamp is
   four flat shapes, and the bat is a body, a head, two ears and a pair of
   scalloped wings. It is drawn from scratch here -- rounded scalloped wings,
   visible ears, a tapered body -- and is deliberately nothing like the
   elongated notched ellipse anyone else uses.

   Geometry: the lamp's mouth is at about (55px, 44px up from the bottom) and
   the signal's centre lands up and to the right of it, so the beam is one
   wedge rotated out of that mouth. --beam-* keep those numbers in one place;
   move the lamp and the beam follows.

   The whole arrangement is bottom-anchored. The tile took on manga and its
   copy went from one line to five, so the beam stopped climbing as steeply as
   it used to -- about 22 degrees now rather than 40. The disc is the same
   112px it always was: what changed is where the art sits, not how big it is.

   The disc is raised far enough that its top sits behind the last line or two
   of the copy. That is deliberate, and it is why the copy carries z-index 1:
   the glow passes under the text, which stays readable over a pale wash.

   The bat is the dark part, and it does NOT clear the paragraph's last line
   by height -- it overlaps that band by about 18px and gets away with it only
   because the line is short and ends well left of the disc. So this placement
   is tied to the copy: rewrite the paragraph so the final line runs the full
   width and the bat will sit on top of it. If that happens, lower `bottom`
   here until it clears, and re-aim --beam-angle at the new centre (the angle
   is atan2 of the disc centre against the lamp mouth, and --beam-len is the
   distance between them).

   Two things to know before moving any of these numbers. The disc rests at
   scale(.7) and only reaches its full 112px on hover, so it is the HOVER size
   that has to clear the copy -- measuring the resting box will tell you there
   is room when there is not. And the disc is anchored to the bottom rather
   than a top percentage, so another line of copy moves nothing.
*/
.card-signal {
  --lamp-x: 55px;          /* the mouth of the lamp, from the left  */
  --lamp-y: 44px;          /* ...and up from the bottom             */
  --beam-len: 190px;
  --beam-spread: 95px;
  --beam-angle: -22deg;
  --signal-size: 112px;
  --signal-glow: rgba(246, 223, 150, 0.95);

  position: relative;
  overflow: hidden;        /* the beam runs past the corner; clip it */
}

.card-signal h3,
.card-signal p {
  position: relative;
  z-index: 1;
}

.card-signal .spotlight {
  position: absolute;
  left: 10px;
  bottom: 0;
  width: 64px;
  height: 48px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 48'%3E%3Cg fill='%2326332b'%3E%3Cpolygon points='12.6,36.4 39.2,4.7 52.8,19.3 19.4,43.7'/%3E%3Cellipse cx='46' cy='12' rx='5' ry='9' transform='rotate(43 46 12)'/%3E%3Crect x='7' y='39' width='19' height='5' rx='2.5'/%3E%3Cpolygon points='10,44 24,44 27,48 7,48'/%3E%3C/g%3E%3C/svg%3E")
              center / contain no-repeat;
  opacity: 0.3;            /* the prince's and the lotus's resting weight */
  z-index: 0;
  pointer-events: none;
}

.card-signal .beam {
  position: absolute;
  left: var(--lamp-x);
  bottom: var(--lamp-y);
  width: var(--beam-len);
  height: var(--beam-spread);
  margin-bottom: calc(var(--beam-spread) / -2);   /* centre on the mouth */
  /* A wedge: a slit at the lamp opening out to the full spread at the far
     end, fading as it goes so the light falls off with distance. */
  clip-path: polygon(0 46%, 100% 0, 100% 100%, 0 54%);
  background: linear-gradient(to right,
                              rgba(246, 223, 150, 0.55),
                              rgba(246, 223, 150, 0.16) 55%,
                              rgba(246, 223, 150, 0) 100%);
  transform-origin: 0 50%;
  transform: rotate(var(--beam-angle)) scaleX(.15);
  opacity: 0;
  z-index: 0;
  pointer-events: none;
}

.card-signal .signal {
  position: absolute;
  /* A fixed offset rather than a percentage: the beam's angle is computed
     from this point, and a percentage moved the target every time the column
     width changed. */
  right: 14px;
  bottom: 60px;            /* where the beam lands */
  width: var(--signal-size);
  height: var(--signal-size);
  opacity: 0;
  transform: scale(.7);
  z-index: 0;
  pointer-events: none;
}

/* The lit disc. Split from .signal for the same reason the lotus's glow is:
   the parent fades with the pointer, the child carries the looping pulse. */
.card-signal .signal::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(closest-side,
                              var(--signal-glow),
                              rgba(246, 223, 150, 0.35) 62%,
                              rgba(246, 223, 150, 0) 100%);
}

/* The bat, sitting in the middle of the disc. */
.card-signal .signal::after {
  content: "";
  position: absolute;
  inset: 15%;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 54'%3E%3Cg fill='%23223528'%3E%3Cellipse cx='50' cy='30' rx='7.5' ry='16'/%3E%3Ccircle cx='50' cy='16' r='7'/%3E%3Cpolygon points='44.5,11 41,1 49,8'/%3E%3Cpolygon points='55.5,11 59,1 51,8'/%3E%3Cpath d='M57 19C70 10 84 12 98 6C95 21 91 30 85 38C82 31 79 30 74 37C71 30 67 30 62 38C59 32 57 31 55 34Z'/%3E%3Cpath d='M43 19C30 10 16 12 2 6C5 21 9 30 15 38C18 31 21 30 26 37C29 30 33 30 38 38C41 32 43 31 45 34Z'/%3E%3C/g%3E%3C/svg%3E")
              center / contain no-repeat;
}

@keyframes signal-pulse {
  0%, 100% { opacity: .85; transform: scale(1);    }
  50%      { opacity:   1; transform: scale(1.05); }
}

@media (hover: hover) {
  .card-signal .spotlight { transition: opacity .3s ease; }
  .card-signal .beam {
    transition: opacity .3s ease, transform .45s cubic-bezier(.22, .9, .3, 1);
  }
  .card-signal .signal {
    /* Delayed, so the beam is most of the way across before the bat lands. */
    transition: opacity .3s ease .22s, transform .4s cubic-bezier(.34, 1.4, .64, 1) .22s;
  }

  .card-signal:hover .spotlight { opacity: .5; }
  .card-signal:hover .beam {
    opacity: 1;
    transform: rotate(var(--beam-angle)) scaleX(1);
  }
  .card-signal:hover .signal {
    opacity: 1;
    transform: scale(1);
  }
  .card-signal:hover .signal::before {
    animation: signal-pulse 2.4s ease-in-out infinite .6s;
  }
}

@media (hover: hover) and (prefers-reduced-motion: reduce) {
  .card-signal .beam,
  .card-signal .signal { transition: opacity .3s ease; }
  .card-signal:hover .beam   { transform: rotate(var(--beam-angle)) scaleX(1); }
  .card-signal:hover .signal { transform: scale(1); }
  .card-signal:hover .signal::before { animation: none; }
}

/* ---------- the board games tile: a pair of dice ----------
   Two dice, the same size, sitting in the empty bottom-right corner at the
   same resting weight as the other tiles' art. Hovering tumbles them where
   they stand: each hops, turns a full 360, and comes back down on its mark.

   An earlier pass threw them up against the tile's right border and let them
   drop back. It read as wonky -- two dice converging on one point, arriving
   turned, rebounding off a border that is a hairline rather than a wall --
   so this is the plainer version.

   Landing back on its own mark is still the point: the animation only exists
   while the tile is hovered, so anything ending somewhere else would snap
   back the moment the pointer left. A closed loop can just stop.

   The faces are four data-URI SVGs -- a rounded square and some pips -- and
   the keyframes swap between them as the die turns, so the value flickers
   mid-roll and settles. background-image does not interpolate, it jumps at
   each keyframe, which is exactly the read we want.
*/
.card-dice {
  position: relative;
}

.card-dice h3,
.card-dice p {
  position: relative;
  z-index: 1;
}

.card-dice .dice {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.card-dice .die {
  --die: 40px;

  position: absolute;
  width: var(--die);
  height: var(--die);
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect x='1.6' y='1.6' width='36.8' height='36.8' rx='8' fill='%23f3ecdd' stroke='%2326332b' stroke-width='2.6'/%3E%3Ccircle cx='11' cy='11' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='11' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='20' cy='20' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='11' cy='29' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='29' r='3.5' fill='%2326332b'/%3E%3C/svg%3E") center / contain no-repeat;
  opacity: 0.3;
}

.card-dice .die:nth-child(1) { right: 11%; bottom: 7%; }
.card-dice .die:nth-child(2) { right: 32%; bottom: 4%; }

/* A hop with a full turn in it, and the face changing on the way round. */
@keyframes die-tumble {
  0%   { transform: translate(0, 0) rotate(0deg);          background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect x='1.6' y='1.6' width='36.8' height='36.8' rx='8' fill='%23f3ecdd' stroke='%2326332b' stroke-width='2.6'/%3E%3Ccircle cx='11' cy='11' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='11' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='20' cy='20' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='11' cy='29' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='29' r='3.5' fill='%2326332b'/%3E%3C/svg%3E"); }
  20%  { transform: translate(-3px, -24px) rotate(96deg);   background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect x='1.6' y='1.6' width='36.8' height='36.8' rx='8' fill='%23f3ecdd' stroke='%2326332b' stroke-width='2.6'/%3E%3Ccircle cx='13' cy='13' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='27' cy='27' r='3.5' fill='%2326332b'/%3E%3C/svg%3E"); }
  45%  { transform: translate(-6px, -32px) rotate(205deg);  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect x='1.6' y='1.6' width='36.8' height='36.8' rx='8' fill='%23f3ecdd' stroke='%2326332b' stroke-width='2.6'/%3E%3Ccircle cx='11' cy='10' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='11' cy='20' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='11' cy='30' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='10' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='20' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='30' r='3.5' fill='%2326332b'/%3E%3C/svg%3E"); }
  70%  { transform: translate(-4px, -9px) rotate(303deg);   background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect x='1.6' y='1.6' width='36.8' height='36.8' rx='8' fill='%23f3ecdd' stroke='%2326332b' stroke-width='2.6'/%3E%3Ccircle cx='11' cy='11' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='20' cy='20' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='29' r='3.5' fill='%2326332b'/%3E%3C/svg%3E"); }
  86%  { transform: translate(-1px, -3px) rotate(347deg);                            }
  100% { transform: translate(0, 0) rotate(360deg);         background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect x='1.6' y='1.6' width='36.8' height='36.8' rx='8' fill='%23f3ecdd' stroke='%2326332b' stroke-width='2.6'/%3E%3Ccircle cx='11' cy='11' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='11' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='20' cy='20' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='11' cy='29' r='3.5' fill='%2326332b'/%3E%3Ccircle cx='29' cy='29' r='3.5' fill='%2326332b'/%3E%3C/svg%3E"); }
}

@media (hover: hover) {
  .card-dice .die { transition: opacity .3s ease; }
  .card-dice:hover .die { opacity: 0.5; }

  /* The second die is thrown a beat later and takes a touch longer, so the
     two never land together. */
  .card-dice:hover .die:nth-child(1) { animation: die-tumble .95s cubic-bezier(.3,.7,.4,1) 1; }
  .card-dice:hover .die:nth-child(2) { animation: die-tumble 1.1s cubic-bezier(.3,.7,.4,1) .13s 1; }
}

@media (hover: hover) and (prefers-reduced-motion: reduce) {
  .card-dice:hover .die:nth-child(1),
  .card-dice:hover .die:nth-child(2) { animation: none; }
}

/* ---------- the dragon in the D&D tile ---------- */

/*
  Two paintings of the same dragon -- one calm, one breathing fire -- keyed
  off their purple backdrop by make-dragon.py. Because that script crops both
  to one shared box, the pair is registered: crossfading them moves nothing
  but the flame.

  This one is not a corner ornament like the lotus and the prince. The
  painting is a wide reclining composition, so it runs the full width of the
  tile along the bottom and the copy sits above it rather than over it.
*/
.card-dragon {
  position: relative;
  overflow: hidden;
}

.card-dragon .dragon {
  position: absolute;
  /* Anchored bottom right, at 90% of the width it would span corner to
     corner -- the painting reads better with a little air off the left. */
  right: 0.5rem;
  bottom: 0.5rem;
  width: calc((100% - 1rem) * 0.9);
  /* A faint ground for the copy rather than a picture on the tile, the way
     the lotus and the prince sit. This is on the wrapper, so it holds in both
     states: the hover rule below switches the fire painting on *within* this
     0.3 and can never lift the pair above it. */
  opacity: 0.3;
  /* 640x287 is what make-dragon.py writes; holding the ratio here means the
     box is the right height before either image has loaded. */
  aspect-ratio: 640 / 287;
  z-index: 0;
  pointer-events: none;
}

.card-dragon .dragon img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/*
  No crossfade. Fading one painting out while the other faded in dipped the
  pair's combined coverage mid-transition -- at the halfway point neither is
  fully there -- and that dip read as a blink.

  So the calm painting simply stays put and the fire one switches on over the
  top of it. The two are the same dragon in register, so the upper covers the
  lower exactly and only the flame appears. Nothing fades, nothing dips.
*/
.card-dragon .dragon-fire { opacity: 0; }

.card-dragon h3,
.card-dragon p {
  position: relative;
  z-index: 1;
}

/*
  The swap and the glow are both scoped to (hover: hover), like the lotus and
  the dice: on a touch screen the tile just shows the calm painting.
*/
@media (hover: hover) {
  .card-dragon:hover .dragon-fire { opacity: 1; }
  .card-dragon:hover .fire-glow { opacity: 1; animation: fire-glow-pulse 2.4s ease-in-out infinite; }
}

/*
  Sits under both paintings, centred on the flame -- which the artwork puts
  around 9% across and 36% down. Percentage insets keep it there at any tile
  width. It is a radial gradient rather than a blurred shape so it costs one
  paint and no filter.
*/
.card-dragon .fire-glow {
  position: absolute;
  left: 9%;
  top: 36%;
  width: 52%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle,
              rgba(255,186,80,.95) 0%,
              rgba(240,110,30,.58) 40%,
              rgba(216,61,20,0) 75%);
  opacity: 0;
  transition: opacity .45s ease;
}

@keyframes fire-glow-pulse {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  50%      { transform: translate(-50%, -50%) scale(1.14); }
}

@media (prefers-reduced-motion: reduce) {
  /* The crossfade is already killed by the global transition reset; this is
     for the pulse, which is an animation and survives it. */
  .card-dragon:hover .fire-glow { animation: none; }
}

/* ---------- the page in the Graphic Novels tile ----------

   The thing that says "graphic novel" and not "comic" is the page itself: a
   grid of panels, read in order, ending on a wide one. Comics next door owns
   the bat-signal, so this tile leans on sequential art instead -- a whole page
   rather than a single image.

   The ornament is that page, laid into the bottom-right corner on the same
   0.5rem inset the pack uses, and the animation is the reading path: each
   panel inks in one after the next, left to right and down, and the balloon
   lands on the wide middle panel once the eye has arrived there. Idle it is a
   faint outline like the other tiles' art; the hover fills it in.

   No images -- the panels are borders and the balloon is one data-URI SVG.
   Tuned on _graphic-novels-demo.html. */

.card-novel {
  position: relative;
  overflow: hidden;
}

.card-novel h3,
.card-novel p {
  position: relative;
  z-index: 1;
}

/* The page. A corner ornament on the same 0.5rem inset the pack uses, sized
   as a fraction of the tile so it tracks the column width. */
.card-novel .page {
  --panel-opacity: 0.22;      /* idle: a ghost of a page      */
  --panel-opacity-read: 0.5;  /* hover: inked in              */
  --read-step: .09s;          /* gap between panels, in order */

  position: absolute;
  inset: auto 0.5rem 0.5rem auto;
  width: 35%;
  aspect-ratio: 5 / 6;
  z-index: 0;
  pointer-events: none;

  display: grid;
  /* Six columns so the bottom row can be split unevenly -- a narrow panel
     against a wide one, which is what keeps the page from reading as a
     spreadsheet. */
  grid-template-columns: repeat(6, 1fr);
  grid-template-rows: 1fr 1.15fr 1fr;
  gap: 6px;
}

.card-novel .panel {
  border: 2px solid var(--ink);
  border-radius: 2px;
  background: transparent;
  opacity: var(--panel-opacity);
  transform: translateY(3px);
}

/* Reading order: two across the top, a wide one under them, then a narrow and
   a wide to close. */
.card-novel .panel:nth-child(1) { grid-area: 1 / 1 / 2 / 4; }
.card-novel .panel:nth-child(2) { grid-area: 1 / 4 / 2 / 7; }
.card-novel .panel:nth-child(3) { grid-area: 2 / 1 / 3 / 7; }
.card-novel .panel:nth-child(4) { grid-area: 3 / 1 / 4 / 3; }
.card-novel .panel:nth-child(5) { grid-area: 3 / 3 / 4 / 7; }

/* The balloon sits on the wide middle panel, the one the eye lands on third. */
.card-novel .balloon {
  position: absolute;
  left: 8%;
  top: 40%;
  width: 44%;
  aspect-ratio: 74 / 52;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 74 52'%3E%3Cpath d='M10 2h54a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30l-14 12 3-12h-9a8 8 0 0 1-8-8V10a8 8 0 0 1 8-8z' fill='%23fbf8f1' stroke='%2326332b' stroke-width='3' stroke-linejoin='round'/%3E%3Cpath d='M15 16h44M15 25h30' stroke='%2326332b' stroke-width='3' stroke-linecap='round' opacity='.55'/%3E%3C/svg%3E") center / contain no-repeat;
  opacity: 0;
  transform: scale(.4);
  transform-origin: 20% 90%;   /* grows out of its own tail */
}

@media (hover: hover) {
  .card-novel .panel {
    transition: opacity .3s ease,
                background-color .3s ease,
                transform .3s cubic-bezier(.34, 1.56, .64, 1);
  }
  .card-novel:hover .panel {
    opacity: var(--panel-opacity-read);
    background: var(--paper);
    transform: translateY(0);
  }

  /* The stagger is the whole point: the panels arrive in reading order, so the
     eye is walked across the page rather than shown it all at once. */
  .card-novel:hover .panel:nth-child(1) { transition-delay: calc(var(--read-step) * 0); }
  .card-novel:hover .panel:nth-child(2) { transition-delay: calc(var(--read-step) * 1); }
  .card-novel:hover .panel:nth-child(3) { transition-delay: calc(var(--read-step) * 2); }
  .card-novel:hover .panel:nth-child(4) { transition-delay: calc(var(--read-step) * 3); }
  .card-novel:hover .panel:nth-child(5) { transition-delay: calc(var(--read-step) * 4); }

  .card-novel .balloon {
    transition: opacity .25s ease,
                transform .4s cubic-bezier(.34, 1.7, .64, 1);
  }
  /* Lands a beat after panel 3 has inked in, not with it. */
  .card-novel:hover .balloon {
    opacity: .85;
    transform: scale(1);
    transition-delay: calc(var(--read-step) * 2 + .18s);
  }
}

@media (hover: hover) and (prefers-reduced-motion: reduce) {
  .card-novel .panel,
  .card-novel .balloon { transition: none; transition-delay: 0s; }
  .card-novel:hover .panel { transform: none; }
  .card-novel:hover .balloon { transform: scale(1); }
}

/* ---------- the hand in the community play space tile ----------

   Seven cards, backs out, rising over the tile's bottom edge: the tile is the
   view from your own seat at the table, so the fan is cut off where your hand
   would be rather than floating inside the tile.

   The fan is one rotation each about a pivot below the card, not seven hand
   placed offsets -- transform-origin does the arc, so the spread is a single
   number (--spread) and hover only has to change that number.
*/
.card-hand {
  position: relative;
  /* The bottoms of the cards run past the tile edge; this is what cuts them. */
  overflow: hidden;
}

.card-hand h3,
.card-hand p {
  position: relative;
  z-index: 1;
}

.card-hand .hand {
  --spread: 8.5deg;
  --lift: 0px;

  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 7rem;
  /* Sat back from the 0.34 the pale version wore: a dark card back at that
     weight was the loudest thing in the row. */
  opacity: 0.26;
  z-index: 0;
  pointer-events: none;
}

.card-hand .held {
  position: absolute;
  left: 50%;
  /* Held below the tile's floor, so even the middle card is cropped. */
  bottom: -1.5rem;
  width: 3.6rem;
  height: 5.2rem;
  border: 1.5px solid var(--ink);
  border-radius: 4px;
  /* A dark card back rather than a blank one. Not any particular game's --
     no symbols, no wordmark -- just the arrangement the older card backs
     share: a deep border, a lighter panel inside it, and a big pale oval
     sitting high on the card. The gradient is the light falling on a card
     tipped away from you; flat fills made the fan look like seven stickers. */
  background:
    linear-gradient(150deg, #5c4733 55%, #463627 100%);
  /* The pivot is below the card, roughly where your knuckles would be: every
     card rotates about that one point, which is what makes a fan. */
  transform-origin: 50% 165%;
  transform: translateX(-50%) translateY(var(--lift))
             rotate(calc(var(--i) * var(--spread)));
}

/* The panel inside the border, in the tan these backs carry. */
.card-hand .held::before {
  content: "";
  position: absolute;
  inset: 3px;
  border: 1px solid #b99a72;
  border-radius: 2px;
  opacity: .55;
}

/*
  The oval. It sits high rather than centred because the fan only ever shows
  the top of each card -- the hand is cropped by the tile's floor, so a
  centred one would be under the edge and never seen.
*/
.card-hand .held::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 16%;
  width: 62%;
  height: 40%;
  margin-left: -31%;
  border-radius: 50%;
  background: #cbb08a;
  border: 1px solid #6d573f;
  opacity: .75;
}

/* Fanned from the middle out. The later cards sit over the earlier ones, so
   the stacking follows the fan's own left-to-right order. */
.card-hand .held:nth-child(1) { --i: -3; }
.card-hand .held:nth-child(2) { --i: -2; }
.card-hand .held:nth-child(3) { --i: -1; }
.card-hand .held:nth-child(4) { --i:  0; }
.card-hand .held:nth-child(5) { --i:  1; }
.card-hand .held:nth-child(6) { --i:  2; }
.card-hand .held:nth-child(7) { --i:  3; }

@media (hover: hover) {
  .card-hand .hand { transition: opacity .3s ease; }
  .card-hand .held {
    transition: transform .45s cubic-bezier(.34, 1.4, .64, 1);
  }

  /* The outer cards travel furthest, so they start a beat later and the fan
     opens from the middle out rather than all at once. Written out per card
     rather than calc'd off --i, which would need abs(). */
  .card-hand .held:nth-child(4) { transition-delay: 0s;    }
  .card-hand .held:nth-child(3),
  .card-hand .held:nth-child(5) { transition-delay: .035s; }
  .card-hand .held:nth-child(2),
  .card-hand .held:nth-child(6) { transition-delay: .07s;  }
  .card-hand .held:nth-child(1),
  .card-hand .held:nth-child(7) { transition-delay: .105s; }

  .card-hand:hover .hand {
    --spread: 11deg;
    --lift: -1.1rem;

    opacity: 0.38;
  }
}

@media (hover: hover) and (prefers-reduced-motion: reduce) {
  .card-hand .held { transition: none; transition-delay: 0s; }
  .card-hand:hover .hand { --lift: 0px; }
}

/* ---------- the miniatures in the Wargames and Minis tile ----------

   Our own drawings rather than a publisher's artwork. This tile used to carry
   the Warhammer logo, which is licensed to us as Authorized Imagery and has
   to be shown exactly and entirely -- no cropping, fading or effects -- so
   anything animated had to be ours. The sheen built for that logo is parked,
   unloaded, in warhammer-sheen.css. The tile now covers several games
   anyway, which is why the row is three different sculpts.

   The copy ends on grabbing a brush and making an army your own, so the hover
   is an evening at the painting table: three bare models in a row, and the
   brush works down the line -- crest to base on the first, along to the
   second, then the third -- and goes back to where it started.

   Three sculpts, not one repeated: a knight, a goblin, and a walking
   machine. Three of the same silhouette would have read as one model drawn
   badly three times, and the point of the row is that a painting table has a
   bit of everything on it. They share a footprint -- same base, same floor --
   so the row still lines up.

   Each is drawn ONCE in <defs> and placed twice, bare and painted, with every
   shape taking its fill from a custom property (--c-armour, --c-skin, and so
   on). The two <use> elements set those properties to different values --
   bare paper for one, the site's palette for the other -- because custom
   properties inherit into a <use>'s shadow content where ordinary selectors
   cannot reach. So a painted copy can never drift out of register with its
   bare one: they are the same geometry.
*/
.card-mini {
  position: relative;
  overflow: hidden;
}

.card-mini h3,
.card-mini p {
  position: relative;
  z-index: 1;
}

/* Three across is a wide subject, so this runs the width of the tile along
   the bottom the way the dragon does, rather than sitting in a corner. */
.card-mini .mini {
  --stroke-time: 3.9s;
  --paint-time: .62s;

  position: absolute;
  left: 0.5rem;
  right: 0.5rem;
  bottom: 0.6rem;
  width: 94%;
  margin-inline: auto;
  aspect-ratio: 480 / 200;
  opacity: 0.32;
  z-index: 0;
  pointer-events: none;
}

.card-mini .mini svg {
  display: block;
  width: 100%;
  height: 100%;
  /* One ink line for the whole drawing; the fills are what change. */
  stroke: var(--ink);
  stroke-width: 2.2;
  stroke-linejoin: round;
}

/* Bare resin: paper where the colour will go. */
.card-mini .bare {
  --c-armour: var(--paper);
  --c-cloak:  var(--paper-warm);
  --c-trim:   var(--paper);
  --c-shield: var(--paper-warm);
  --c-base:   var(--paper-warm);
  --c-haft:   var(--paper);
  --c-skin:   var(--paper);
}

/*
  Painted, in the site's own palette so the tile still belongs to the page.

  The reveal is clip-path: inset() from 100% (nothing) to 0 (all of it).
  Percentages there resolve against the element's own bounding box, so each
  model wipes from its own crest to its own base at any tile width.
*/
.card-mini .paint {
  --c-armour: #55636a;          /* steel, a touch cooler than --ink-soft */
  --c-cloak:  var(--berry);
  --c-trim:   var(--gold);
  --c-shield: var(--leaf-deep);
  --c-base:   var(--leaf);
  --c-haft:   #8a6a44;          /* wood, and the walker's feet */
  --c-skin:   #7d8f5a;          /* the goblin */

  clip-path: inset(100% 0 0 0);
}

/*
  CSS transforms on SVG elements rotate about the reference box's centre by
  default, which is not what the numbers below assume. view-box with a 0 0
  origin makes them behave exactly like an SVG transform attribute: rotation
  about the user-space origin.
*/
.card-mini .brush {
  transform-box: view-box;
  transform-origin: 0 0;
  transform: translate(455px, 92.8px) rotate(20deg);
}

.card-mini .bristle { fill: var(--berry); }

/*
  Down the line and home again. Each model gets the same three moves -- up to
  the crest, down the body, lift off -- 150 to the right of the last, and the
  brush ends on its own mark.

  Ending where it started is the point, the way the dice land back on theirs:
  the animation only exists while the tile is hovered, so anything finishing
  somewhere else would snap back the moment the pointer left. A closed loop
  can just stop.
*/
@keyframes mini-stroke {
  0%   { transform: translate(455px, 92.8px)   rotate(20deg); }

  7%   { transform: translate(101.6px, 3.8px)  rotate(16deg); }   /* model 1 */
  23%  { transform: translate(99.6px, 67.8px)  rotate(22deg); }
  28%  { transform: translate(115px, 62px)     rotate(20deg); }

  /* The goblin stands a head shorter, so the stroke starts lower on him --
     the tip has to meet the model, not the air above it. */
  36%  { transform: translate(251.6px, 20px)   rotate(16deg); }   /* goblin  */
  52%  { transform: translate(249.6px, 72px)   rotate(22deg); }
  57%  { transform: translate(265px, 66px)     rotate(20deg); }

  /* And the walker is the tallest thing on the row. */
  65%  { transform: translate(401.6px, -4px)   rotate(16deg); }   /* walker  */
  81%  { transform: translate(399.6px, 70px)   rotate(22deg); }
  86%  { transform: translate(415px, 64px)     rotate(20deg); }

  100% { transform: translate(455px, 92.8px)   rotate(20deg); }   /* home     */
}

@media (hover: hover) {
  .card-mini .mini { transition: opacity .35s ease; }

  /*
    Each model's colour is delayed to the moment the brush reaches its crest
    and finishes about when the tip reaches its base, so the paint follows the
    tip rather than racing it. The numbers are the keyframe percentages above
    read against --stroke-time: 7%, 36% and 65% of 3.9s.
  */
  .card-mini .paint { transition: clip-path var(--paint-time) cubic-bezier(.4, 0, .35, 1); }
  .card-mini .m1 .paint { transition-delay: .27s; }
  .card-mini .m2 .paint { transition-delay: 1.40s; }
  .card-mini .m3 .paint { transition-delay: 2.54s; }

  .card-mini:hover .mini  { opacity: 0.5; }
  .card-mini:hover .paint { clip-path: inset(0 0 0 0); }
  .card-mini:hover .brush { animation: mini-stroke var(--stroke-time) cubic-bezier(.4, 0, .4, 1) 1; }
}

@media (hover: hover) and (prefers-reduced-motion: reduce) {
  .card-mini .paint { transition: none; }
  .card-mini:hover .brush { animation: none; }
}
