/* ==========================================================================
   Rogue Survivor Reloaded - static site stylesheet
   Served from docs/ on GitHub Pages. All URLs are relative so the site works
   both at a domain root and under a /<repo>/ project subpath.
   ========================================================================== */

/* --- Web fonts ----------------------------------------------------------- */
/* JetBrains Mono, self-hosted from assets/fonts/. Self-hosted rather than
   linked from a CDN so the site makes no third-party request, which also means
   no external origin can see a visitor reading the manual.

   Four faces are declared but the family only ships two weights, 400 and 700.
   Anything asked for between them is matched to whichever is nearer, so a 500
   lands on Regular and a 600 on Bold -- the browser does not synthesise an
   intermediate weight, which is what would make it look smeared.

   `swap` so text is readable immediately in the fallback rather than waiting
   on a ~90 KB file. The fallback stack is monospace on purpose: this family is
   what --font-body resolves to, so a sans-serif flash would be a visible jump
   in metrics on every line. */
@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/JetBrainsMono-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/JetBrainsMono-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/JetBrainsMono-Italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/JetBrainsMono-BoldItalic.woff2") format("woff2");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

/* Storm Gust, self-hosted from assets/fonts/, and used only for headings.

   It is a *display* face and a proportional one: 218 glyphs, no fixed pitch,
   and one weight. Two consequences the heading rules below depend on:

   - **Only 400 is declared, and headings must ask for 400.** JetBrains Mono
     ships a real 700 and the h1-h4 rule asks for bold, which it gets for free.
     Storm Gust has no bold face, so leaving the 700 in place would make the
     browser *synthesise* a bold by smearing the outline -- the same smeared
     intermediate weight the JetBrains block above explicitly avoids by
     declaring both real weights. Hence `font-weight: 400` on the heading rule.
   - **It is proportional**, so it cannot be used for the mono token. Nothing
     tabular here: headings are prose.

   `swap` for the same reason as below -- and here it matters more, because a
   proportional face falling back to a monospace one changes the shape of every
   heading rather than just its weight. */
@font-face {
  font-family: "Storm Gust";
  src: url("../fonts/StormGust.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}


/* --- Design tokens ------------------------------------------------------ */

:root {
  /* Backgrounds, borders and text are all warm neutrals (R > G > B) so they
     sit with the ember/rust accents instead of fighting them. The hue is
     rotated off the green cast these had, with luminance held within ~4% of
     the original on every token -- so the change is a tint, not a re-balance,
     and no contrast ratio moves perceptibly. */
  --bg:          #100d0b;
  --bg-raised:   #191512;
  --bg-panel:    #1e1915;
  --bg-sunken:   #0b0908;
  /* The sticky header sits over scrolling content, so it is --bg translucent
     rather than opaque. Named so no colour is hardcoded in a rule. */
  --bg-glass:    rgba(16, 13, 11, 0.92);
  --border:      #332b25;
  --border-lit:  #4a3e35;

  --text:        #dbd6d0;
  --text-dim:    #9e968d;
  --text-faint:  #776f67;

  /* Text accents: a warm ember orange for the primary, a deeper dark red for
     the secondary. Both clear 4.5:1 on --bg *and* on --bg-panel, which matters
     because the secondary is used for 0.72-0.78rem text inside panels
     (.eyebrow, .build__meta) as well as for borders. */
  --ember:       #f08a3d;   /* orange - links, headings, focus, primary button */
  --rust:        #dd564e;   /* dark red - eyebrows, borders, sublist markers */
  --amber:       #d9a441;
  --blue:        #6fa8c7;

  /* Dark text sitting on top of an accent fill. Equals --bg; named separately
     because "text on orange" is a different job from "page background". */
  --on-accent:   #100d0b;

  /* "JetBrains Mono" has to come *first* to be used at all. `-apple-system`
     and `BlinkMacSystemFont` are not fallbacks in the usual sense -- they are
     keywords that resolve to the platform UI font, and BlinkSystemFont is
     recognised by Chrome on every OS. Listing them ahead of the web font means
     they always match first, so the site silently renders in the system font
     and the @font-face below is never exercised. The system stack stays
     behind it purely as a fallback for when the woff2 fails to load. */
  --font-body:   "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono",
                 Menlo, Consolas, "Liberation Mono", monospace;
  /* Same family as --font-body on purpose. The site leans on this token for
     every kbd, code span, caption and eyebrow; leaving it on a different stack
     would render inline code in Consolas or Menlo while the text around it was
     JetBrains Mono, which reads as a mistake rather than as emphasis. */
  --font-mono:   "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono",
                 Menlo, Consolas, "Liberation Mono", monospace;
  /* Headings only. Falls back to the body stack on purpose: if the woff2 fails
     to load, a heading in JetBrains Mono still reads as a heading, whereas a
     missing proportional display face should not leave the h1 in a generic
     serif. Same order as --font-body after the family, so the fallback is the
     body face and not a system font. */
  --font-display: "Storm Gust", "JetBrains Mono", ui-monospace,
                  SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --wrap:        1080px;
  --radius:      6px;
  --header-h:    3.5rem;
}

/* --- Reset -------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 1rem);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.65 var(--font-body);
  /* Faint scanline wash so the flat dark background has some texture. */
  background-image:
    radial-gradient(ellipse 80% 50% at 50% -10%, rgba(221, 86, 78, 0.07), transparent 60%);
  background-repeat: no-repeat;
}

img { max-width: 100%; height: auto; display: block; }

a { color: var(--ember); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3, h4 {
  line-height: 1.25;
  margin: 0.6em 0 0.6em;
  /* 400, not the 700 the body face would take. Storm Gust ships no bold face,
     so a 700 here is a synthesised smear rather than a real weight -- see the
     @font-face note. This is the whole reason the heading rule cannot just
     inherit the site's bold convention. */
  font-weight: 400;
  letter-spacing: -0.005em;
}
h1 { font-size: clamp(1.9rem, 4.5vw, 2.9rem); font-family: var(--font-display);}
h2 { font-size: clamp(1.4rem, 3vw, 1.85rem); font-family: var(--font-display);}

/* The manual's section headings use the body face rather than the display one.
   Its h2s are numbered all-caps section titles — "4. ACTIONS & COMMANDS" — which
   run long enough that the display face reads as a different kind of thing from
   the text under it. Scoped to the page rather than folded into the `h2` rule
   above, because every other page uses that rule too and this was asked for here;
   drop the `.page-manual` qualifier to make it site-wide.

   Weight stays 400, inherited from the shared heading rule, and that is now a
   real weight rather than a workaround: Storm Gust ships no bold face, which is
   why the comment on that rule explains the 400, whereas JetBrains Mono has one
   and is already the body face. */
.page-manual h2 { font-family: var(--font-mono); }
h3 { font-size: 1.15rem; font-family: var(--font-mono); font-weight: bold;}
h4 { font-size: 1rem; font-family: var(--font-mono); font-weight: bold;}

p { margin: 0 0 1em; }
ul, ol { margin: 0 0 1em; padding-left: 1.4em; }
li { margin: 0.25em 0; }

code, kbd, samp { font-family: var(--font-mono); font-size: 0.9em; }

hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 0;
}

:focus-visible {
  outline: 2px solid var(--ember);
  outline-offset: 2px;
  border-radius: 2px;
}

/* --- Layout helpers ----------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 1.25rem;
}

.section { padding: 3rem 0; }
.section--tight { padding: 2rem 0; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--ember);
  color: var(--on-accent);
  padding: 0.6rem 1rem;
  font-weight: 700;
  z-index: 100;
}
.skip-link:focus { left: 0; }

.eyebrow {
  font: 700 0.72rem/1 var(--font-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--rust);
  margin: 0 0 0.9rem;
}

.lede {
  font-size: 1.1rem;
  color: var(--text-dim);
  max-width: 62ch;
}

.muted { color: var(--text-dim); }
.small { font-size: 0.85rem; }

/* --- Header / nav ------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--bg-glass);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: 1rem;
  height: var(--header-h);
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: 0.02em;
  color: var(--text);
  white-space: nowrap;
  flex-shrink: 0;
}
.brand:hover { text-decoration: none; color: var(--ember); }
.brand img {
  width: 28px;
  height: 28px;
  image-rendering: pixelated;
  border-radius: 4px;
}
/* The site title is two colours: "Rogue Survivor" in the dark red, "Reloaded" in
   the ember orange. Both are set on the span rather than inherited, so the
   hover rule below needs its own span selector to keep working. */
.brand span { color: var(--rust); }
.brand span em { font-style: normal; color: var(--ember); }
.brand:hover span { color: var(--ember); }

.site-nav {
  margin-left: auto;
  display: flex;
  gap: 0.15rem;
  overflow-x: auto;
  scrollbar-width: none;
}
.site-nav::-webkit-scrollbar { display: none; }

.site-nav a {
  padding: 0.4rem 0.7rem;
  border-radius: var(--radius);
  color: var(--text-dim);
  font-size: 0.9rem;
  font-weight: 500;
  white-space: nowrap;
}
.site-nav a:hover { background: var(--bg-raised); color: var(--text); text-decoration: none; }
.site-nav a[aria-current="page"] { color: var(--ember); background: rgba(240, 138, 61, 0.10); }

/* --- Buttons ------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.65rem 1.15rem;
  border: 1px solid var(--border-lit);
  border-radius: var(--radius);
  background: var(--bg-panel);
  color: var(--text);
  font: 600 0.92rem/1 var(--font-body);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, transform 0.05s;
}
.btn:hover { border-color: var(--rust); text-decoration: none; background: var(--bg-raised); }
.btn:active { transform: translateY(1px); }

.btn--primary {
  background: var(--ember);
  border-color: var(--ember);
  color: var(--on-accent);
}
.btn--primary:hover { background: #ffa85e; border-color: #ffa85e; color: var(--on-accent); }

.btn-row { display: flex; flex-wrap: wrap; gap: 0.6rem; }

/* --- Hero --------------------------------------------------------------- */

.hero { padding: 3.5rem 0 3rem; }

.hero__grid {
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: 3rem;
  align-items: center;
}

.hero__visual { display: block; }

.hero h1 { margin-bottom: 0.4em; }

.hero__title-accent {
  display: block;
  color: var(--ember);
  font-family: var(--font-display);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-bottom: 0.8rem;
  font-size: 2em;
  line-height: 1em;
}

.hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  margin: 1.4rem 0 0;
  padding: 0;
  list-style: none;
  font: 500 0.8rem/1 var(--font-mono);
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.hero__meta li { margin: 0; }
.hero__meta li + li { position: relative; }
.hero__meta li + li::before {
  content: "\2022";
  position: absolute;
  left: -0.75rem;
  color: var(--border-lit);
}

/* The sprite mosaic that used to stand in for a screenshot is gone -- there is a
   real one in the Screenshots block below. Its rules went with it, because
   check-site.mjs fails on a class that is defined but never used. The tiles are
   still on disk under assets/img/ if the mosaic ever comes back. */

/* --- Screenshots -------------------------------------------------------- */
/* Real gameplay captures, so `image-rendering: pixelated` is deliberately NOT
   set here: these are 1:1 canvas output, not scaled-up 32x32 sprites. */

.shots {
  display: grid;
  gap: 1.25rem;
  /* `min(420px, 100%)` rather than a bare `420px`. A grid track's minimum is
     also its *min-content contribution* to the parent, so a hard 420px floor in
     a column narrower than that does not clip -- it inflates the parent. Measured
     in the hero at a 1024px viewport: the visual column was pinned at 420px when
     its `0.85fr` share was 398px, so the gallery quietly took ~70px off the
     headline beside it. `min()` lets the track shrink to the container instead,
     which is what "one image per row" is supposed to mean. */
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
}

/* The original-vs-port comparison, where equal *widths* are the wrong answer.
 *
 * The two captures are not the same shape: `screen_original.webp` is 1282x960
 * (4:3, the original WinForms window) and `screen1.webp` is 1562x867 (16:9, the
 * port's resized UI). `.shots` gives both cards the same width, so both land on
 * the same display width but at *different scale factors* -- measured at a 1280px
 * viewport, 0.398 against 0.327, which is the port drawn at 82% of the original's
 * size. The sprites, the tiles and the UI text are all physically smaller in the
 * port shot, and a reader has no way to tell that from the port being worse at
 * drawing the game. That is the one thing this section is asking to be judged on,
 * so it is the one thing a layout rule must not quietly colour.
 *
 * Columns proportional to the two files' native widths put both at the same scale
 * (0.366 at a 1280px viewport) instead of the same width. The widescreen card
 * becomes the wider one, which is also the honest signal: the capture really is
 * wider, and nothing is cropped or hidden to make the boxes match.
 *
 * A side effect worth having: the cards stop being 99px different in height
 * (382 against 283) and become 34px different (351 against 317), so the row reads
 * as a pair rather than as one card with something missing.
 *
 * The numbers are a coupling to those two files, and only those two. Re-export
 * either screenshot at a new size and this needs updating -- which is the point of
 * writing it as `fr` rather than as two hard-coded pixel widths, since the ratio
 * is the thing that has to be right.
 */
.shots--compare {
  grid-template-columns: 1282fr 1562fr;
}

/* Stacked, like every other gallery, once there is not room for two columns.
 * Stated here rather than left to the 900px `.shots` rule below: that rule is
 * 400 lines further down the file and this one silently depends on losing to it,
 * which is the kind of coupling a later edit breaks without anyone noticing. */
@media (max-width: 900px) {
  .shots--compare { grid-template-columns: 1fr; }
}

/* The sprite-set gallery: five captures, every one of them 1562x867.
 *
 * Unlike `.shots--compare` there is no scale to keep honest between the columns,
 * because the captures are the same shape -- so equal widths are correct here and
 * two sprites really are as big as each other, which is the whole point of the
 * section. What the 420px floor of `.shots` is wrong for is the *count*: five
 * images at two columns leaves an orphan on the last row. A 330px floor lays them
 * out 3 + 2 at the wrap width and still gets three at half of it.
 *
 * Stacked at 700px rather than 900px, which is where two 330px tracks plus the gap
 * stop fitting -- the narrower floor means this gallery still has two columns at the
 * width `.shots--compare` has already collapsed to one. Stated here, next to the
 * rule that depends on it, for the reason given above. */
.shots--sets {
  grid-template-columns: repeat(auto-fit, minmax(min(330px, 100%), 1fr));
}

@media (max-width: 700px) {
  .shots--sets { grid-template-columns: 1fr; }
}

.shot {
  margin: 0;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px;
  box-shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.9);
}

.shot__img {
  display: block;
  width: 100%;
  height: auto;
  background: #000;
  border-radius: 2px;
}

.shot__cap {
  margin: 0.7rem 0.4rem 0.3rem;
  font: 500 0.72rem/1.5 var(--font-mono);
  color: var(--text-faint);
  letter-spacing: 0.04em;
}

/* --- Cards -------------------------------------------------------------- */

.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}

.grid--2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }

.card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.15rem 1.25rem;
}
.card > :last-child { margin-bottom: 0; }
.card h3 { color: var(--text); }
.card p { color: var(--text-dim); font-size: 0.94rem; }

.card--feature { border-left: 2px solid var(--rust); }

.card__icon {
  width: 40px;
  height: 40px;
  image-rendering: pixelated;
  margin-bottom: 0.7rem;
}

/* The numbered .steps list went with the how-to-play section. Its rules went
   with it, because check-site.mjs fails on a class that is defined but never
   used. Reinstating the section means reinstating these too. */

/* --- Key / command display ---------------------------------------------- */

kbd {
  display: inline-block;
  min-width: 1.5rem;
  padding: 0.15rem 0.4rem;
  background: var(--bg-sunken);
  border: 1px solid var(--border-lit);
  border-bottom-width: 2px;
  border-radius: 4px;
  font-size: 0.78rem;
  line-height: 1.4;
  text-align: center;
  color: var(--text);
  white-space: nowrap;
}

.keylist { display: grid; gap: 0.5rem 1.5rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.keylist div { display: flex; align-items: baseline; gap: 0.6rem; }
.keylist span { color: var(--text-dim); font-size: 0.9rem; }
.keylist kbd { flex-shrink: 0; }

/* --- Tables ------------------------------------------------------------- */

.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.92rem;
}

th, td {
  text-align: left;
  padding: 0.6rem 0.9rem;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

thead th {
  background: var(--bg-sunken);
  color: var(--text-dim);
  /* 700, not 600: the family has no 600, so 600 would be matched up to Bold
     anyway. Declaring what actually renders beats leaving it to the matcher. */
  font: 700 0.75rem/1.4 var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}

tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: rgba(240, 138, 61, 0.04); }

td:first-child { white-space: nowrap; }
td:first-child kbd { margin-right: 0.15rem; }

/* --- Callouts ----------------------------------------------------------- */

.callout {
  border: 1px solid var(--border);
  border-left: 2px solid var(--border-lit);
  border-radius: var(--radius);
  background: var(--bg-raised);
  padding: 0.9rem 1.1rem;
  margin: 0 0 1.2rem;
  font-size: 0.93rem;
}
.callout > :last-child { margin-bottom: 0; }
.callout strong { color: var(--text); }

.callout--warn { border-left-color: var(--amber); }
.callout--tip  { border-left-color: var(--rust); }
.callout--note { border-left-color: var(--blue); }

/* A keyboard layout, printed as preformatted text. Mono and tight so the grid
   lines up, with the direction names spaced far enough right to read as a second
   column rather than as part of the key sequence. */
pre.keys {
  font-family: var(--font-mono);
  font-size: 0.88rem;
  line-height: 1.5;
  margin: 0.6rem 0 0;
  color: var(--text-dim);
  white-space: pre;
  overflow-x: auto;
}

.callout__label {
  display: block;
  font: 700 0.7rem/1 var(--font-mono);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 0.5rem;
}
.callout--warn .callout__label { color: var(--amber); }
.callout--tip  .callout__label { color: var(--ember); }
.callout--note .callout__label { color: var(--blue); }

blockquote {
  margin: 0 0 1.2rem;
  padding: 0.1rem 0 0.1rem 1.1rem;
  border-left: 2px solid var(--rust);
  color: var(--text-dim);
  font-style: italic;
}
blockquote p:last-child { margin-bottom: 0; }

/* --- Bestiary / sprite strips ------------------------------------------- */

.sprite-strip {
  display: grid;
  gap: 0.6rem;
  grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
}

.sprite-cell {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.7rem 0.5rem;
  text-align: center;
}
.sprite-cell img {
  width: 48px;
  height: 48px;
  image-rendering: pixelated;
  margin: 0 auto 0.5rem;
}
.sprite-cell b { display: block; font-size: 0.78rem; line-height: 1.3; color: var(--text); }
.sprite-cell span { display: block; font-size: 0.7rem; color: var(--text-faint); margin-top: 0.15rem; }

/* Horizontal marquee of scenery signs. */
.signs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  justify-content: center;
}
.signs img {
  width: 40px;
  height: 40px;
  image-rendering: pixelated;
  opacity: 0.75;
  transition: opacity 0.15s;
}
.signs img:hover { opacity: 1; }

/* --- Page header (inner pages) ------------------------------------------ */

.page-head {
  padding: 2.5rem 0 1.5rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 2rem;
}
.page-head h1 { margin-bottom: 0.3em; }
.page-head p { color: var(--text-dim); max-width: 65ch; margin-bottom: 0; }

/* --- Manual layout ------------------------------------------------------ */

.manual-layout {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: 2.5rem;
  align-items: start;
}

.toc {
  position: sticky;
  top: calc(var(--header-h) + 1rem);
  max-height: calc(100vh - var(--header-h) - 2rem);
  overflow-y: auto;
  font-size: 0.86rem;
  border-left: 2px solid var(--border);
  padding-left: 0.9rem;
}
.toc__label {
  font: 700 0.68rem/1 var(--font-mono);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 0.7rem;
}
.toc ol { list-style: none; margin: 0; padding: 0; }
.toc > ol > li { margin: 0 0 0.35rem; }
.toc a { color: var(--text-dim); display: block; padding: 0.15rem 0; }
.toc a:hover { color: var(--ember); text-decoration: none; }
.toc a.is-active { color: var(--ember); }
.toc ol ol { margin: 0.15rem 0 0.5rem; padding-left: 0.8rem; }
.toc ol ol a { font-size: 0.8rem; color: var(--text-faint); }

.manual-filter {
  width: 100%;
  padding: 0.55rem 0.8rem;
  margin-bottom: 1.5rem;
  background: var(--bg-sunken);
  border: 1px solid var(--border-lit);
  border-radius: var(--radius);
  color: var(--text);
  font: 0.9rem var(--font-body);
}
.manual-filter::placeholder { color: var(--text-faint); }

.manual h2 {
  margin-top: 2.75rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
  scroll-margin-top: calc(var(--header-h) + 1rem);
}
.manual h2:first-of-type { margin-top: 0; border-top: 0; padding-top: 0; }

.manual h3 {
  margin-top: 2rem;
  color: var(--ember);
  font: 700 0.82rem/1.4 var(--font-mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  scroll-margin-top: calc(var(--header-h) + 1rem);
}
.manual h4 {
  margin-top: 1.4rem;
  font-size: 0.98rem;
  color: var(--amber);
  scroll-margin-top: calc(var(--header-h) + 1rem);
}


.manual ul, .manual ol { padding-left: 1.3em; }
.manual li { margin: 0.15em 0; }

.manual .nested { list-style: none; padding-left: 1.1rem; }
.manual .nested > li { position: relative; }
.manual .nested > li::before {
  content: "\2013";
  position: absolute;
  left: -0.9rem;
  color: var(--text-faint);
}

.manual .sublist { list-style: none; padding-left: 0.6rem; margin: 0.2rem 0 0.5rem; }
.manual .sublist > li { position: relative; }
.manual .sublist > li::before {
  content: "\25B8";
  position: absolute;
  left: -0.85rem;
  color: var(--rust);
  font-size: 0.7rem;
  top: 0.25em;
}

.manual .build {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-panel);
  padding: 0.9rem 1.1rem;
  margin: 0 0 1rem;
}
.manual .build > :last-child { margin-bottom: 0; }
.manual .build__title {
  font-weight: 700;
  color: var(--text);
  margin: 0 0 0.15rem;
}
.manual .build__meta { font: 0.78rem var(--font-mono); color: var(--rust); margin: 0 0 0.6rem; }

.manual mark {
  background: rgba(240, 138, 61, 0.20);
  color: #ffe3c9;
  border-radius: 2px;
  padding: 0 0.15em;
}

/* The "(default H)" note on a manual heading, and the inline key hints used on
   the controls page. Deliberately unscoped - both pages use them. */
.h-key {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  margin-left: 0.35rem;
  vertical-align: middle;
  font-weight: 400;
}

.h-src {
  font: 500 0.68rem/1 var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.h-plus,
.h-then {
  font: 500 0.72rem/1 var(--font-mono);
  color: var(--text-faint);
  margin: 0 0.05rem;
}

h3 .h-key kbd,
h4 .h-key kbd {
  font-size: 0.7rem;
  padding: 0.1rem 0.3rem;
  border-bottom-width: 1px;
  color: var(--text-dim);
}

q { quotes: "\201C" "\201D"; color: inherit; }

.is-hidden { display: none !important; }

/* --- Footer ------------------------------------------------------------- */

.site-footer {
  margin-top: 4rem;
  border-top: 1px solid var(--border);
  background: var(--bg-sunken);
  padding: 2rem 0;
  font-size: 0.87rem;
  color: var(--text-faint);
}
.site-footer a { color: var(--text-dim); }
.site-footer a:hover { color: var(--ember); }

.site-footer__grid {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem 3rem;
  justify-content: space-between;
}
.site-footer h4 {
  font: 700 0.7rem/1 var(--font-mono);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 0 0 0.7rem;
}
.site-footer ul { list-style: none; margin: 0; padding: 0; }
.site-footer li { margin: 0.3rem 0; }

.site-footer__legal {
  margin-top: 1.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
  font-size: 0.8rem;
  max-width: 70ch;
}
.site-footer__legal p { margin: 0 0 0.5rem; }

/* --- Responsive --------------------------------------------------------- */

@media (max-width: 900px) {
  .hero__grid { grid-template-columns: 1fr; gap: 2.5rem; }
  .hero__visual { display: none; }
  .shots { grid-template-columns: 1fr; }
}

@media (max-width: 820px) {
  .manual-layout { grid-template-columns: 1fr; }
  .toc {
    position: static;
    max-height: none;
    border-left: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.9rem 1rem;
    background: var(--bg-panel);
  }
  .toc ol ol { display: none; }
}

@media (max-width: 560px) {
  .section { padding: 2.25rem 0; }
  .site-header__inner { gap: 0.5rem; }
  .brand span { display: none; }
}

/* --- Print -------------------------------------------------------------- */

@media print {
  .site-header, .site-footer, .toc, .manual-filter, .btn-row { display: none; }
  body { background: #fff; color: #000; }
  .manual h2 { break-before: page; }
  a { color: #000; text-decoration: underline; }
}

/* --- Screenshot lightbox ---------------------------------------------------
   Driven by :target rather than JavaScript, so a screenshot is expandable on a
   page with scripting off, and the open/close state is a real URL the player can
   bookmark or share. `lightbox.js` only adds Escape-to-close and focus handling,
   which CSS cannot express.

   The state lives on the body, not on the overlay, for two reasons: a
   full-screen fixed overlay on :target alone would still leave the page behind it
   scrollable, and every open control would need the overlay's id hardcoded twice
   (once in the href, once in the markup) for no gain. `body:has(.lightbox:target)`
   is one rule and it is the only place that knows the overlay exists. */

body:has(.lightbox:target) { overflow: hidden; }

/* The affordance. The whole card is the hit target, not just the image, because
   a 1562px-wide screenshot scaled into a 420px column is not a comfortable thing
   to aim at. */
.shot__zoom {
  display: block;
  position: relative;
  border-radius: 2px;
  cursor: zoom-in;
}

.shot__zoom::after {
  content: "Expand";
  position: absolute;
  right: 8px;
  bottom: 8px;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--border-lit);
  border-radius: 3px;
  background: var(--bg-glass);
  color: var(--text-dim);
  font: 500 0.68rem/1 var(--font-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0;
  transition: opacity 120ms ease;
}

.shot__zoom:hover::after,
.shot__zoom:focus-visible::after { opacity: 1; }

/* No hover on a touch device, so the hint would never appear and the cards would
   look unremarkable. Shown outright there instead of hidden. */
@media (hover: none) {
  .shot__zoom::after { opacity: 1; }
}

.lightbox {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 100;
  padding: clamp(1rem, 4vw, 3rem);
  grid-template-rows: auto 1fr;
  gap: 0.75rem;
}

.lightbox:target { display: grid; }

/* The backdrop is a link, so clicking away from the image closes without any
   script. It sits under the panel and fills the viewport, which is why the
   panel needs a higher z-index rather than the backdrop being a pseudo-element:
   a pseudo-element cannot be clicked to navigate. */
.lightbox__backdrop {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: #060504;
  cursor: zoom-out;
}

.lightbox__panel {
  position: relative;
  z-index: 1;
  margin: 0;
  display: grid;
  grid-template-rows: 1fr auto;
  gap: 0.75rem;
  min-height: 0;
}

.lightbox__img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  min-height: 0;
  margin: 0 auto;
  object-fit: contain;
  background: #000;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 30px 90px -30px rgba(0, 0, 0, 1);
}

.lightbox__cap {
  margin: 0;
  text-align: center;
  color: var(--text-dim);
  font: 500 0.8rem/1.5 var(--font-mono);
}

.lightbox__close {
  position: absolute;
  top: clamp(0.5rem, 2vw, 1.5rem);
  right: clamp(0.5rem, 2vw, 1.5rem);
  z-index: 2;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border: 1px solid var(--border-lit);
  border-radius: 50%;
  background: var(--bg-glass);
  color: var(--text);
  font-size: 1.15rem;
  line-height: 1;
  cursor: pointer;
}

.lightbox__close:hover { border-color: var(--ember); color: var(--ember); }

/* The close control is hidden while its overlay is closed, so it never becomes a
   stray tab stop. `:not(:target)` rather than a JS class for the same reason the
   rest of this is CSS. */
.lightbox:not(:target) .lightbox__close { display: none; }

/* Respect a reduced-motion preference for the one transition here. */
@media (prefers-reduced-motion: reduce) {
  .shot__zoom::after { transition: none; }
}
