/* Mistriapedia
 *
 * SPRITES ARE REAL GAME PIXEL ART. Every sprite selector carries
 * `image-rendering: pixelated`, and nothing here may ever set it to `auto`: these are
 * 16px and 32px source images scaled up, and smoothing turns them to mush.
 * Scale factors stay INTEGER multiples for the same reason.
 */

:root {
  --ink: #3b2a2f;
  --ink-soft: #6b5257;
  --paper: #fdf6ec;
  --panel: #fffaf3;
  --line: #e4d3c2;
  --accent: #b5546b;
  --accent-soft: #f6e2e6;
  --leaf: #6a8f5f;
  /* The ramp's own two ends, and where it stops ramping. Below --sky-h the page holds
   * --sky-end, so a long page does not run out of gradient. */
  --sky-start: #65bae3;
  --sky-end: #77cfbe;
  --sky-h: 1400px;
  /* One dither cell. The art is 16px to the tile, so 3px keeps the cells reading as
   * pixels rather than as a texture. --sky-tile is derived in the generated sky.css. */
  --sky-cell: 3px;
  /* Sampled from the LAST ROW of branding/4.png, so the footer continues the grass
   * exactly instead of approximately. */
  --grass: #4fa760;
  /* The wooden container frame. Insets are DERIVED in scripts/slice-frame.py from the
   * art itself, not typed: the base is deeper than the top and carries its own detail
   * row, so one symmetric inset would either eat the base or leak border into the fill.
   * Source is 66x34 with 4/4/6/4 insets, emitted at 2x, hence 8/8/12/8 here. */
  /* How far the ground strip rides up behind the content. */
  /* How far the scene rides up behind the content. Kept modest: main is opaque, so
   * everything above this line is hidden, and overlapping too far buries the clouds
   * along with it. */
  --ground-overlap: clamp(90px, 12vw, 200px);
  --frame-fill: #fffeeb;
  --notebook-fill: #fffeeb;
  /* Both button states are real art, so the pressed look is never faked with a filter.
   * Their border widths sum to the same 22px, so the box does not reflow when pressed. */
  --btn-fill: #fffeeb;
  --btn-fill-pressed: #fbd9cd;
  --frame-top: 8px;
  --frame-side: 8px;
  --frame-bottom: 12px;
  --shadow: 0 1px 0 var(--line);
  --radius: 10px;
  --wrap: 1140px;
}

* { box-sizing: border-box; }

/* The backstop. On a page shorter than the viewport the footer does not reach the
 * bottom, and without this you get a strip of sky below it. */
html { background: var(--paper); }

body {
  margin: 0;
  /* Below the ramp the page holds the gradient's end colour. */
  background: var(--sky-end);
  color: var(--ink);
  font: 16px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  min-height: 100vh;
  /* .scene sits at z-index -1. Without the isolation it lands in the ROOT stacking
   * context and body's own background paints straight over it. */
  position: relative;
  isolation: isolate;
}

/* ---------- the ground strip ---------- */
/* All three layers are the same 576x324 canvas, so they share an origin: full width,
 * anchored to the BOTTOM. Anchor them any other way and the horizon tears where the
 * hills meet the grass. */
.ground {
  position: relative;
  /* Behind the content, which carries z-index 1. The strip's upper region is genuinely
   * transparent, so cards float over sky rather than over artwork. */
  z-index: 0;
  width: 100%;
  /* The art is 576x324, so the strip keeps that ratio and the grass always meets the
   * footer on its own last row.
   *
   * NO max-height HERE. The layers are bottom-anchored, so clipping the container cuts
   * the TRANSPARENT TOP off the clouds layer and leaves the visible edge sitting in
   * #bce1f1, the opaque haze that starts at row 214 of 2.png. That is what produced the
   * hard horizontal line across the page. The fade below is a second guard on the same
   * thing, so no viewport size can reintroduce it. */
  aspect-ratio: 576 / 324;
  /* Pulled up so the scene sits BEHIND the last rows of content instead of starting
   * after them as a separate band. */
  margin-top: calc(-1 * var(--ground-overlap));
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #0000 0, #000 18%);
          mask-image: linear-gradient(to bottom, #0000 0, #000 18%);
}

/* The wrapper takes the scroll parallax; the inner <i> carries the art and the ambient
 * drift. Splitting them is not tidiness: a CSS animation on transform outranks an inline
 * style, so animating the same node the script writes to would cancel the parallax. */
.ground-layer { position: absolute; inset: 0; will-change: transform; }

.ground-layer > i {
  position: absolute;
  inset: 0;
  display: block;
  background-repeat: no-repeat;
  background-position: bottom center;
  background-size: 100% auto;
  image-rendering: pixelated;
}

.ground-clouds { background-image: url('/branding/2.png'); }
.ground-hills  { background-image: url('/branding/3.png'); }
.ground-grass  { background-image: url('/branding/4.png'); }

/* The soft float. The clouds move most and the hills barely, which reads as depth; the
 * grass is the ground and does not move at all. The two durations are deliberately not
 * multiples of each other, so the layers drift in and out of phase instead of pulsing
 * together on a visible beat. */
/* --float is the offset itself, already negative, rather than a magnitude wrapped in a
 * calc() inside the keyframe. One less thing to resolve per frame and no edge case. */
@keyframes ground-float {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, var(--float), 0); }
}

.ground-clouds { --float: -22px; animation: ground-float 8s ease-in-out infinite; }
.ground-hills  { --float: -9px;  animation: ground-float 13s ease-in-out infinite; }

/* With scripting off, or under prefers-reduced-motion, the strip is simply a still
 * picture, which is correct rather than degraded. */
@media (prefers-reduced-motion: reduce) {
  .ground-layer { transform: none !important; }
  .ground-layer > i { animation: none !important; }
}

a { color: var(--accent); text-underline-offset: 2px; }
a:hover { color: var(--ink); }

/* ---------- chrome ---------- */
/* The masthead is exactly as tall as the scene, so the menu bar lands on the horizon
 * rather than cutting through the grass. Both read off --scene-h; changing one value
 * moves them together. */
.masthead { display: flex; justify-content: center; padding: 40px 16px 26px; text-align: center; }
.brand { display: inline-block; text-decoration: none; color: #fff; }
/* The logo is pixel art at 400x107 and is shown at its native size, never upscaled to a
 * fraction, so it stays crisp. */
.brand img { display: block; width: min(400px, 78vw); height: auto; image-rendering: pixelated; }
.brand-sub { display: block; margin-top: 6px; font-size: 13px; font-weight: 600; color: #fff;
  letter-spacing: 0.14em; text-transform: uppercase; text-shadow: 0 1px 2px rgb(38 78 100 / 0.55); }

/* The bar itself is transparent so the buttons read against the sky, which is what
 * makes them look like buttons rather than tabs in a band. It is deliberately NOT
 * sticky: a sticky strip would need its own opaque background, and a cream band behind
 * cream buttons has almost no contrast. The footer carries the full sitemap, so nothing
 * becomes unreachable further down a page. */
.menubar { position: relative; z-index: 30; padding: 0 16px 14px; }
.menu { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin: 0 auto; padding: 0; max-width: var(--wrap); list-style: none; }
.menu > li { position: relative; }
.menu > li > a {
  display: block;
  padding: 1px 8px 0;
  font-weight: 700;
  text-decoration: none;
  color: var(--ink);
  /* background-COLOR, never the shorthand. `background:` resets background-clip to
   * border-box, and then the fill floods the transparent rounded corners that the art
   * deliberately leaves clear, giving every button four square shoulders. */
  background-color: var(--btn-fill);
  background-clip: padding-box;
  border-style: solid;
  border-width: 8px 8px 14px;
  border-image: url('/branding/button.png') 8 8 14 8 repeat;
  image-rendering: pixelated;
}

/* The pressed art is a separate 34x31 image with the shadow moved from the bottom to the
 * top, and its insets are the resting ones mirrored: 14/8/8/8 against 8/8/14/8. Both sum
 * to 22px of border, so swapping them cannot change the button's height. */
.menu > li > a:hover,
.menu > li > a:focus-visible,
.menu > li:focus-within > a,
.menu > li > a[aria-current="page"] {
  background-color: var(--btn-fill-pressed);
  border-width: 14px 8px 8px;
  border-image: url('/branding/button-pressed.png') 14 8 8 8 repeat;
  color: var(--ink);
}

/* CSS-only submenus over real anchors, so every link is in the DOM of every page:
 * crawlers see them, they work with scripting off, and they are keyboard reachable.
 * A JS menu that injected items on click would hide them from crawlers. */
.submenu { position: absolute; left: 50%; translate: -50% 0; top: 100%; min-width: 190px; margin: 0; padding: 6px; list-style: none;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 8px 22px rgb(59 42 47 / 0.13); opacity: 0; visibility: hidden; }
.has-menu:hover .submenu, .has-menu:focus-within .submenu { opacity: 1; visibility: visible; }
.submenu a { display: block; padding: 8px 12px; border-radius: 6px; text-decoration: none; color: var(--ink); }
.submenu a:hover { background: var(--accent-soft); color: var(--accent); }

/* No hover on touch, so a panel that can never open is collapsed to its top link. */
@media (hover: none) { .submenu { display: none; } }

/* The paper rises out of the grass as one card, so the sky stays visible down the sides
 * instead of stopping dead at the bottom of the scene. */
/* Content floats on the sky as panels, matching the reference layout, so main itself is
 * transparent and each .panel carries the paper. */
main { position: relative; z-index: 1; max-width: var(--wrap); margin: 0 auto;
  /* The bottom padding keeps the last panel clear of the grass. */
  padding: 26px 16px calc(var(--ground-overlap) * 0.55); }

/* No border: the strip's last row and this background are the same colour, so any line
 * between them would read as a seam in the artwork. */
.site-footer { background: var(--grass); color: #fff; margin-top: -1px; }
.footer-groups { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 22px;
  max-width: var(--wrap); margin: 0 auto; padding: 30px 16px; }
.footer-title { display: block; font-size: 13px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.1em; color: rgb(255 255 255 / 0.75); margin: 0 0 8px; }
.footer-groups ul { margin: 0; padding: 0; list-style: none; }
.footer-groups li { margin-bottom: 5px; }
.footer-groups a { text-decoration: none; color: #fff; }
.footer-groups a:hover { text-decoration: underline; }
.footer-note { max-width: var(--wrap); margin: 0 auto; padding: 0 16px 6px; font-size: 13px; color: rgb(255 255 255 / 0.8); }
.footer-sisters { max-width: var(--wrap); margin: 0 auto; padding: 0 16px 26px; font-size: 13px; color: rgb(255 255 255 / 0.8); }
.footer-sisters a { color: #fff; text-decoration: underline; }
.footer-sisters a:hover { color: #fff2c4; }

/* ---------- page furniture ---------- */
.crumbs ol { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 0 0 10px; padding: 0; list-style: none; font-size: 13px; }
.crumbs li + li::before { content: "/"; margin-right: 6px; opacity: 0.6; }

/* The page head sits on the SKY, not on paper, so it is white with a shadow rather than
 * ink. That applies site-wide because every .page-head sits on the sky. */
.page-head { margin-bottom: 20px; color: #fff; text-align: center;
  text-shadow: 0 1px 3px rgb(24 74 96 / 0.45); }
.page-head h1 { margin: 0 0 8px; font-size: clamp(26px, 4vw, 38px); line-height: 1.15; letter-spacing: -0.02em; }
.page-sub { margin: 0 auto; max-width: 74ch; color: rgb(255 255 255 / 0.94); }
/* Default link colour is low-contrast against the sky, so links in the head are white
 * and underlined, gold on hover. */
.page-sub a { color: #fff; text-decoration: underline; }
.page-sub a:hover { color: #fff2c4; }

/*
 * On a character page the portrait rises out of the hero and into the space the page head
 * occupies, so the head is shifted right and left-aligned to give the figure a clear
 * column. Scoped with :has() so no other page moves.
 *
 * Without this the title and the intro sentence sit behind the art, which is what the
 * 2x portrait exposed: at 1x the figure was short enough to stay below them.
 */
/* The crumbs are a SIBLING of .page-head, not a child, so padding the head alone left the
 * trail stranded at the far left while the title sat in the right column. Both move. */
body:has(.npc-hero) .crumbs,
body:has(.npc-hero) .page-head { padding-left: 420px; text-align: left; }
body:has(.npc-hero) .page-sub { margin-left: 0; }
body:has(.npc-hero) .crumbs ol { justify-content: flex-start; }
/* The head sits directly above the hero on this route, so the usual gap is closed: the
 * space below it is filled by the portrait rather than left empty. */
body:has(.npc-hero) .page-head { margin-bottom: 0; }

@media (max-width: 900px) {
  body:has(.npc-hero) .crumbs,
  body:has(.npc-hero) .page-head { padding-left: 0; text-align: center; }
  body:has(.npc-hero) .page-sub { margin-left: auto; }
  body:has(.npc-hero) .crumbs ol { justify-content: center; }
}

/* Loose notes that are direct children of main are on the sky too; the ones inside a
 * panel keep soft ink. */
main > .note { color: rgb(255 255 255 / 0.92); text-shadow: 0 1px 2px rgb(24 74 96 / 0.4); }
main > .note a { color: #fff; }

.crumbs { color: rgb(255 255 255 / 0.85); }
.crumbs a { color: #fff; }

/* The wooden frame is the BORDER, so all padding here is clearance INSIDE it. It was
 * tight enough that chips sat on the frame; these are the breathing room. */
.panel { padding: 16px 22px 18px; margin: 0 0 20px; }
/* ---------- the wooden frame ---------- */
/* Worn ONLY by the section containers. Cards, chips and pills keep the flat bevel:
 * framing every card would bury the content and cost an extra background layer each,
 * which is the same call the sibling project made after trying it. */
.panel, .hero, .table-wrap, .home-sec {
  background-color: var(--frame-fill);
  /* The frame's corners are ROUNDED, so the corner pixels are transparent. Clipping to
   * the padding box stops a square of fill poking out through them, and it must be
   * background-COLOR: the shorthand would reset the clip again. */
  background-clip: padding-box;
  border-style: solid;
  border-width: var(--frame-top) var(--frame-side) var(--frame-bottom);
  border-image: url('/branding/panel-frame.png') 8 8 12 8 repeat;
  border-radius: 0;
  box-shadow: none;
  image-rendering: pixelated;
}

.panel h2 { margin: 0 0 12px; font-size: 19px; }
.panel h3 { margin: 16px 0 8px; font-size: 15px; color: var(--ink-soft); }
.note { color: var(--ink-soft); font-size: 14px; }

.hero { display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-start;
  padding: 18px 22px 20px; margin-bottom: 20px; }

/* The character hero. The portrait stands ON the panel's bottom edge and SPILLS OVER the
 * top, so the figure breaks the frame rather than sitting inside it.
 *
 * Three things make that work:
 *   - the panel keeps overflow visible, so the art is not clipped
 *   - the art is absolutely positioned against the panel's BORDER box via bottom: 0, so
 *     it stands on the frame's base whatever the panel's height turns out to be
 *   - the panel reserves left padding for it, so the facts never slide underneath
 *
 * Children paint above an element's border, so the portrait covers the top of the wooden
 * frame without needing a z-index. min-height keeps a short panel from letting the figure
 * overhang further than the design intends.
 */
/*
 * 2x: 474x334, an exact doubling of the shared 237x167 canvas, so it stays pixel perfect.
 * 3x was tried and is 711px, which is 62% of the page: the art fits, but only by widening
 * the whole route, and the page is better at its normal width.
 *
 * Everything here is derived from that one size:
 *   --npc-col     the left column the art lives in, exactly the portrait's width
 *   padding-left  the column plus a gutter, so the facts start to its RIGHT
 *   min-height    how tall the panel is on its own
 *   margin-top    (portrait height - panel height), so the figure's top lands just under
 *                 the page head rather than climbing into the menu bar
 *
 * The art is CENTRED in its column rather than pinned left, so a narrow character sits in
 * the middle of the space instead of hugging the edge.
 */
/*
 * THE COLUMN IS SIZED TO THE FIGURES, NOT TO THE CANVAS.
 *
 * The shared canvas is 237px wide only because of two outliers: Caldarus reaches its left
 * edge and Seridia its right. The MEDIAN figure spans x 70-184, about 114px, so half the
 * canvas is transparent padding and reserving the full width leaves a visible hole on
 * either side of most characters.
 *
 * So the column is 400px and the 474px canvas is CENTRED on it, letting the transparent
 * padding hang 37px outside on each side. On the left that falls in the page margin. On
 * the right the gutter is 40px, which is wider than the overhang, so even Seridia's
 * figure stops short of the text.
 *
 * margin-top is small because the page head sits in the RIGHT column: the portrait rises
 * alongside it rather than into it, so there is nothing to clear.
 */
.npc-hero {
  --npc-col: 380px;
  position: relative;
  overflow: visible;
  min-height: 200px;
  padding-left: calc(var(--npc-col) + 40px);
  align-items: center;
  margin-top: 24px;
}
.npc-portrait {
  position: absolute;
  left: calc(var(--npc-col) / 2);
  translate: -50% 0;
  bottom: 0;
  width: 436px;
  height: 334px;
  image-rendering: pixelated;
}

/* The breakpoint is set by the ART, not by a round number: below roughly 474px of
 * portrait plus room for the facts there is nowhere for the text to go, so the figure
 * moves above it. It keeps an exact 1x rather than being scaled to fit, because a
 * fractional scale would undo the pixel alignment. */
@media (max-width: 900px) {
  .npc-hero { padding-left: 0; padding-top: 10px; margin-top: 0; min-height: 0; }
  .npc-portrait { position: static; display: block; translate: none; width: 218px; height: 167px; margin: 0 auto 6px; }
  .portrait { width: 218px; height: 167px; }
  .portraits { grid-template-columns: repeat(auto-fill, minmax(218px, 1fr)); }
}

.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin: 0; flex: 1 1 320px; }
.facts dt { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-soft); }
.facts dd { margin: 2px 0 0; font-weight: 700; font-size: 17px; }

/* ---------- sprites ---------- */
.sprite, .chip-sprite, .card-art, .hero-art, .stage img, .portrait, .home-sprite {
  image-rendering: pixelated;
}
.chip-sprite { width: 32px; height: 32px; object-fit: contain; flex: none; }
.card-art { width: 96px; height: 96px; object-fit: contain; }

/* Character cards: the portrait stands on the card and SPILLS OVER the top, the same
 * move as the detail hero. Doubled from 96px to 192px, so a ~112x144 portrait renders
 * about 150px wide and fills the card's width.
 *
 * Half the portrait sits above the card, so the grid needs its own top padding for the
 * first row and a row-gap larger than the overhang, or row two wears row one's feet.
 * Scoped to a modifier class because the plain .grid is also used for cards with no art
 * at all, which would otherwise gain a 100px hole above them. */
/* .grid.grid-portraits, not .grid-portraits: `.grid { gap: 14px }` is a SHORTHAND that
 * resets row-gap, it sits later in this file, and at equal specificity the later rule
 * wins. That is what made the rows overlap. The compound selector takes order out of it.
 *
 * The row gap has to clear the overhang (96px) with room to spare, or a portrait's feet
 * land on the card above. */
/*
 * PIXEL PERFECT. Every portrait shares one 237x167 canvas (see scripts/extract-sprites.mjs)
 * and is drawn at EXACTLY 1x, so one source pixel is one screen pixel.
 *
 * That constraint is the whole reason for the fixed numbers here. A portrait shown at a
 * height that is not an integer multiple of its source renders some rows one pixel tall
 * and others two, and pixel art looks subtly wobbly even under image-rendering: pixelated.
 * The only other pixel-perfect size is 2x, which is bigger than these cards want.
 *
 * The card is sized from the art rather than the other way round.
 */
.grid.grid-portraits { grid-template-columns: repeat(auto-fill, minmax(253px, 1fr));
  padding-top: 96px; row-gap: 130px; column-gap: 18px; }
.grid.grid-portraits .card { position: relative; overflow: visible; padding-top: 84px; }
.grid.grid-portraits .card-art {
  position: absolute;
  top: -84px;
  left: 50%;
  translate: -50% 0;
  width: 237px;
  height: 167px;
  image-rendering: pixelated;
}
.hero-art { width: 192px; height: 192px; object-fit: contain; flex: none; }
.hero-art-item { width: 128px; height: 128px; }
.home-sprite { width: 96px; height: 96px; object-fit: contain; }

/* Portraits are ~86x139, so they are sized by HEIGHT at an integer-ish scale and left to
 * find their own width. */
/* 2x: 474x334, an exact doubling of the shared canvas, so it stays pixel perfect.
 *
 * The column is 400px and the canvas is CENTRED on it, so the transparent padding either
 * side hangs outside rather than showing as a hole. Same reasoning as the character hero:
 * the canvas is 237 wide because of two outliers, while the median figure is 114. */
.home-portrait {
  position: absolute;
  left: calc(var(--home-col) / 2);
  translate: -50% 0;
  bottom: 0;
  width: 436px;
  height: 334px;
  max-width: none;
  image-rendering: pixelated;
}

/* Four small icons beat one small icon: an 18x18 sprite alone reads as a smudge. */
.home-montage { display: grid; grid-template-columns: repeat(2, auto); gap: 14px;
  place-items: center; }
.home-tile { width: 64px; height: 64px; object-fit: contain; image-rendering: pixelated; }
/* 2x, matching the hero. */
.portrait { width: 436px; height: 334px; max-width: 100%; }
.sprite-missing { display: inline-block; width: 32px; height: 32px; background: var(--accent-soft); border-radius: 4px; }

/* ---------- chips, cards, grids ---------- */
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chips-tight { gap: 4px; }
/* No pills anywhere. A chip is a sprite beside its name, and it earns its emphasis from
 * the art rather than from a capsule around it. Rows of capsules read as rows of buttons
 * and bury the thing they are labelling. */
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 6px 2px 0;
  text-decoration: none; color: var(--ink); font-size: 14px; }
.chip:hover { color: var(--accent); text-decoration: underline; }

/* Inside a table a chip is a plain labelled row, not a pill. A column of 124 pills reads
 * as 124 buttons and buries the sprite that is doing the actual work. Outside a table
 * they stay pills, because there they ARE the tappable thing. */
.table-wrap .chip { padding: 0; }
.chip-count { color: var(--ink-soft); font-variant-numeric: tabular-nums; }

.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
.card { display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 14px 10px;
  background: #fff; border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 2px 8px rgb(24 74 96 / 0.10);
  text-decoration: none; color: var(--ink); text-align: center; }
.card:hover { border-color: var(--accent); }
.card-name { font-weight: 700; }
.card-meta { font-size: 13px; color: var(--ink-soft); }

.tag { display: inline-block; font-size: 12px; color: var(--ink-soft); }
.tag + .tag::before { content: "· "; }
.tag-date { color: var(--accent); font-weight: 700; }

/* Seasons are coloured words, not capsules. */
.pill { display: inline-block; margin-right: 8px; font-size: 13px; font-weight: 700; }
.pill-spring { color: #4c7137; }
.pill-summer { color: #94701c; }
.pill-fall { color: #9a5230; }
.pill-winter { color: #3f6288; }

.stages { display: flex; flex-wrap: wrap; gap: 14px; }

/* Overworld sprites.
 *
 * The source frame is 80x80 but the character occupies about 21x29 of it, so the art is
 * cropped to a box SHARED by all 507 of them (28x34) before it gets here. That is what
 * removes the empty space; the scale below only sizes what is left.
 *
 * 28x34 at an INTEGER 4x. A fractional scale blurs pixel art even under
 * image-rendering: pixelated, so the box is sized to exact multiples rather than to a
 * round number of CSS pixels. */
.sprite-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 10px 20px; }
.sprite-row h3 { margin: 0 0 4px; text-transform: capitalize; }
.sprite-set { display: flex; gap: 10px; }
.sprite-set figure { margin: 0; text-align: center; }
.sprite-set figcaption { font-size: 12px; color: var(--ink-soft); text-transform: capitalize; }
.npc-walk { width: 112px; height: 136px; object-fit: contain; image-rendering: pixelated; }

/* The portrait gallery has the same empty-space problem at a smaller scale, so it gets
 * the same treatment: a tighter grid instead of a wrapping flex row. */
.portraits { display: grid; grid-template-columns: repeat(auto-fill, minmax(436px, 1fr)); gap: 20px; justify-items: center; }
.stages figure, .portraits figure { margin: 0; text-align: center; }
.stages figcaption, .portraits figcaption { font-size: 12px; color: var(--ink-soft); text-transform: capitalize; }
.stages img { width: 64px; height: 64px; object-fit: contain; }

/* ---------- tables ---------- */
/* Wide content scrolls inside its own container so the page body never scrolls sideways. */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { padding: 10px 16px; text-align: left; border-bottom: 1px solid var(--line); vertical-align: middle; }
th { position: sticky; top: 0; background: var(--frame-fill); font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--ink-soft); white-space: nowrap; }
tbody tr:last-child td { border-bottom: 0; }
.table-wrap table { background: var(--frame-fill); }
tbody tr:hover { background: var(--accent-soft); }

/* Every sortable column advertises itself at rest. The resting rule must come FIRST:
 * the three states have equal specificity, so order decides. */
th[data-sort] { cursor: pointer; }
th[data-sort]::after { content: " ⇅"; opacity: 0.35; }
th[data-sort][aria-sort="ascending"]::after { content: " ↑"; opacity: 1; }
th[data-sort][aria-sort="descending"]::after { content: " ↓"; opacity: 1; }

/* ---------- home ---------- */
/*
 * EVERY section shares ONE art column, so the prose starts at the same x all the way down
 * the page. The column is sized by the widest thing that goes in it - the 2x portrait -
 * and the montages are centred within it rather than left-aligned in a narrower column of
 * their own.
 *
 * Sizing each section to its own art is what made the columns disagree: a 150px montage
 * column beside a 400px portrait column put two paragraphs 250px apart.
 */
.home-sec {
  /*
   * Narrower than the 436px canvas on purpose. Every figure is centred in that canvas,
   * so the transparent overflow hangs outside the column and only the art shows. That is
   * what buys the prose its width: Celine's figure is 172px at 2x, so a 240px column
   * still leaves 34px of clear space either side of her.
   */
  --home-col: 240px;
  --home-gutter: 40px;
  display: grid;
  grid-template-columns: var(--home-col) 1fr;
  gap: var(--home-gutter);
  align-items: center;
  padding: 18px 22px 20px;
  margin-bottom: 20px;
}
/*
 * The portrait section is laid out exactly like the character hero: the figure STANDS ON
 * the card's bottom edge and spills over its top, rather than sitting inside a grid cell.
 *
 * Same three derived numbers as .npc-hero, and the same reason for each:
 *   --home-col    the column the art occupies
 *   padding-left  the column plus a gutter, so the prose starts to its RIGHT
 *   margin-top    the overhang, (portrait height - card height), so the figure clears
 *                 whatever sits above it
 *
 * The montage sections keep the plain grid: they have nothing to spill over, and they
 * inherit the same --home-col so their prose lines up with this one.
 */
.home-sec.has-portrait {
  display: block;
  position: relative;
  overflow: visible;
  /*
   * The gap above this card is the portrait's OVERHANG, not decoration: the figure is
   * 334px and stands on the card's bottom edge, so it rises (334 - card height) above it
   * and margin-top has to clear that.
   *
   * The way to shrink the gap is therefore to make the CARD taller, not the margin
   * smaller. A taller card eats more of the portrait, leaving less to hang over. At 270px
   * the overhang is 64px, against 144px when the card was 190px tall.
   *
   * The character hero does not need this because its heading sits in the right column,
   * so the figure rises beside the text rather than above it. Here the heading is centred
   * and full width, so there is nothing to rise alongside.
   */
  min-height: 270px;
  /*
   * The card's own 22px padding has to be added back here. `padding-left` overrides the
   * `padding` shorthand entirely, so writing `col + gutter` put this section's text 22px
   * LEFT of every grid section's, which is a misalignment you can see but not explain.
   */
  padding-left: calc(22px + var(--home-col) + var(--home-gutter));
  margin-top: 72px;
}
.home-sec.has-portrait .home-art { position: static; }
.home-sec h2 { margin: 0 0 6px; font-size: 21px; }
.home-sec h2 a { text-decoration: none; }
.home-art { display: flex; justify-content: center; }
@media (max-width: 620px) {
  .home-sec { grid-template-columns: 1fr; }
  .home-art { justify-content: flex-start; }
}

/* ---------- farm layout planner ---------- */
/*
 * Fullscreen. The site chrome is hidden outright on this route and the planner takes the
 * whole viewport, so the editor gets every pixel and no band of sky sits above it.
 *
 * The masthead, menu bar, ground strip and footer are all hidden, which is why the nav
 * reappears INSIDE the tool panel: without it the page would be a dead end.
 *
 * The base rule is `position: fixed; inset: 0` and there is nothing to keep in step,
 * because the chrome it would otherwise have to measure is not rendered at all. That is
 * deliberately simpler than the sibling's arrangement, which floats its menu over the map
 * and has to track a header height that changes when the bar wraps.
 */
body:has(.planner) .masthead,
body:has(.planner) .menubar,
body:has(.planner) .ground,
body:has(.planner) .site-footer,
body:has(.planner) .sky { display: none; }

body:has(.planner) main {
  max-width: none;
  margin: 0;
  padding: 0;
  background: none;
  border-radius: 0;
  box-shadow: none;
}

.planner { position: fixed; inset: 0; z-index: 1; background: #17120d; }

/* The panel floats over the map, top left. */
.pl-side {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 20;
  width: 268px;
  max-height: calc(100vh - 24px);
  overflow-y: auto;
  padding: 12px 16px 14px;
  background-color: var(--frame-fill);
  background-clip: padding-box;
  border-style: solid;
  border-width: var(--frame-top) var(--frame-side) var(--frame-bottom);
  border-image: url('/branding/panel-frame.png') 8 8 12 8 repeat;
  image-rendering: pixelated;
}

.pl-bar { text-align: center; }
/* The logo is pixel art at 400x107 shown at an exact half, so it stays on whole pixels. */
.pl-brand img { display: block; width: 200px; height: auto; margin: 2px auto 6px; image-rendering: pixelated; }
.pl-nav { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px; margin-bottom: 8px; }
.pl-nav a { font-size: 12px; font-weight: 700; text-decoration: none; color: var(--ink);
  padding: 2px 6px; }
.pl-nav a:hover { color: var(--accent); text-decoration: underline; }

.planner h1 { margin: 0 0 4px; font-size: 17px; line-height: 1.2; }
.pl-intro { margin: 0 0 8px; font-size: 12px; color: var(--ink-soft); }

.pl-tools { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.pl-tools button { font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  padding: 1px 8px 0; color: var(--ink); background-color: var(--btn-fill); background-clip: padding-box;
  border-style: solid; border-width: 8px 8px 14px;
  border-image: url('/branding/button.png') 8 8 14 8 repeat; image-rendering: pixelated; }
.pl-tools button:hover, .pl-tools button[aria-pressed="true"] {
  background-color: var(--btn-fill-pressed); border-width: 14px 8px 8px;
  border-image: url('/branding/button-pressed.png') 14 8 8 8 repeat; }

.pl-group h3 { margin: 8px 0 4px; font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--ink-soft); }
.pl-group h3 span { opacity: 0.6; }
.pl-items { display: grid; grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); gap: 4px; }
.pl-items button { padding: 3px; cursor: pointer; background: var(--paper);
  border: 1px solid var(--line); border-radius: 6px; line-height: 0; }
.pl-items button:hover { border-color: var(--accent); }
.pl-items button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
.pl-icon { width: 28px; height: 28px; object-fit: contain; image-rendering: pixelated; }

/* Status bar floats top RIGHT, so the panel and it read as one strip across the top. */
.pl-status { position: absolute; top: 12px; right: 12px; z-index: 20; margin: 0;
  padding: 6px 12px; font-size: 13px; font-variant-numeric: tabular-nums; color: var(--ink);
  background: var(--frame-fill); border: 1px solid var(--line); border-radius: 8px;
  max-width: min(46vw, 520px); }

/* The map fills the route and pans; nothing else scrolls. */
.pl-scroll { position: absolute; inset: 0; overflow: auto; }

/* The stage MUST create its own stacking context. Placed nodes carry a z-index and
 * without isolation those values compete directly with the floating panel, putting every
 * crop on top of the controls. */
/* Dragging across an <img> starts a native image drag, which cancels the pointer stream
 * and makes click-and-drag painting stop after the first tile. Blocking the drag and the
 * selection is what makes the stroke continuous; touch-action keeps a finger drag from
 * scrolling the map instead of painting on it. */
.pl-stage { position: relative; isolation: isolate; touch-action: none;
  user-select: none; -webkit-user-select: none; cursor: crosshair; }
.pl-stage img { -webkit-user-drag: none; user-select: none; }
.pl-map { position: absolute; inset: 0; image-rendering: pixelated; }
.pl-grid { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.pl-node { position: absolute; z-index: 1; image-rendering: pixelated; pointer-events: none; }

@media (max-width: 820px) {
  .pl-side { width: min(268px, 70vw); max-height: calc(100vh - 90px); }
  .pl-status { top: auto; bottom: 12px; right: 12px; max-width: calc(100vw - 24px); }
}

/* ---------- calendar ---------- */
/*
 * One notebook per season, stacked in a single column.
 *
 * The art is used at NATIVE SIZE. A 9-slice stretches only the middle, so the binding and
 * the page edge stay exactly as drawn however tall the page grows; scaling the source up
 * would make the rings chunkier without letting any more content inside.
 *
 * border-image-repeat is ROUND, not repeat. The top-middle tile carries two rings, and
 * repeat would cut one in half at whatever width the page lands on, while round scales the
 * tile so a whole number always fits.
 *
 * The insets are explicit rather than derived (see scripts/slice-frame.py): the binding is
 * not the fill colour, so the uniform-block test that works on the wooden frame would
 * return a top inset that slices straight through the rings.
 *
 * The border IS the page edge, so all the content sits in the padding box inside it.
 */
.cal-year { display: flex; flex-direction: column; gap: 26px; }

  /* see the padding note on .panel */
.cal-season {
  background-color: var(--notebook-fill);
  background-clip: padding-box;
  border-style: solid;
  /* 26px sides: the corner has to contain two rings so the middle stays a plain band.
   * See scripts/slice-frame.py for why. */
  border-width: 20px 26px 5px;
  border-image-slice: 20 26 5 26;
  border-image-repeat: round;
  border-image-source: url('/branding/notebook-spring.png');
  image-rendering: pixelated;
  padding: 16px 20px 18px;
}

.cal-spring { border-image-source: url('/branding/notebook-spring.png'); }
.cal-summer { border-image-source: url('/branding/notebook-summer.png'); }
.cal-fall   { border-image-source: url('/branding/notebook-fall.png'); }
.cal-winter { border-image-source: url('/branding/notebook-winter.png'); }

/* The heading sits under the binding rather than in it: the binding is the BORDER, and
 * nothing can be laid out inside a border. */
.cal-season h3 { margin: 0 0 10px; font-size: 22px; text-align: center; }

.cal-weekdays, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.cal-weekdays { margin-bottom: 5px; }
.cal-weekdays span { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-soft); text-align: center; }

.cal-day { position: relative; aspect-ratio: 1; min-height: 84px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px; padding: 3px;
  background: var(--paper); border: 1px solid var(--line); }
.cal-day.is-marked { background: var(--accent-soft); border-color: var(--accent); }
.cal-num { position: absolute; top: 2px; left: 4px; font-size: 10px; color: var(--ink-soft);
  font-variant-numeric: tabular-nums; }
.cal-day.is-marked .cal-num { color: var(--accent); font-weight: 700; }

.cal-mark { display: block; line-height: 0; text-decoration: none; }
.cal-icon { width: 60px; height: 60px; object-fit: contain; image-rendering: pixelated; }
.cal-fest { font-size: 9px; line-height: 1.1; font-weight: 700; color: var(--accent);
  text-align: center; text-transform: uppercase; }

.cal-foot { margin: 12px 0 6px; font-size: 13px; color: var(--ink-soft); }

.cal-spring .cal-day.is-marked { background: #e8f4dd; border-color: #7ba263; }
.cal-summer .cal-day.is-marked { background: #fdf0cf; border-color: #c9a13c; }
.cal-fall   .cal-day.is-marked { background: #fae0d2; border-color: #c07a52; }
.cal-winter .cal-day.is-marked { background: #dfeaf7; border-color: #6a8db8; }

/* ---------- facets ---------- */
.facets { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px; margin-bottom: 26px; }
.facet-row { display: flex; flex-wrap: wrap; gap: 6px; }
.facet-row button { font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  padding: 1px 8px 0; color: var(--ink); background-color: var(--btn-fill);
  background-clip: padding-box; border-style: solid; border-width: 8px 8px 14px;
  border-image: url('/branding/button.png') 8 8 14 8 repeat; image-rendering: pixelated; }
.facet-row button:hover, .facet-row button[aria-pressed="true"] {
  background-color: var(--btn-fill-pressed); border-width: 14px 8px 8px;
  border-image: url('/branding/button-pressed.png') 14 8 8 8 repeat; }
.card[hidden] { display: none; }

/* ---------- skills, museum, shops ---------- */
.perks { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; }
.perk h4 { margin: 0 0 4px; font-size: 15px; }
.perk p { margin: 0; font-size: 13px; color: var(--ink-soft); }
.perk-cost { font-size: 12px; font-weight: 600; color: var(--accent); }
.ranks { margin: 0; padding-left: 20px; }
.ranks li { margin-bottom: 3px; }
.mset { margin-bottom: 16px; }
.mset:last-child { margin-bottom: 0; }
.mset h4 { margin: 0 0 3px; font-size: 15px; }
ul.plain { margin: 0; padding-left: 18px; }
ul.plain li { margin-bottom: 4px; }

/* ---------- location maps ---------- */
.grid.grid-maps { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
.map-card { padding: 0; overflow: hidden; }
/* Thumbnails are whole maps, some 3000px wide, so they are cropped to a fixed box rather
 * than letterboxed: a 4:1 beach shown whole would be a sliver. */
.map-thumb { width: 100%; height: 150px; object-fit: cover; object-position: center;
  image-rendering: pixelated; display: block; }
.map-card .card-name { margin-top: 10px; }
.map-card .card-meta { margin-bottom: 12px; }

/* A map can be 3000px wide, so it scrolls inside its own box and the page body does not. */
.map-view { overflow: auto; max-height: 74vh; background: var(--paper); }
.map-view img { display: block; max-width: none; image-rendering: pixelated; }

/* ---------- npc schedules ---------- */
.sched-season { margin-bottom: 28px; }
.sched-season:last-child { margin-bottom: 0; }
/* A full-width rule that runs from the season name to the far edge, so four seasons in
 * one panel read as four sections rather than one long list. The line is drawn by a
 * flex-grown pseudo-element rather than a border, so it always fills exactly the space
 * the text leaves. */
.sched-season h3 { display: flex; align-items: center; gap: 12px; margin: 0 0 12px;
  font-size: 19px; text-transform: uppercase; letter-spacing: 0.08em; }
.sched-season h3::after { content: ""; flex: 1; height: 2px; background: var(--line); }
.sched-spring h3 { color: #4c7137; } .sched-spring h3::after { background: #7ba263; }
.sched-summer h3 { color: #94701c; } .sched-summer h3::after { background: #c9a13c; }
.sched-fall   h3 { color: #9a5230; } .sched-fall   h3::after { background: #c07a52; }
.sched-winter h3 { color: #3f6288; } .sched-winter h3::after { background: #6a8db8; }
.sched-days { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 14px; }
.sched-day h4 { margin: 0 0 5px; font-size: 13px; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--ink-soft); }
.sched-list { margin: 0; padding: 0; list-style: none; font-size: 13px; }
.sched-list li { display: flex; gap: 8px; padding: 3px 0; border-bottom: 1px solid var(--line); }
.sched-list li:last-child { border-bottom: 0; }
/* Tabular numerals so the times form a column instead of jittering. */
.sched-time { flex: none; width: 62px; font-variant-numeric: tabular-nums; color: var(--ink-soft); }
.sched-place { display: flex; flex-direction: column; }
.sched-spot { font-size: 11px; color: var(--ink-soft); }

/* Game HUD glyphs beside a number. Pixel art, so integer scaling and no smoothing. */
.stat-icon {
  width: 16px; height: 16px; margin-left: 6px; vertical-align: -3px;
  image-rendering: pixelated;
}
.facts dd { display: flex; align-items: center; gap: 0; }

/* ---------- table search ---------- */
.table-search { flex: 1 1 220px; min-width: 200px; }
.table-search input {
  width: 100%; padding: 7px 10px; font: inherit; font-size: 13px;
  color: var(--ink); background: var(--paper);
  border: 2px solid var(--line); border-radius: 4px;
}
.table-search input:focus-visible { outline: 2px solid var(--ink-soft); outline-offset: 1px; }
/* Present for screen readers and for the input's accessible name, out of the layout. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* ---------- quests on a character page ---------- */
/* Columns, because Adeline gives 54 and a single list would run off the page. The heading
   carries the count so a group reads as complete without counting the rows. */
.quest-group, .infusion-group { margin: 0 0 16px; }
.quest-group:last-child, .infusion-group:last-child { margin-bottom: 0; }
.quest-group h3, .infusion-group h3 { margin: 0 0 6px; font-size: 13px; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--ink-soft); }
.quest-list { columns: 2; column-gap: 22px; font-size: 13px; }
.quest-list li { break-inside: avoid; padding: 3px 0; border-bottom: 1px solid var(--line); }
.quest-list .note { margin-left: 6px; font-size: 11px; }
@media (max-width: 620px) { .quest-list { columns: 1; } }

/* ---------- heart events (spoilers) ---------- */
.spoiler { margin: 0 0 10px; border: 1px solid var(--line); background: var(--paper); }
.spoiler:last-child { margin-bottom: 0; }
.spoiler > summary { cursor: pointer; padding: 10px 14px; font-weight: 700;
  list-style: none; display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; }
.spoiler > summary::-webkit-details-marker { display: none; }
.spoiler > summary::before { content: "+"; font-weight: 700; color: var(--accent); }
.spoiler[open] > summary::before { content: "-"; }
.spoiler > summary:hover { background: var(--accent-soft); }
.spoiler-hint { font-size: 12px; font-weight: 400; color: var(--ink-soft); }
.hl-script { padding: 4px 16px 14px; border-top: 1px solid var(--line); }
.hl-line { margin: 0 0 8px; }
.hl-who { display: block; font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--accent); }
/* The player's replies are indented and marked, so a scene reads as a conversation
 * rather than as one long monologue. */
.hl-choice { margin: 0 0 8px 22px; padding-left: 10px; border-left: 3px solid var(--line);
  font-style: italic; color: var(--ink-soft); }
.hl-script h4 { margin: 16px 0 8px; font-size: 13px; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--ink-soft); }

.facet-count { margin: 0; align-self: center; font-size: 13px; color: rgb(255 255 255 / 0.9);
  text-shadow: 0 1px 2px rgb(24 74 96 / 0.4); }
.facets[data-table-facets] { justify-content: flex-start; }
tr[hidden] { display: none; }
th[data-sort]:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.tier-row { margin-bottom: 12px; }
.tier-row:last-child { margin-bottom: 0; }
.tier-row h4 { margin: 0 0 5px; font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--ink-soft); }
.card-art-row { display: flex; gap: 4px; justify-content: center; margin-bottom: 4px; }

@media (max-width: 860px) {
  /* No room for a 400px column beside the prose, so the figure goes above it at an exact
   * 1x rather than being scaled to fit. */
  .home-sec.has-portrait { padding-left: 0; margin-top: 0; min-height: 0; }
  .home-portrait { position: static; translate: none; display: block;
    width: 218px; height: 167px; margin: 0 auto 8px; }
}
