/*
 *  shelf.css — the game shelf at games.bonmipang.com.
 *
 *  Visual language is the park's, not a new one: cream cards, dark wood bars,
 *  one honey-gold primary action, blue only for tools, green only for open.
 *  Tokens are UI-DESIGN-STANDARD §2.3 verbatim so this page and every game
 *  lobby keep saying the same thing with the same colours.
 *
 *  Two rules this file exists to keep:
 *    - the park dock (shared/client/social.js) owns the top-right corner. The
 *      header reserves its width with --park-dock-rest-w instead of a magic
 *      number, because that strip measures 40–583px depending on what is open.
 *    - on phones there is exactly ONE scrolling container (the document).
 *      No nested overflow:auto, which eats the touch gesture.
 */

:root {
  --ui-cream-0: #fffdf7; --ui-cream-1: #fff8e8; --ui-cream-2: #f6e8c8;
  --ui-ink: #3a2c12; --ui-muted: #806d4d; --ui-gold: #f5a91b; --ui-gold-dark: #9d6508;
  --ui-blue: #2e8fd6; --ui-green: #35ad58; --ui-danger: #d94b52;
  --ui-radius-sm: 11px; --ui-radius-md: 18px; --ui-radius-lg: 24px;
  --ui-shadow-card: 0 14px 38px rgba(63,42,19,.22), inset 0 1px 0 rgba(255,255,255,.75);
  --ui-speed-fast: 160ms; --ui-speed: 220ms;

  --font-ui: -apple-system, "Avenir Next", "Segoe UI", "Noto Sans TC", sans-serif;
  --font-display: 'Huninn', var(--font-ui);

  --wood: rgba(50,32,14,.66);
  --wood-edge: 1.5px solid rgba(255,230,160,.35);
  --wood-ink: #fff5dc;
  --card-face: linear-gradient(rgba(255,253,247,.95), rgba(255,248,232,.92));

  /* The park panels are dark by default because the first two games to adopt
     them were dark games. Only the hooks are set here — never their defaults. */
  --soc-sheet: #fffdf7; --soc-edge: rgba(150,115,55,.3); --soc-ink: #3a2c12;
  --soc-secfg: #806d4d; --soc-card: #fff8e8; --soc-cardfg: #3a2c12; --soc-accent: #f5a91b;
}

/* jf open 粉圓 (justfont open-huninn, OFL) — the rounded display face the
   share cards are typeset in, so the page and the pictures on it agree.
   SUBSET, not the whole font: 209 glyphs (every game name in zh-TW, zh-CN and
   English, plus the handful of fixed words below) is 36KB against 4.9MB.
   It is therefore used ONLY on text whose characters are in that subset —
   anything else would fall back to the system face mid-word. */
@font-face {
  font-family: 'Huninn';
  src: url('../assets/fonts/huninn-subset.woff2') format('woff2');
  font-weight: 400 900;
  font-display: swap;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  min-height: 100%;
  color: var(--ui-ink);
  font-family: var(--font-ui);
  background: #fdf3df;
  -webkit-font-smoothing: antialiased;
}

/* The park scene is a BANNER on the hero, not a page-wide fixed background.
   It used to be `background-attachment: fixed` on <body>, which makes the
   compositor repaint a full-width image on every layout - with the grid
   rebuilding 25 cards on each filter change, that crashed the renderer
   outright in testing. Scoping the picture to the element that actually shows
   it is both cheaper and what the design wants: art at the top, cream below. */
.heroArt {
  /* The PICTURE is taller than the hero's own content, and runs on behind the
     filter scroll instead of pushing it down: a cover is 1200x630, so at the
     full window width the whole thing needs width/1.905 of height, which is
     more than the sign and the two lines under it occupy. max(), so a narrow
     phone - where the text needs more height than the picture - still has the
     picture cover the whole hero rather than leaving a strip of sky. */
  position: absolute; top: 0; left: 0; right: 0; z-index: 0; overflow: hidden;
  height: max(100%, calc(100vw / 1.905));
  /* A plain sky wash, not the painted park: that picture has the sign baked
     into it, and the sign is now a separate overlay - showing both would be
     two signs stacked. The first slide covers this within a frame. */
  background: linear-gradient(#bfe4f8, #e6f3fb 60%, #f6ead2);
}
/* the two cross-fading slides */
.heroSlide {
  /* inset 0 and plain centring: the slides used to be inflated by 6% and
     pushed off-centre, which just enlarged the covers for no gain. */
  position: absolute; inset: 0;
  background-position: center center; background-size: cover; background-repeat: no-repeat;
  opacity: 0; transition: opacity 1100ms ease-in-out;
}
.heroSlide.on { opacity: 1; }
/* The covers used to be dimmed over their whole area so the sign would read
   over them. The sign is a solid wooden board with its own shadow, so it does
   not need the help, and the dim was costing the picture everything that made
   it worth showing. What is left is a light wash in the top strip only, where
   the header's cream bar meets the art, plus the fade at the bottom. */
.heroArt::before {
  content: ''; position: absolute; inset: 0; z-index: 1;
  /* A flat gradient, NOT backdrop-filter: blurring a full-width hero is a
     compositor job on every frame of the cross-fade, which is expensive on a
     phone and crashed the renderer outright in testing. */
  background: linear-gradient(rgba(26,16,5,.34), rgba(26,16,5,.10) 32%, rgba(26,16,5,0) 55%);
}

/* the sign, lifted out of the painted scene so it can sit still while the
   slideshow moves behind it */
.heroBoard {
  position: absolute; z-index: 2;
  /* one source of truth for the plate's size: the mascots and the hero's top
     padding are both derived from it, so it can be resized in one place */
  top: var(--plate-top); left: 50%; transform: translateX(-50%);
  width: var(--plate-w); height: auto;
  filter: drop-shadow(0 14px 22px rgba(40,24,8,.45));
  pointer-events: none; user-select: none;
}
/* The bottom of the picture dissolves into the page, so the filter scroll never
   sits on a hard edge. 24%, not the 42% it was: at 42% the haze began level
   with the sign and ate the bottom two fifths of every cover, which made the
   scene look shallow.
   This band really is blurred, not just washed out. backdrop-filter over a
   FULL-WIDTH hero was what crashed the renderer in testing - over a 24% strip
   it is a few hundred thousand pixels and costs nothing noticeable. The mask
   ramps the blur in from nothing, so there is no line where it starts; without
   it the blur has a visible top edge of its own. */
.heroArt::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 24%; z-index: 2;
  /* solid cream by 82% of the band, not at its very last pixel: the picture
     runs on behind the filter scroll, and with the scroll rolled up the
     first row of cards starts before the band ends - anything still
     showing there is park scenery behind the cards' own gaps. */
  background: linear-gradient(to bottom, rgba(253,243,223,0), rgba(253,243,223,.55) 50%, #fdf3df 82%);
  -webkit-backdrop-filter: blur(9px);
  backdrop-filter: blur(9px);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 42%);
  mask-image: linear-gradient(to bottom, transparent, #000 42%);
}
/* No portrait variant any more: the backdrop is a slideshow of covers, which
   crops the same either way, and the painted portrait plate carries the sign
   baked into it - behind the sign overlay that would be two signs. */

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

.shelfBar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: 14px;
  padding: 9px clamp(12px, 3vw, 26px);
  background: linear-gradient(rgba(255,252,245,.95), rgba(255,246,228,.88));
  border-bottom: 2px solid rgba(190,150,80,.22);
  box-shadow: 0 6px 20px rgba(90,60,25,.12);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  /* the dock lives in this corner; leave its measured width alone */
  padding-right: calc(clamp(12px, 3vw, 26px) + var(--park-dock-rest-w, 0px));
}
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; text-decoration: none; }
.wordmark {
  height: clamp(26px, 3.4vw, 36px); width: auto; display: block;
  fill: #6b4411;
  filter: drop-shadow(0 2px 0 rgba(255,255,255,.75));
}
.brandDiv { width: 2px; height: 22px; border-radius: 2px; background: rgba(150,110,45,.3); }
.brandZh {
  font-size: clamp(15px, 2vw, 19px); font-weight: 800; letter-spacing: 1px;
  color: #6b4411; white-space: nowrap;
}

.searchWrap { position: relative; flex: 1 1 auto; max-width: 420px; min-width: 0; }
.searchWrap svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); color: #b4914f; pointer-events: none; }
#q {
  width: 100%; height: 42px; padding: 0 14px 0 38px;
  border-radius: 999px; border: 2px solid rgba(190,150,80,.28);
  background: #fffdf7; color: var(--ui-ink);
  box-shadow: inset 0 2px 5px rgba(120,85,30,.1);
  font: inherit; font-size: 14px;
  transition: box-shadow var(--ui-speed-fast), border-color var(--ui-speed-fast);
}
#q::placeholder { color: #a8946f; }
#q:focus { outline: none; border-color: #ffd45e; box-shadow: 0 0 0 4px rgba(245,169,27,.28); }

.barBtns { display: flex; align-items: center; gap: 8px; margin-left: auto; }

/* Wood-plank button: the park's own back/close language. */
.plank {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 40px; padding: 0 14px;
  border: 2px solid #d0a06a; border-radius: var(--ui-radius-sm);
  background: linear-gradient(#a8743f, #7a4f26); color: #ffe9a8;
  font: inherit; font-size: 13px; font-weight: 700; text-decoration: none;
  box-shadow: 0 5px 0 #5a3618; cursor: pointer;
  transition: transform var(--ui-speed-fast), box-shadow var(--ui-speed-fast);
}
.plank:hover { transform: translateY(-1px); box-shadow: 0 6px 0 #5a3618; }
.plank:active { transform: translateY(4px); box-shadow: 0 1px 0 #5a3618; }

/* hidden while the dock is open, so the two cannot sit on top of each other */
#langSel.dockOpen {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 140ms ease, visibility 0s linear 140ms;
}
#langSel {
  height: 42px; padding: 0 12px;
  border-radius: 999px; border: 2px solid rgba(190,150,80,.28);
  background: #fffdf7; color: #6b4411; font: inherit; font-size: 13px; font-weight: 700;
  cursor: pointer;
}

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

.hero {
  position: relative; isolation: isolate;
  display: grid; gap: 12px; justify-items: center; text-align: center;
  /* The sign is a fixed aspect (1397x629 - board plus the two mascots on it),
     so its height is width/2.221 and the tagline can sit directly beneath it
     at any size, with no guessed gap. The -14px tucks the tagline onto the
     board's lower lip instead of leaving a band of empty sky between them;
     the board reaches the bottom of the picture, so that lip IS the bottom.
     The picture is padded on the right so that the BOARD is centred in it
     rather than the group - Popcorn hangs off the left end, and centring the
     group would push the lettering off to the right. */
  --plate-w: min(72vw, 880px);
  --plate-top: clamp(6px, 1vw, 16px);
  --plate-h: calc(var(--plate-w) / 2.221);
  padding: calc(var(--plate-top) + var(--plate-h) - 14px) 16px clamp(22px, 4vh, 40px);
  margin-bottom: -26px;        /* the filter panel laps onto the scene */
}
.hero > *:not(.heroArt):not(.heroBoard) { position: relative; z-index: 2; }

/* The sign is PAINTED INTO the hero picture now (the arch gateway with
   「米花娛樂」 carved on it), so there is no CSS board any more. The heading
   still exists in the markup for screen readers and for search engines - a
   page whose only title is pixels has no title - it is just not drawn twice. */
.heroSign {
  position: relative; z-index: 2;
  display: grid; justify-items: center; gap: 0;
}
.signTitle {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* the tagline rides its own darker plank, hung under the title */
.signTag {
  margin: 0; max-width: 32em; position: relative; z-index: 3;
  padding: 8px clamp(14px, 3vw, 26px);
  border-radius: 999px;
  background: linear-gradient(#6d4420, #4f2f14);
  border: 2.5px solid #c59a62;
  box-shadow: 0 5px 0 #3d2410;
  font-size: clamp(12px, 1.5vw, 15.5px); font-weight: 700;
  color: #ffe9b8;
}

.heroStats { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 14px; z-index: 2; }
.statChip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 16px; border-radius: 999px;
  background: linear-gradient(rgba(60,38,16,.84), rgba(44,27,10,.84));
  border: 2px solid rgba(255,230,160,.4); color: #fff3d8;
  font-size: 13px; font-weight: 800;
  box-shadow: 0 4px 0 rgba(40,24,8,.5);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}
/* a dot in the status colour, so the two counts differ by more than a number */
.statChip::before {
  content: ''; width: 9px; height: 9px; border-radius: 50%;
  background: var(--ui-green); box-shadow: 0 0 0 2px rgba(255,255,255,.3);
}
.statChip.soon::before { background: #f5c043; }
.statChip b { color: #ffd45e; font-size: 15px; }

/* --- filter rail ----------------------------------------------------------- */

.rail {
  /* NOT sticky. It used to pin under the header, which meant the game cards
     slid underneath the scroll as you went down the page - the scroll sitting
     on top of the games. It belongs to the top of the shelf, so it scrolls
     away with it. */
  position: relative; z-index: 20;
  margin: 0 auto; padding: 12px clamp(12px, 3vw, 26px) 4px;
  max-width: 1360px;
}
/* A scroll that really rolls: two wooden rollers with a parchment sheet
   between them. Rolled up, the sheet has no height and the lower roller is
   gone, so what is left is ONE stick with the filter control on it.
   Pure CSS, no image - the sheet stretches to whatever the filter rows need,
   and a bitmap would either tile visibly or need a cache-busted re-export
   every time a genre is added. */
.railCard {
  position: relative;
  display: grid;
  margin: 10px 0 34px;
}

/* the rollers. They overhang the sheet at both ends, like real dowels. */
.rod {
  position: relative; z-index: 2;
  margin: 0 -12px;
  min-height: 46px;
  border-radius: 23px;
  background: linear-gradient(#d3ab69, #a3763a 38%, #7d5323 72%, #5f3d16);
  box-shadow:
    inset 0 3px 0 rgba(255,238,195,.6),
    inset 0 -4px 8px rgba(45,27,6,.5),
    0 6px 14px rgba(60,38,12,.34);
}
/* turned end caps */
.rod::before, .rod::after {
  content: ''; position: absolute; top: 50%; width: 15px; height: 30px;
  transform: translateY(-50%); border-radius: 8px;
  background: linear-gradient(#e0bc82, #a97c3f 55%, #6d4719);
  box-shadow: inset 0 2px 0 rgba(255,240,205,.6), 0 3px 6px rgba(50,30,8,.4);
}
.rod::before { left: -4px; }
.rod::after  { right: -4px; }

.rodTop { padding: 6px clamp(22px, 3.5vw, 42px); }
.rodBottom {
  min-height: 30px; border-radius: 15px;
  transition: min-height var(--ui-speed), opacity var(--ui-speed);
}
/* rolled up: one stick, nothing hanging off it */
.railCard.collapsed .rodBottom { min-height: 0; opacity: 0; pointer-events: none; }

/* the parchment */
.sheet {
  margin: -14px 0;              /* tucks under both rollers */
  padding: 0 clamp(16px, 3vw, 34px);
  background:
    radial-gradient(ellipse at 12% 18%, rgba(176,132,62,.14), transparent 46%),
    radial-gradient(ellipse at 88% 76%, rgba(176,132,62,.13), transparent 48%),
    linear-gradient(#fdf3db, #f8e7c2 55%, #f3dcb0);
  border-left: 2px solid rgba(176,132,62,.3);
  border-right: 2px solid rgba(176,132,62,.3);
  box-shadow:
    inset 0 0 44px rgba(140,98,38,.16),
    inset 0 20px 20px -20px rgba(90,58,18,.5),   /* curling away under the rod */
    inset 0 -20px 20px -20px rgba(90,58,18,.5),
    0 14px 30px rgba(70,46,16,.22);
}
/* no sheet showing at all when it is rolled up */
.railCard.collapsed .sheet {
  box-shadow: none; border-color: transparent; background: none;
}
.fRow { display: flex; align-items: center; gap: 8px; min-width: 0; }
.fLabel {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px; width: 5.2em;
  font-size: 13px; font-weight: 800; color: #6b4d21; letter-spacing: .4px;
}
.fLabel .chipIcon { color: #8a6a35; }
.chips {
  display: flex; gap: 7px; flex-wrap: wrap; min-width: 0;
}
.chip {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 38px; padding: 0 15px;
  border-radius: 999px; border: 2px solid rgba(150,115,55,.28);
  background: rgba(255,252,242,.92); color: #6b4d21;
  font: inherit; font-size: 13.5px; font-weight: 800; white-space: nowrap;
  box-shadow: 0 3px 0 rgba(150,110,40,.13); cursor: pointer;
  transition: transform var(--ui-speed-fast), box-shadow var(--ui-speed-fast),
              background var(--ui-speed-fast), color var(--ui-speed-fast);
}
.chipIcon { display: inline-flex; align-items: center; color: #b1854a; flex: 0 0 auto; }
.chip[aria-pressed="true"] .chipIcon { color: #7a4f12; }
.chip:hover { transform: translateY(-1px); background: #fffdf7; }
.chip:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(150,110,40,.14); }
.chip[aria-pressed="true"] {
  background: linear-gradient(#ffdf7f, #f7ad1d); border-color: #fff0bb; color: #5a3800;
  box-shadow: 0 4px 0 #c9871a;
}
.chip svg { flex: 0 0 auto; }

.railEnd { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.sortLabel { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; }
/* on the roller, the labels are carved into dark wood */
.rodTop .count, .rodTop .sortLabel { color: #ffeec4; text-shadow: 0 1px 2px rgba(40,22,5,.6); }
.rodTop .linkBtn { color: #ffd98a; }
.rodTop #sortSel {
  background: rgba(255,247,228,.92); border-color: rgba(90,58,20,.45); color: #6b4d21;
}

/* --- collapsing the filters ------------------------------------------------
   The head never moves; only the chip rows fold. Animated with grid rows
   rather than max-height, so it works at whatever height the rows happen to
   be (three rows on a desktop, three scrolling rows on a phone) without a
   magic number that clips the last row when a genre is added. */
.railBody {
  display: grid; grid-template-rows: 1fr;
  transition: grid-template-rows var(--ui-speed) cubic-bezier(.3,.7,.3,1);
}
.railBodyInner { display: grid; gap: 10px; min-height: 0; overflow: hidden; padding: 24px 0 26px; }
.railCard.collapsed .railBody { grid-template-rows: 0fr; }
/* visibility, not just height: a chip folded away must leave the tab order too */
.railCard.collapsed .railBodyInner { visibility: hidden; padding: 0; }

.chip.toggle { font-weight: 800; }
.chip.toggle .chev { transition: transform var(--ui-speed); }
.railCard.collapsed .chip.toggle .chev { transform: rotate(-90deg); }
/* how many filters are on, so a collapsed rail still says so */
.fCount {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 19px; height: 19px; padding: 0 5px;
  border-radius: 999px; background: #c9871a; color: #fff8e8;
  font-size: 11.5px; font-weight: 800;
}
.fCount[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .railBody, .chip.toggle .chev { transition: none; }
}
.count { font-size: 13px; font-weight: 800; color: var(--ui-muted); }
#sortSel {
  height: 34px; padding: 0 8px; border-radius: var(--ui-radius-sm);
  border: 2px solid rgba(150,115,55,.22); background: rgba(255,252,244,.95);
  color: #6b5327; font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
}
.linkBtn {
  margin-left: auto;
  border: none; background: none; padding: 6px 4px;
  color: var(--ui-blue); font: inherit; font-size: 13px; font-weight: 800;
  cursor: pointer; border-radius: 8px;
}
.linkBtn[hidden] { display: none; }
.linkBtn:hover { text-decoration: underline; }

/* --- the grid -------------------------------------------------------------- */

.card[hidden], .empty[hidden] { display: none; }

.grid {
  display: grid; gap: clamp(12px, 1.6vw, 20px);
  grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
  max-width: 1360px; margin: 0 auto; padding: 4px clamp(12px, 3vw, 26px) 40px;
}

.card {
  display: flex; flex-direction: column;
  background: var(--card-face);
  border: 2.5px solid #fff; border-radius: var(--ui-radius-md);
  box-shadow: var(--ui-shadow-card);
  overflow: hidden;
  text-decoration: none; color: inherit;
  transition: transform var(--ui-speed), box-shadow var(--ui-speed);
}
.card:hover, .card:focus-visible {
  transform: translateY(-5px);
  box-shadow: 0 22px 46px rgba(63,42,19,.3), inset 0 1px 0 rgba(255,255,255,.8);
}
.card:focus-visible { outline: 3px solid rgba(46,143,214,.78); outline-offset: 2px; }

.shot {
  position: relative; aspect-ratio: 1200 / 630; overflow: hidden;
  background: linear-gradient(135deg, #f6e8c8, #ffe9b8);
}
.shot img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 420ms cubic-bezier(.2,.7,.3,1);
}
.card:hover .shot img { transform: scale(1.055); }
/* A cover that fails to load must not leave a torn alt-text card. */
.shot img[data-failed] { display: none; }

.badge {
  position: absolute; top: 9px; left: 9px;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px; border-radius: 999px;
  font-size: 11.5px; font-weight: 800; letter-spacing: .3px;
}
.badge.open { background: linear-gradient(#6fdc6f, #2faa4a); color: #06340f; border: 2px solid #c6f3c6; }
.badge.soon { background: var(--wood); color: var(--wood-ink); border: var(--wood-edge); }
/* The rating mark. Its colour is the official one for that level - green for
   普遍級, blue for 保護級, orange for 輔12 - the same coding a player sees on
   the game's own starting page, so the badge is recognisable rather than
   decorative. */
.rBadge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 24px; height: 24px; padding: 0 6px;
  border-radius: 7px; border: 2px solid #fff;
  color: #fff; font-size: 12.5px; font-weight: 900; line-height: 1;
  box-shadow: 0 2px 0 rgba(40,25,8,.22);
  flex: 0 0 auto;
}
.rAll    { background: #35a35a; }
.rSix    { background: #2e7fd6; }
.rTwelve { background: #ef8a1f; }
.rBadge.onShot {
  position: absolute; top: 9px; right: 9px;
  min-width: 30px; height: 30px; font-size: 13px;
  box-shadow: 0 3px 8px rgba(30,18,5,.45);
}

.body { display: flex; flex-direction: column; gap: 7px; padding: 12px 14px 14px; flex: 1 1 auto; }
.title {
  margin: 0; font-family: var(--font-display);
  font-size: 18px; font-weight: 800; line-height: 1.3; color: #d8700f;
}
.sub {
  margin: -2px 0 0; font-family: var(--font-display);
  font-size: 12.5px; font-weight: 700; letter-spacing: .4px; color: #9a7f52;
}
.blurb {
  margin: 0; font-size: 13px; line-height: 1.5; color: #6b5327;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.metaRow { display: flex; gap: 6px; flex-wrap: wrap; margin-top: auto; padding-top: 4px; }
.meta {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px; border-radius: 999px;
  background: rgba(246,232,200,.8); border: 1.5px solid rgba(150,115,55,.18);
  font-size: 11.5px; font-weight: 700; color: #6b5327;
}
.meta svg { color: #a8853f; }

.play {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  margin-top: 10px; min-height: 44px; width: 100%;
  border-radius: var(--ui-radius-sm); border: 2px solid #ffedb0;
  background: linear-gradient(#ffe08a, #f5a91b); color: #5a3800;
  font: inherit; font-size: 15px; font-weight: 800;
  box-shadow: 0 6px 0 #c9871a;
  transition: transform var(--ui-speed-fast), box-shadow var(--ui-speed-fast);
}
.card:hover .play { transform: translateY(-1px); box-shadow: 0 7px 0 #c9871a; }
.card:active .play { transform: translateY(5px); box-shadow: 0 1px 0 #c9871a; }

/* Not open yet: the whole card says so, and it is not a link. */
.card.isSoon { opacity: .9; }
.card.isSoon .shot img { filter: grayscale(.65) saturate(.7); }
.card.isSoon:hover { transform: none; box-shadow: var(--ui-shadow-card); }
.card.isSoon .play {
  background: rgba(246,232,200,.9); border-color: rgba(150,115,55,.2);
  color: #93805f; box-shadow: 0 3px 0 rgba(150,110,40,.14); cursor: default;
}
.card.isSoon:hover .play { transform: none; box-shadow: 0 3px 0 rgba(150,110,40,.14); }

/* entrance stagger — index set per card in shelf.js */
.card { animation: cardIn 380ms cubic-bezier(.2,.8,.3,1) both; animation-delay: calc(var(--i, 0) * 26ms); }
@keyframes cardIn { from { opacity: 0; transform: translateY(14px) scale(.985); } to { opacity: 1; transform: none; } }

/* --- empty state ----------------------------------------------------------- */

.empty {
  grid-column: 1 / -1;
  display: grid; gap: 12px; justify-items: center;
  padding: 44px 20px;
  background: var(--card-face); border: 2.5px solid #fff; border-radius: var(--ui-radius-md);
  box-shadow: var(--ui-shadow-card);
  text-align: center;
}
.empty p { margin: 0; font-size: 15px; font-weight: 700; color: var(--ui-muted); }
.empty svg { color: #d8c08a; }

/* --- park door ------------------------------------------------------------- */

.parkDoor {
  max-width: 1360px; margin: 0 auto 30px; padding: 0 clamp(12px, 3vw, 26px);
}
.parkDoorInner {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 14px 18px;
  background: var(--wood); border: var(--wood-edge); border-radius: var(--ui-radius-md);
  color: var(--wood-ink);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.parkDoorText { display: grid; gap: 2px; min-width: 0; }
.parkDoorText strong { font-size: 15px; font-weight: 800; }
.parkDoorText span { font-size: 12.5px; color: #e8d6ae; }
.parkDoor .plank { margin-left: auto; }

/* --- about / directory / FAQ (static HTML from tools/build-seo.mjs) --------
   The text a crawler reads when it does not run the shelf's JS. It is real
   page content, so it is styled as a card like everything else, not hidden.
   System face, not Huninn: these words are outside the font's subset. */

.about {
  max-width: 1360px; margin: 0 auto 30px; padding: 0 clamp(12px, 3vw, 26px);
}
.aboutCard {
  padding: clamp(18px, 3vw, 30px);
  background: var(--card-face); border: 2.5px solid #fff; border-radius: var(--ui-radius-md);
  box-shadow: var(--ui-shadow-card);
  color: var(--ui-ink);
}
.aboutCard h2 {
  margin: 26px 0 12px; font-size: 18px; font-weight: 800; color: #6b4411;
}
.aboutCard h2:first-child { margin-top: 0; }
.aboutLead { margin: 0; max-width: 72ch; font-size: 14.5px; line-height: 1.75; color: var(--ui-muted); }

.dir {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.dir li {
  padding: 12px 14px; border-radius: var(--ui-radius-sm);
  background: var(--ui-cream-1); border: 1.5px solid var(--soc-edge);
}
.dir a {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  color: var(--ui-ink); text-decoration: none;
}
.dir a:hover strong, .dir a:focus-visible strong { color: var(--ui-gold-dark); text-decoration: underline; }
.dir strong { font-size: 15px; font-weight: 800; }
.dirBrand { font-size: 11.5px; font-weight: 700; letter-spacing: .04em; color: var(--ui-muted); }
.dirMeta { display: block; margin-top: 3px; font-size: 12px; font-weight: 700; color: var(--ui-gold-dark); }
.dir p { margin: 5px 0 0; font-size: 13px; line-height: 1.6; color: var(--ui-muted); }

.faq { display: grid; gap: 8px; }
.faq details {
  border-radius: var(--ui-radius-sm); background: var(--ui-cream-1); border: 1.5px solid var(--soc-edge);
}
.faq summary {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 14px; cursor: pointer; list-style: none;
  font-size: 14.5px; font-weight: 800;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: ''; flex: none; width: 8px; height: 8px;
  border-right: 2.5px solid var(--ui-gold-dark); border-bottom: 2.5px solid var(--ui-gold-dark);
  transform: rotate(45deg) translateY(-2px); transition: transform var(--ui-speed-fast);
}
.faq details[open] summary::after { transform: rotate(-135deg) translateY(-2px); }
.faq summary:focus-visible { outline: 2px solid var(--ui-gold); outline-offset: 2px; border-radius: var(--ui-radius-sm); }
.faq details p { margin: 0; padding: 0 14px 14px; font-size: 13.5px; line-height: 1.75; color: var(--ui-muted); }

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

.foot {
  max-width: 1360px; margin: 0 auto; padding: 0 clamp(12px, 3vw, 26px) 90px;
  display: grid; gap: 6px; justify-items: center; text-align: center;
}
.foot p { margin: 0; font-size: 12px; color: var(--ui-muted); }

/* --- footer attribution ----------------------------------------------------- */
.poweredBy {
  margin-top: 24px !important; padding-top: 20px;
  border-top: 2px solid rgba(150,115,55,.18); width: min(100%, 520px);
  font-size: 13px; color: #806d4d;
}
.poweredBy strong { color: #6b4411; font-weight: 800; }
.footCopy { margin-top: 6px !important; font-size: 11.5px; color: #9a8560; }
.foot .legend { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.foot .legend span { display: inline-flex; align-items: center; gap: 5px; font-weight: 700; }


/* --- responsive ------------------------------------------------------------ */

@media (max-width: 820px) {
  .shelfBar { flex-wrap: wrap; gap: 9px; padding-bottom: 9px; }
  .brand { order: 1; }
  .barBtns { order: 2; }
  .searchWrap { order: 3; flex: 1 0 100%; max-width: none; }
  .rail { position: static; padding-top: 8px; }
  /* One scrolling container on a phone: the chip rows scroll sideways, and
     nothing else may capture the vertical gesture.
     overflow-y is `hidden`, NOT `visible`: a box with one axis set to auto
     computes `visible` on the other axis to `auto` as well, which would make
     every chip row a vertical scroll container and swallow the page's own
     scroll gesture — the 2026-09 lobby bug, one level down. */
  .chips { flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; padding-bottom: 3px; scrollbar-width: none; }
  .chips::-webkit-scrollbar { display: none; }
  .fLabel { width: 3.2em; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
  .foot { padding-bottom: 110px; }   /* the dock is fixed to the bottom here */
}

@media (max-width: 480px) {
  .grid { grid-template-columns: 1fr; }
  .chip { min-height: 44px; }        /* touch target */
  .heroTitle { font-size: 25px; }
}

/* Phone held sideways is decided by height, not width: 844x390 is wider than
   820 and would otherwise be treated as a desktop. */
@media (max-height: 520px) {
  /* shrink the board rather than the padding: the padding is derived from the
     board's height, so overriding it alone would drop the board on top of the
     tagline. The picture stops running on past the hero as well: a 844x390
     phone would give it 443px inside a 390px window, which is a first screen
     you cannot see the end of. */
  .hero { --plate-w: min(42vw, 400px); }
  .heroArt { height: 100%; }
  .heroStats { display: none; }
  .rail { position: static; }
}

@media (prefers-reduced-motion: reduce) {
  .card, .shot img, .chip, .plank, .play { transition: none; animation: none; }
  .card:hover { transform: none; }
  .card:hover .shot img { transform: none; }
}
