/* Noto Serif Thai, self-hosted.

   The stylesheet asked for this face from the start and nothing ever served
   it, so Thai fell to whatever the system had while Latin and the digits fell
   to Georgia — two unrelated faces inside a single title, and Georgia's
   old-style figures sitting at a different height from the Thai beside them.

   Served from this origin rather than a font CDN: it would otherwise be the
   only third-party request the site makes, the only asset whose failure to
   arrive changes the page, and the only thing telling someone else who is
   reading. The subsets and their unicode ranges are Google's own, so a
   browser still downloads only the blocks a page actually uses.

   Licensed under the SIL Open Font License 1.1 — see
   sites/a/noto-serif-thai-OFL.txt, which ships beside the fonts as the
   licence requires. */
@font-face {
  font-family: "Noto Serif Thai";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/sites/a/noto-serif-thai-400-thai.woff2") format("woff2");
  unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
}

@font-face {
  font-family: "Noto Serif Thai";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/sites/a/noto-serif-thai-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Noto Serif Thai";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/sites/a/noto-serif-thai-700-thai.woff2") format("woff2");
  unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
}

@font-face {
  font-family: "Noto Serif Thai";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/sites/a/noto-serif-thai-700-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Light is the printed-paper reading; dark is the same page in ink. Both are
   warm on purpose — a neutral grey dark mode would break the editorial feel
   the serif face and paper ground establish. */
:root {
  --ink: #1c1a17;
  --muted: #6b6560;
  --line: #e2ddd5;
  --paper: #faf7f1;
  --card: #ffffff;
  --sunk: #f2ede4;
  --shadow: 0 1px 2px rgba(28, 26, 23, 0.06);
  --shadow-lift: 0 10px 26px rgba(28, 26, 23, 0.13);
  /* The accent arrives from the server as a fixed hex, so it cannot be
     restated per theme. Mixing toward white lifts it to a readable weight on
     the dark ground; browsers without color-mix fall back to the raw accent,
     which is dimmer but still legible. */
  /* The raw accent is a display colour, not a text colour: white on it
     measures 3.87:1 and it measures 3.87:1 on white, both short of the 4.5
     that text needs. Darkened it clears AA in every place it carries meaning
     (5.63:1 either way) and still reads as the same coral. The raw accent
     stays available for large display type. */
  --accent-text: color-mix(in srgb, var(--accent, #d85a30) 80%, #000000);
  --accent-strong: color-mix(in srgb, var(--accent, #d85a30) 80%, #000000);
  --on-accent: #ffffff;
  /* --line is a hairline at 1.26:1 against its ground, which is fine for a
     divider and far short of the 3:1 a control's own boundary needs. */
  --line-strong: #948d86;
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ink: #f0ebe1;
    --muted: #a49a8d;
    --line: #322c25;
    --paper: #14120f;
    --card: #1e1b16;
    --sunk: #100e0c;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-lift: 0 12px 30px rgba(0, 0, 0, 0.55);
    --accent-text: color-mix(in srgb, var(--accent, #d85a30) 74%, #ffffff);
    /* On the dark ground the raw accent is already a 4.83:1 button against
       paper-coloured text, so it needs no darkening — only its label flips. */
    --accent-strong: var(--accent, #d85a30);
    --on-accent: #14120f;
    --line-strong: #76716b;
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --ink: #f0ebe1;
  --muted: #a49a8d;
  --line: #322c25;
  --paper: #14120f;
  --card: #1e1b16;
  --sunk: #100e0c;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-lift: 0 12px 30px rgba(0, 0, 0, 0.55);
  --accent-text: color-mix(in srgb, var(--accent, #d85a30) 74%, #ffffff);
  --accent-strong: var(--accent, #d85a30);
  --on-accent: #14120f;
  --line-strong: #76716b;
  color-scheme: dark;
}

:root {
  --ease: cubic-bezier(0.2, 0, 0, 1);
  --dur-fast: 120ms;
  --dur: 220ms;
  --dur-slow: 420ms;
}

html { scroll-behavior: smooth; }

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: "Noto Serif Thai", Georgia, serif;
  line-height: 1.7;
}

a { color: inherit; }

/* Site B is a lit poster wall and site C a printed journal. This one is the
   paper itself: a warm wash falling from the top edge and a grain fine enough
   to read as stock rather than as an effect. Both are drawn — the grain is an
   inline SVG — so the texture costs no request and cannot fail to arrive. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--accent, #d85a30) 5%, transparent),
    transparent 320px
  );
}

body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.025;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='4'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23p)'/%3E%3C/svg%3E");
}

.page { max-width: 1100px; margin: 0 auto; padding: 24px 20px 64px; }

.site-head {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}

.masthead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  max-width: 1100px;
  margin: 0 auto;
  padding: 18px 20px 12px;
}

.masthead-aside { display: flex; align-items: center; gap: 16px; }

.site-name { text-decoration: none; display: grid; }
.site-name strong {
  font-weight: 700;
  font-size: 21px;
  letter-spacing: 0.01em;
  color: var(--accent-text);
}
.site-name span { font-size: 13px; color: var(--muted); }
/* Icon buttons rather than three words of running text. As links they were
   41x22, under any touch minimum and reading as body copy in the middle of the
   masthead; as circles they are a control, and they say what they are through
   the link's own accessible name rather than a visible label. */
.site-social { display: flex; gap: 8px; }
.site-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--line-strong, var(--line));
  color: var(--muted);
  text-decoration: none;
  font-size: 13px;
}
.site-social a:hover { color: var(--ink); border-color: var(--ink); }
.site-social svg { display: block; }

.theme-toggle {
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.04em;
  padding: 5px 12px;
  border-radius: 999px;
  cursor: pointer;
}
.theme-toggle:hover { color: var(--ink); border-color: var(--muted); }
/* The label names the mode the click will switch TO, so it stays meaningful
   without any script reading the current state. */
.theme-toggle__light { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle__dark { display: none; }
  :root:not([data-theme="light"]) .theme-toggle__light { display: inline; }
}
:root[data-theme="dark"] .theme-toggle__dark { display: none; }
:root[data-theme="dark"] .theme-toggle__light { display: inline; }
:root[data-theme="light"] .theme-toggle__dark { display: inline; }
:root[data-theme="light"] .theme-toggle__light { display: none; }

.site-nav {
  display: flex;
  align-items: stretch;
  gap: 4px;
  flex-wrap: wrap;
  max-width: 1100px;
  margin: 0 auto;
  padding: 0 20px;
  border-top: 1px solid var(--line);
  font-size: 14px;
}
.site-nav__spacer { flex: 1 1 auto; }

.site-nav > a,
.nav-menu > summary {
  display: inline-flex;
  align-items: center;
  padding: 11px 12px;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  white-space: nowrap;
}
.site-nav > a:hover,
.nav-menu > summary:hover { color: var(--ink); }
.site-nav > a[aria-current="page"],
.nav-menu[data-active="true"] > summary {
  color: var(--ink);
  border-bottom-color: var(--accent-text);
}
/* The nav's own `> a` rule is (0,1,1); these are (0,2,0) and so win the
   padding and the 2px transparent underline every other nav item carries. */
.site-nav .site-adult-link {
  display: inline-flex;
  align-items: center;
  align-self: center;
  gap: 8px;
  min-height: 36px;
  padding: 4px 14px 4px 5px;
  border: 1px solid var(--line-strong, var(--line));
  border-radius: 999px;
  color: var(--muted);
  font-size: 13px;
  white-space: nowrap;
}
.site-nav .site-adult-link:hover { color: var(--ink); border-color: var(--ink); }

.site-adult-link__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  height: 26px;
  padding: 0 8px;
  border-radius: 999px;
  background: var(--sunk);
  color: var(--ink);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* Going back is an action, not a boundary, so it drops the badge padding and
   leads with the direction it moves in. */
.site-nav .site-adult-link--back { padding: 4px 14px; }
.site-adult-link svg { display: block; }

.nav-menu { position: relative; }
.nav-menu > summary { list-style: none; }
.nav-menu > summary::-webkit-details-marker { display: none; }
.nav-menu > summary::after {
  content: "";
  margin-left: 7px;
  border: 4px solid transparent;
  border-top-color: currentColor;
  transform: translateY(2px);
}
.nav-menu[open] > summary::after { transform: translateY(-2px) rotate(180deg); }

.nav-menu__panel {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 30;
  display: grid;
  min-width: 200px;
  max-width: min(320px, calc(100vw - 32px));
  max-height: min(60vh, 420px);
  overflow-y: auto;
  padding: 6px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--shadow);
}
.nav-menu__panel a {
  padding: 8px 10px;
  border-radius: 6px;
  color: var(--ink);
  text-decoration: none;
  font-size: 14px;
}
.nav-menu__panel a:hover { background: var(--sunk); }

@media (max-width: 640px) {
  .site-head { position: static; }
  .masthead { padding: 14px 16px 10px; }
  .site-nav { padding: 0 8px; font-size: 13px; }
  .site-nav > a,
  .nav-menu > summary { padding: 10px 8px; }
  .site-nav__spacer { display: none; }
}

h1 { font-size: 30px; line-height: 1.35; margin: 0 0 8px; text-wrap: balance; }

/* A magazine sets a section by name and a rule, not by a bigger font alone. */
h2 {
  font-size: 19px;
  margin: 40px 0 16px;
  display: flex;
  align-items: center;
  gap: 14px;
}
h2::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--line-strong, var(--line)), transparent);
}

.intro { color: var(--muted); margin: 0 0 20px; }

.shelf { margin-bottom: 36px; }

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

/* Sticky, so every control stays beside what it changes. The grid below runs
   tens of thousands of pixels, and a filter you have to scroll back up to is
   the same problem as one buried below the shelves. --head-h is published by
   the script from the masthead's real height; the toolbar itself only exists
   once the script runs, so there is no un-scripted state to get this wrong. */
/* It carried the same card, border and radius as the works below it, so the
   page opened on a content-shaped panel holding no content. A utility strip
   with one rule under it reads as chrome, which is what it is. It stays
   opaque: it is sticky, and the grid scrolls beneath it. */
.catalog-tools {
  position: sticky;
  top: var(--head-h, 0px);
  z-index: 10;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  margin: 0 0 28px;
  padding: 12px 0;
  background: var(--paper);
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
}

.tools-search { flex: 1 1 220px; display: flex; }
.tools-search input {
  width: 100%;
  font: inherit;
  font-size: 15px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line-strong, var(--line));
  border-radius: 999px;
  padding: 8px 16px;
}
.tools-search input::placeholder { color: var(--muted); }
.tools-search input:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 1px; }

.tools-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; }
.tools-group { display: flex; flex-wrap: wrap; gap: 6px; }
.tools-group button {
  font: inherit;
  font-size: 13px;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--line-strong, var(--line));
  border-radius: 999px;
  padding: 6px 13px;
  cursor: pointer;
}
.tools-group button:hover { color: var(--ink); border-color: var(--muted); }
.tools-group button[aria-checked="true"] {
  color: var(--on-accent, #fff);
  background: var(--accent-strong, var(--accent, #d85a30));
  border-color: var(--accent-strong, var(--accent, #d85a30));
}
/* Selecting the catch-all is not a selection — it is the absence of one. Given
   the accent fill it was the loudest mark on a page where nothing had been
   narrowed, which is exactly backwards. */
.tools-group button[aria-checked="true"][data-value=""] {
  color: var(--ink);
  background: var(--sunk);
  border-color: var(--line-strong, var(--line));
}

.tools-sort select {
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line-strong, var(--line));
  border-radius: 999px;
  padding: 7px 12px;
  cursor: pointer;
}

.tools-count { margin: 0; font-size: 13px; color: var(--muted); white-space: nowrap; }

/* On a phone the toolbar was four stacked rows inside a bordered panel — 199px
   of controls, and together with the masthead 46% of the screen before any
   work appeared. It reads as a utility strip here rather than a content card,
   and every chip lives on one row the reader can push sideways. */
@media (max-width: 640px) {
  .catalog-tools {
    /* It is sticky, so it has to stay opaque: the grid scrolls under it. */
    background: var(--paper);
    border: 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    padding: 10px 0;
    gap: 8px 10px;
  }
  .tools-search { flex-basis: 100%; }

  .tools-filters {
    flex: 1 1 100%;
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    /* Bleeds to the page edge so the row reads as continuing past it, then
       pads itself back so the first and last chip still clear the margin. */
    margin: 0 -20px;
    padding: 0 20px;
  }
  .tools-filters::-webkit-scrollbar { display: none; }
  .tools-group { flex-wrap: nowrap; }
  .tools-group button { white-space: nowrap; }
  /* Running as one row, the two groups butt together and "ทุกสถานะ" reads as
     another kind. The rule is the only thing left saying where one ends. */
  .tools-filters > .tools-group + .tools-group {
    margin-left: 4px;
    padding-left: 14px;
    border-left: 1px solid var(--line);
  }

  /* Both are under the 44px minimum at their desktop size. */
  .tools-group button,
  .tools-sort select {
    min-height: 44px;
    padding-block: 0;
  }
}

/* The curated shelves are a browse view, not the listing. Wrapped into a full
   grid the three of them ran thousands of pixels tall and pushed the real
   catalogue off screen. One scrollable row each keeps the whole page skimmable
   and puts the full grid back within reach. The column width leaves part of a
   card visible at the edge on narrow screens, which is what tells a visitor
   the row scrolls. */
.card-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: clamp(140px, 42vw, 190px);
  gap: 16px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
  /* Room for the scrollbar so it never sits on top of the card borders. */
  padding-bottom: 10px;
}
.card-row > .card { scroll-snap-align: start; }
.card-row::-webkit-scrollbar { height: 8px; }
.card-row::-webkit-scrollbar-track { background: transparent; }
.card-row::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
.card-row:hover::-webkit-scrollbar-thumb { background: var(--muted); }

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
}

/* A 160px floor needs 336px of content box for two columns, so every phone at
   or below 375px — the most common Android width among them — fell to a single
   column and a page tens of thousands of pixels tall. 320px less the page's
   40px of padding leaves 280, so with a 12px gap the floor cannot exceed 134.
   
   This override has to sit after the rule it overrides. It first shipped above
   it, where equal specificity meant the base rule won and none of this applied
   at any width — which went unnoticed because the test asserted the
   stylesheet contained the text, not that a rendered grid had two columns. */
@media (max-width: 640px) {
  .card-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(132px, 100%), 1fr));
    gap: 12px;
  }
}

.card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
.card a { text-decoration: none; display: block; }
.card-cover { display: block; aspect-ratio: 3 / 4; background: var(--sunk); overflow: hidden; }
.card-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-body { display: grid; gap: 4px; padding: 10px 12px 14px; }
/* Two values across the whole catalogue, printed above every title in the
   one saturated colour on the page: the loudest mark was carrying about one
   bit. Muted by default; only "finished" — the state that actually changes
   what a reader decides — keeps the accent. */
.card-status { font-size: 12px; color: var(--muted); }
.card[data-status="complete"] .card-status { color: var(--accent-text); }
.card-score { font-size: 12px; color: var(--muted); font-weight: 400; }
/* Titles run from a few words to 105 characters, and an unclamped one made
   its whole grid row as tall as itself — cards ranged 95px to 225px, so no
   two rows shared a baseline and a short title sat above 90px of nothing.
   Two lines are reserved whether or not they are used, which is what makes
   every card the same height. The full title is the <h1> one click away. */
.card-body strong {
  font-size: 15px;
  line-height: 1.45;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  min-height: calc(2 * 1.45em);
}
.card-meta { font-size: 12px; color: var(--muted); }

/* A Gaussian blur spreads roughly three times its radius, so an 18px blur
   reaches about 54px past any edge — but the image was only scaled 8%, giving
   it 7.5px of overhang inside a card cover. The outer band of every cover was
   therefore mixing with the transparent area beyond the image and fading out,
   which both weakened the obscuring where a figure usually sits and read as a
   broken image. Scaling further pushes that fade outside the visible box. */
.cover--blurred img { filter: blur(24px); transform: scale(1.35); }
.cover--blurred { position: relative; }

/* Without this a blurred cover is indistinguishable from one that failed to
   load. The banner explaining it sits at the top of the page and scrolls away;
   the badge stays with the cover it describes. Scoped to the two large covers
   — the 64px paired-edition thumbnail would be swallowed by it. */
.card-cover.cover--blurred::after,
.detail-cover.cover--blurred::after {
  content: "18+";
  position: absolute;
  right: 8px;
  bottom: 8px;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 11px;
  letter-spacing: 0.04em;
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line-strong, var(--line));
}

/* Two columns could not work here. A cover is 3:4, and most summaries are a
   short paragraph, so at any width worth showing the art the cover hangs below
   the text beside it — 112px at 280px wide, and further the larger it gets.
   Cropping is not the answer either: the art is the thing being catalogued.
   
   So the text runs around it, the way a magazine sets a plate into a column.
   The cover keeps its full ratio at a size worth looking at, the facts and the
   reader links fill the space beside and under it, and the page has no L in
   it at any summary length. */
.detail-grid {
  display: block;
  max-width: 78ch;
}
/* The cover sits inside an <aside>, so that is what floats. Floating the
   span instead leaves the aside a full-width block and the cover fills it. */
.detail-grid > aside {
  float: inline-start;
  width: 300px;
  margin: 4px 30px 20px 0;
}
.summary { font-size: 17px; }
/* Flex and grid containers establish their own formatting context, so these
   sit beside the float rather than under it — which is what keeps the facts
   from stacking into a narrow ribbon. */
.detail-copy { min-width: 0; }
.detail-cover { display: block; aspect-ratio: 3 / 4; border-radius: 12px; overflow: hidden; background: var(--sunk); }
.detail-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.detail-title { margin-bottom: 4px; }
.title-en { margin: 0 0 20px; color: var(--muted); font-size: 15px; }
.summary { margin: 0 0 16px; }
.tags { display: flex; flex-wrap: wrap; gap: 8px; font-size: 13px; color: var(--muted); margin-bottom: 20px; }
/* No work carries tags yet, and the row rendered anyway: 20px of margin
   between the summary and the facts that read as loose spacing rather than as
   an empty element. */
.tags:empty { display: none; }
.tags span {
  padding: 3px 10px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
}

.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin: 0 0 28px; }
.facts dt { font-size: 12px; color: var(--muted); }
.facts dd { margin: 2px 0 0; font-size: 15px; }

.reader-row { display: flex; flex-wrap: wrap; gap: 10px; }
.reader-link {
  display: inline-block;
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--accent-strong, var(--accent, #d85a30));
  color: var(--on-accent, #fff);
  text-decoration: none;
  font-size: 15px;
}
.reader-empty { color: var(--muted); }

.pair {
  display: flex;
  gap: 14px;
  align-items: center;
  margin-top: 28px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  text-decoration: none;
}
.pair-cover { display: block; width: 64px; flex: 0 0 64px; aspect-ratio: 3 / 4; border-radius: 6px; overflow: hidden; background: var(--sunk); }
.pair-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pair span { display: grid; gap: 2px; }
.pair small { font-size: 12px; color: var(--muted); }
.pair strong { font-size: 15px; }
.pair b { font-size: 13px; font-weight: 400; color: var(--accent-text); }

.similar { margin-top: 40px; }

.age-gate button {
  margin-top: 12px;
  padding: 12px 22px;
  border: 0;
  border-radius: 999px;
  background: var(--accent-strong, var(--accent, #d85a30));
  color: var(--on-accent, #fff);
  font: inherit;
  cursor: pointer;
}
.age-note { color: var(--muted); font-size: 13px; margin-top: 16px; }

/* An empty result used to be one grey line, with no way to tell whether the
   search box or a chip had emptied it and no single control to undo either. */
.empty [data-catalog-why] { color: var(--ink); }
.empty button {
  margin-top: 12px;
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--line-strong, var(--line));
  border-radius: 999px;
  padding: 9px 18px;
  cursor: pointer;
}
.empty button:hover { border-color: var(--ink); }

.age-form { margin: 0; }
.age-error {
  margin: 10px 0 0;
  font-size: 13px;
  color: var(--accent-text, var(--accent, #d85a30));
}

.age-banner {
  padding: 16px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 12px;
  margin-bottom: 20px;
  font-size: 14px;
  color: var(--ink);
}

.age-banner button {
  display: block;
  margin-top: 12px;
  padding: 10px 18px;
  border: 0;
  border-radius: 999px;
  background: var(--accent-strong, var(--accent, #d85a30));
  color: var(--on-accent, #fff);
  font: inherit;
  cursor: pointer;
  font-size: 14px;
}

.empty { color: var(--muted); }
.site-foot { max-width: 1100px; margin: 0 auto; padding: 24px 20px 48px; border-top: 1px solid var(--line); display: flex; gap: 16px; font-size: 13px; color: var(--muted); }

@media (max-width: 700px) {
  .detail-grid > aside {
    float: none;
    width: 56%;
    max-width: 220px;
    margin: 0 0 18px;
  }
  h1 { font-size: 24px; }
}


/* ---------------------------------------------------------------------------
   Motion.

   Two rules govern everything here. First, an animation must end where the
   static page already was: each one either transitions between states the CSS
   already had, or runs once from an opening frame back to the element's own
   values. Nothing depends on a script to become visible, so a failed script,
   a dead observer or a blocked asset leaves the page exactly as it renders
   today rather than blank.

   Second, no scroll-triggered reveal on the catalogue. Fading 130 cards in as
   the reader scrolls makes browsing feel slower than it is, which is the
   opposite of what this page needs, and it is the pattern that fails worst
   when its observer never runs.
   --------------------------------------------------------------------------- */

.card {
  transition:
    transform var(--dur) var(--ease),
    box-shadow var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}
.card:hover,
.card:focus-within {
  transform: translateY(-4px);
  border-color: var(--line-strong, var(--line));
  box-shadow: var(--shadow-lift);
}

/* Blurred adult covers are scaled 1.35 so the blur's own fade stays outside
   the frame. A hover scale would replace that transform and pull the fade
   back into view, so they keep still. */
.card-cover:not(.cover--blurred) img {
  transition: transform var(--dur-slow) var(--ease);
}
.card:hover .card-cover:not(.cover--blurred) img {
  transform: scale(1.05);
}

.site-nav > a,
.nav-menu > summary,
.nav-menu__panel a,
.tools-search input,
.tools-group button,
.tools-sort select,
.theme-toggle,
.empty button,
.reader-link,
.pair,
.site-social a {
  transition:
    background-color var(--dur-fast) var(--ease),
    border-color var(--dur-fast) var(--ease),
    color var(--dur-fast) var(--ease);
}

.reader-link:hover { filter: brightness(1.08); }
.pair:hover { border-color: var(--line-strong, var(--line)); }
.nav-menu > summary::after { transition: transform var(--dur) var(--ease); }

/* A disclosure panel has no state to transition from — it is displayed or it
   is not — so it gets a one-shot animation instead. */
.nav-menu[open] > .nav-menu__panel {
  animation: menu-in var(--dur) var(--ease) both;
}
@keyframes menu-in {
  from { opacity: 0; transform: translateY(-6px); }
}

/* The toolbar earns an edge only once it is actually holding position over
   the grid, which is the moment it stops reading as part of the page. */
.catalog-tools {
  transition:
    box-shadow var(--dur) var(--ease),
    border-color var(--dur) var(--ease);
}
.catalog-tools.is-pinned {
  border-bottom-color: var(--line-strong, var(--line));
  box-shadow: 0 10px 18px -14px rgba(28, 26, 23, 0.5);
}

/* Arrival. `both` holds the opening frame through the delay; with only a
   `from` frame the animation resolves to the element's own values, so the
   page settles into exactly what it would have rendered anyway. */
@keyframes rise {
  from { opacity: 0; transform: translateY(10px); }
}

.shelf { animation: rise var(--dur-slow) var(--ease) both; }
.shelf:nth-of-type(2) { animation-delay: 70ms; }
.shelf:nth-of-type(3) { animation-delay: 140ms; }
.shelf:nth-of-type(n + 4) { animation-delay: 210ms; }

/* Only the first screenful is staggered — animating all 130 cards costs more
   than it says. */
.card-row > .card:nth-child(-n + 8),
.card-grid > .card:nth-child(-n + 12) {
  animation: rise var(--dur-slow) var(--ease) both;
}
.card-row > .card:nth-child(2), .card-grid > .card:nth-child(2) { animation-delay: 40ms; }
.card-row > .card:nth-child(3), .card-grid > .card:nth-child(3) { animation-delay: 80ms; }
.card-row > .card:nth-child(4), .card-grid > .card:nth-child(4) { animation-delay: 120ms; }
.card-row > .card:nth-child(5), .card-grid > .card:nth-child(5) { animation-delay: 160ms; }
.card-row > .card:nth-child(n + 6), .card-grid > .card:nth-child(n + 6) { animation-delay: 200ms; }

/* Covers arrive one at a time over a slow connection, and 145 of them popping
   in is the jumpiest thing on the page. The class is added by the script only
   after its load handler is attached, so an image can never be left holding
   opacity 0 with nothing listening for it. */
.card-cover img,
.detail-cover img {
  transition: opacity var(--dur-slow) var(--ease);
}
.card-cover img.is-loading,
.detail-cover img.is-loading { opacity: 0; }

/* The whole file, off. Reduced motion is a preference about vestibular
   comfort, not a style, so it wins over every rule above. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }
  .card:hover,
  .card:focus-within { transform: none; }
  .card:hover .card-cover:not(.cover--blurred) img { transform: none; }
}
