/* ── Theme tokens ─────────────────────────────────────────────── */
:root {
  --bg:        #f4efe4;
  --bg-2:      #ebe3d3;
  --panel:     #fffdf7;
  --panel-2:   #f7f1e4;
  --ink:       #2b2419;
  --ink-2:     #5c5142;
  --ink-3:     #8b7f6c;
  --line:      #d9cfba;
  --line-2:    #c2b49a;
  --accent:    #a4632a;
  --accent-2:  #7d4a1e;
  --accent-ink:#ffffff;
  --gold:      #b8860b;
  --shadow:    0 1px 2px rgba(43,36,25,.08), 0 8px 24px rgba(43,36,25,.07);
  --radius:    0px;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

html[data-theme="dark"] {
  --bg:        #16130f;
  --bg-2:      #1d1a14;
  --panel:     #221e18;
  --panel-2:   #2a251d;
  --ink:       #ede4d4;
  --ink-2:     #b9ac96;
  --ink-3:     #8a7d68;
  --line:      #3a3227;
  --line-2:    #4d4335;
  --accent:    #d99447;
  --accent-2:  #f0b268;
  --accent-ink:#1a1610;
  --gold:      #e0b452;
  --shadow:    0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.35);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--sans);
  background: var(--bg);
  color: var(--ink);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }

.skip {
  position: absolute; left: -9999px;
  background: var(--accent); color: var(--accent-ink);
  padding: .6rem 1rem; z-index: 100; border-radius: 0;
}
.skip:focus { left: 0; top: 0; }

.muted { color: var(--ink-3); }

/* ── Top bar ──────────────────────────────────────────────────── */
.topbar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 1rem;
  padding: .6rem clamp(.75rem, 3vw, 2rem);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
}

.brand { display: flex; align-items: center; gap: .6rem; text-decoration: none; color: inherit; flex-shrink: 0; }
.brand-mark {
  width: 34px; height: 34px; display: grid; place-items: center;
  background: var(--accent); color: var(--accent-ink);
  border-radius: 0; font-size: 1.1rem;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.1; }
.brand-text strong { font-size: .95rem; letter-spacing: .01em; }
.brand-text small { font-size: .7rem; color: var(--ink-3); }

.topnav { display: flex; align-items: center; gap: .2rem; flex-shrink: 0; margin-left: auto; }
.topnav a {
  padding: .4rem .7rem; border-radius: 0; font-size: .85rem;
  text-decoration: none; color: var(--ink-2);
}
.topnav a:hover { background: var(--panel-2); color: var(--ink); }
.topnav a.on { background: var(--panel-2); color: var(--ink); font-weight: 600; }

.icon-btn {
  width: 32px; height: 32px; margin-left: .3rem;
  border: 1px solid var(--line-2); border-radius: 0;
  background: var(--panel); color: var(--ink-2);
  cursor: pointer; font-size: .9rem;
}
.icon-btn:hover { color: var(--ink); border-color: var(--accent); }
/* The language toggle carries two letters rather than a glyph: a flag would
   name a country instead of a language, and there is no icon for "Spanish"
   that a reader would recognise faster than the word. */
.lang-btn { font: 600 .68rem/1 var(--mono, ui-monospace, monospace); letter-spacing: .06em; }

/* ── Layout ───────────────────────────────────────────────────── */
main { max-width: 1240px; margin: 0 auto; padding: clamp(1.2rem, 4vw, 2.5rem) clamp(.75rem, 3vw, 2rem) 4rem; }

.hero { margin-bottom: 2rem; }
.hero h1 { font-size: clamp(1.6rem, 4vw, 2.3rem); margin: 0 0 .4rem; letter-spacing: -.02em; }
.hero p { margin: 0; color: var(--ink-2); max-width: 62ch; }

.page-head { display: flex; align-items: flex-start; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.6rem; }
.page-head h1 { margin: 0 0 .25rem; font-size: clamp(1.4rem, 3.5vw, 2rem); letter-spacing: -.02em; }
.page-head p { margin: 0; color: var(--ink-2); max-width: 62ch; }
/* The bug on a page title: the far right of the heading block, level with the
   title itself. Boxed and at full strength rather than a faint glyph — a reader
   who has just found a mistake is looking for somewhere to say so, and half the
   value of putting it on the page is that they can find it without hunting. It
   still reads as a control rather than as part of the title, because the frame
   is the one the topbar's own buttons use. */
.hero, .page-head, .item-head, .ws-head { position: relative; }
.page-issue {
  position: absolute; top: 0; right: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px;
  border: 1px solid var(--line-2); background: var(--panel);
  color: var(--ink-2); text-decoration: none;
  transition: color .12s, border-color .12s, background .12s;
}
.page-issue:hover {
  color: var(--accent-ink); border-color: var(--accent); background: var(--accent);
}
.page-issue .sym { width: 20px; height: 20px; }

.back { display: inline-flex; align-items: center; gap: .3rem; font-size: .82rem; text-decoration: none; margin-bottom: .8rem; color: var(--ink-3); }
.back:hover { color: var(--accent); }

/* ── Industry grid ────────────────────────────────────────────── */
.ind-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
.ind-card {
  display: block; text-decoration: none; color: inherit;
  background: var(--panel); border: 1px solid var(--line);
  border-left: 4px solid var(--c, var(--accent));
  border-radius: 0; padding: 1.1rem 1.2rem;
  transition: transform .15s, box-shadow .15s, border-color .15s;
}
.ind-card:hover { transform: translateY(-2px); box-shadow: var(--shadow); border-color: var(--line-2); border-left-color: var(--c); }
.ind-card .ic { font-size: 1.5rem; line-height: 1; }
.ind-card h2 { margin: .5rem 0 .3rem; font-size: 1.02rem; }
.ind-card p { margin: 0 0 .7rem; font-size: .85rem; color: var(--ink-2); }
.ind-card .count { font-size: .72rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3); }

/* ── Recipe cards ─────────────────────────────────────────────── */
.shop-group { margin-bottom: 2rem; }
.shop-head {
  display: flex; align-items: baseline; gap: .6rem;
  padding-bottom: .5rem; margin-bottom: .9rem;
  border-bottom: 1px solid var(--line);
}
.shop-head h2 { margin: 0; font-size: 1rem; letter-spacing: .01em; }
.shop-head .skill { font-size: .78rem; color: var(--ink-3); font-family: var(--mono); }

.rec-grid { display: grid; gap: .85rem; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }

.rec {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 0; padding: .9rem 1rem;
  scroll-margin-top: 5.5rem;   /* clear the sticky header when jumped to by anchor */
}
.rec:target { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
.rec h3 { margin: 0 0 .6rem; font-size: .92rem; display: flex; align-items: center; gap: .45rem; }
.rec .flow { display: flex; flex-direction: column; gap: .4rem; }
.flow-row { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; }
.flow-label {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3); width: 3.1rem; flex-shrink: 0; font-family: var(--mono);
}
.arrow { margin-left: 3.1rem; line-height: 0; padding: .1rem 0; color: var(--ink-2); opacity: .75; }
.arrow-svg { width: 16px; height: 16px; display: block; }
.rec:hover .arrow { color: var(--accent); opacity: 1; }

.chip {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .17rem .55rem; border-radius: 0;
  border: 1px solid var(--line-2); background: var(--panel-2);
  font-size: .8rem; color: var(--ink); text-decoration: none;
  cursor: pointer; transition: border-color .12s, background .12s;
}
.chip:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--panel-2)); }
.chip .qty { font-family: var(--mono); font-size: .72rem; color: var(--accent); font-weight: 600; }
.chip.out { border-color: color-mix(in srgb, var(--gold) 45%, var(--line-2)); }

.needs { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .6rem; }
.need {
  font-size: .7rem; padding: .12rem .45rem; border-radius: 0;
  background: var(--bg-2); border: 1px solid var(--line);
  color: var(--ink-2); font-family: var(--mono);
}

/* Three stacked bars are squarer than the single bar they replaced, so the
   height follows the width rather than being set twice. */
.ingot { width: 18px; aspect-ratio: 27.4 / 19.4; height: auto; flex: none; }
.ingot.big { width: 36px; margin-right: .5rem; }
.ingot.title { width: 21px; }
td .ingot { margin-right: .4rem; vertical-align: -.28em; }  /* chips use flex gap; cells don't */
.chip.is-metal { padding-left: .4rem; }
.item-head h1 { display: flex; align-items: center; gap: 0; }
.plus { color: var(--ink-3); margin: 0 .35rem; }

/* ── Workshop art ─────────────────────────────────────────────── */
.ws-icon { width: 1.15em; height: 1.15em; flex: none; vertical-align: -.2em; }
.ws-icon .ac { fill: var(--accent); stroke: none; }
.ws-icon.lg  { width: 26px; height: 26px; }
.ws-icon.xl  { width: 42px; height: 42px; }
.ws-icon.xxl { width: 60px; height: 60px; }

/* Every plate stands in the same 96×128 frame, whatever it holds: a 3×3
   building's art at 1:1, a 1×1 building's 32×64 art at 2x, or the fallback SVG.
   Nearest-neighbour at a fraction drops whole pixel rows out of pixel art, so
   the only scales in the sheet are whole ones — full and 2x here, half under
   the mobile breakpoint. `pixelated` is the point of the whole rule: the
   browser default smooths them into mud. */
.ws-pic {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  flex: none; width: 96px; height: 128px;
}
.ws-pic img { display: block; image-rendering: pixelated; width: 96px; height: 128px; }
.ws-pic.small img { width: 64px; height: 128px; }
/* Two buildings in one frame need the room for both. Only the workshop page
   asks for it, where nothing is lined up against the art. */
.ws-pic.xxl:has(img + img) { width: auto; }

.chip.shop .ws-icon { color: var(--accent); }

.shop-link { display: flex; align-items: center; gap: .55rem; text-decoration: none; color: inherit; }
.shop-link .ws-icon { color: var(--accent); }
.shop-link:hover h2 { color: var(--accent); }

/* Chip rows over an industry's step list — the same chips as the pickers, but
   standing on the page rather than inside a panel, so they bring their own
   spacing rather than `.brew-filters`. */
.ind-filters { margin: -.4rem 0 1.3rem; display: flex; flex-direction: column; gap: .4rem; }
/* These labels are sentences ("Finished form"), not the one-word ones the
   pickers use, so they get the room to stay on one line. */
.ind-filters .flabel { width: 6.1rem; }

.group-note { margin: -.4rem 0 1rem; font-size: .84rem; color: var(--ink-3); max-width: 62ch; }

/* The industry map opens with a link out to the wiki page it summarises,
   printed as the URL itself so it can be read off the screen and typed
   somewhere else. The URLs are long, so they wrap as a column on narrow
   screens rather than being truncated. */
.wiki-note {
  margin: -.4rem 0 1.6rem; display: flex; flex-wrap: wrap;
  align-items: baseline; gap: .35rem .7rem;
}
/* The label is what tells a reader the row of URLs under it is the game's own
   documentation rather than a citation this site is hedging with, so it is set
   at full ink and bold — the same size as the other small-caps labels on the
   page, carrying more weight than any of them. */
.wiki-label {
  font-size: .64rem; text-transform: uppercase; letter-spacing: .1em;
  font-weight: 700; color: var(--ink); font-family: var(--mono);
}
/* On a building's page the note sits between the blurb and the build keys, so
   it needs its own space on both sides rather than borrowing any back. */
.ws-head .wiki-note { margin: .5rem 0 1.05rem; }
.wiki-list { list-style: none; margin: 0; padding: 0; }
.wiki-list li + li { margin-top: .15rem; }
.wiki-link {
  font-family: var(--mono); font-size: .76rem; color: var(--accent);
  text-decoration: none; border-bottom: 1px solid transparent;
  overflow-wrap: anywhere;
}
/* The host and the /index.php/ are the same on every one of these, so they
   carry no information at all past the first line — dropped to the muted ink
   they read as the address they are, and the article name reads as the link. */
.wiki-stem  { color: var(--ink-3); }
.wiki-topic { color: var(--accent); font-weight: 600; }
.wiki-link:hover { border-bottom-color: var(--accent); }

/* The tier filter reuses the pickers' chips, but it stands on the page rather
   than inside a panel, so it brings its own spacing instead of `.brew-filters`. */
.ws-filters { margin: -.6rem 0 1.4rem; }

/* `hidden` alone loses to the card's own `display: flex`, and to the section's
   block — the attribute selector is what actually takes them out of the grid. */
.ws-card[hidden], .shop-group[hidden] { display: none; }

.ws-grid { display: grid; gap: .85rem; grid-template-columns: repeat(auto-fill, minmax(370px, 1fr)); }
.ws-card {
  display: flex; gap: .9rem; align-items: flex-start;
  padding: 1rem; text-decoration: none; color: inherit;
  background: var(--panel); border: 1px solid var(--line);
  transition: border-color .15s, box-shadow .15s;
}
.ws-card:hover { border-color: var(--accent); box-shadow: var(--shadow); }
.ws-art { color: var(--ink-2); flex: none; }
.ws-card:hover .ws-art { color: var(--accent); }
.ws-body { min-width: 0; }
.ws-card h3 { margin: 0 0 .3rem; font-size: .95rem; }
.ws-card p { margin: 0 0 .6rem; font-size: .82rem; color: var(--ink-2); }
.ws-meta { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; }
.ws-meta .count {
  font-size: .7rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--accent); font-family: var(--mono);
}

.ws-head { display: flex; gap: 1.2rem; align-items: flex-start; margin-bottom: 1.8rem; }
.ws-head h1 { margin: 0 0 .5rem; font-size: clamp(1.4rem, 3.5vw, 2rem); letter-spacing: -.02em; }
.ws-head .ws-art { color: var(--accent); padding-top: .2rem; }
.ws-head .item-note { margin: .7rem 0 0; }
.ws-inds { font-size: .82rem; margin: .5rem 0 0; }

/* Tier is the one badge in the row that is a number rather than a word, so it
   gets the accent border to stop it reading as another skill name. */
.need.tier { border-color: var(--line-2); color: var(--ink); }

.ws-build { font-size: .82rem; color: var(--ink-3); margin: .6rem 0 0; }
.ws-build .dot { margin: 0 .15rem; }
.keys { display: inline-flex; gap: .18rem; vertical-align: -.1em; }
kbd {
  font-family: var(--mono); font-size: .72rem; line-height: 1;
  padding: .2rem .35rem; min-width: 1.5em; text-align: center;
  background: var(--panel-2); border: 1px solid var(--line-2);
  border-bottom-width: 2px; color: var(--ink);
}

/* ── Item detail ──────────────────────────────────────────────── */
.item-head { margin-bottom: 1.5rem; }
.item-head h1 { margin: 0 0 .3rem; font-size: clamp(1.4rem, 3.5vw, 2rem); }
.item-head .item-note { color: var(--ink-2); max-width: 66ch; margin: .5rem 0 0; }

.two-col { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); align-items: start; }
.col-head {
  font-size: .74rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--ink-3); margin: 0 0 .8rem; font-family: var(--mono);
}
.col-head .n { color: var(--accent); }
.stack { display: flex; flex-direction: column; gap: .7rem; }
.none { color: var(--ink-3); font-size: .85rem; font-style: italic; }

/* ── Tables ───────────────────────────────────────────────────── */
.ref-block { margin-bottom: 2.4rem; scroll-margin-top: 5rem; }
.ref-block h2 { margin: 0 0 .25rem; font-size: 1.1rem; }
.ref-block > p { margin: 0 0 .9rem; color: var(--ink-2); font-size: .88rem; }
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 0; background: var(--panel); }
table { border-collapse: collapse; width: 100%; font-size: .86rem; }
th, td { padding: .5rem .8rem; text-align: left; border-bottom: 1px solid var(--line); vertical-align: top; }
th { background: var(--panel-2); font-size: .72rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3); white-space: nowrap; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--panel-2); }
td:first-child { font-weight: 600; white-space: nowrap; }

.toc { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 2rem; }
.toc a {
  font-size: .8rem; padding: .3rem .7rem; border-radius: 0;
  border: 1px solid var(--line-2); background: var(--panel);
  text-decoration: none; color: var(--ink-2);
}
.toc a:hover { border-color: var(--accent); color: var(--accent); }

/* ── Graph ────────────────────────────────────────────────────── */
/* ── Industry map ─────────────────────────────────────────────── */
/* One view where there used to be two tabs. Full step cards, wired together,
   running down the page rather than across it — which is what buys it out of
   pan and zoom entirely: the page already scrolls, so the map has no viewport
   of its own to be trapped inside and no controls beyond the chips.

   Rows are laid out by flexbox and the wires are drawn afterwards from the
   boxes the browser actually produced, so nothing here has to agree with the
   layout twice. That is also what makes the mobile fallback free: rows wrap,
   the boxes move, the wires are redrawn to wherever they landed. */
/* How big the map the chips settled on is. The chip itself says which path
   this is, so the count is all that is left to write above the map. */
.fm-count {
  display: block; margin: 1.4rem 0 0; white-space: nowrap;
  font-size: .72rem; font-family: var(--mono);
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3);
}
.fm-legend { margin: 1.4rem 0 0; font-size: .78rem; color: var(--ink-3); max-width: 66ch; }

.fmap { position: relative; margin-top: 1.2rem; }
/* The wires sit under the cards and take no pointer events, so a card is still
   a card: hovering, clicking and tabbing all behave as if the SVG weren't there. */
.fmap-wires {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 0; overflow: visible;
}
.fmap-row {
  position: relative; z-index: 1;
  display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-start;
  gap: 1.6rem; padding-bottom: 2.6rem;
}
.fmap-row:last-child { padding-bottom: 0; }

/* A waypoint is not drawn — it is a claim on horizontal space. A wire that
   skips a row passes through one of these, so it gets a lane of its own
   between the cards instead of running underneath them. */
.f-way { width: 0; align-self: stretch; flex: none; }

/* No arrowheads: the map runs down the page and every wire on it means "next",
   so a tip on each one only crowded the top edge of the card it landed on.

   Every wire is straight legs and square corners, so the joins are rounded:
   the corner itself is eased in the path, and this catches the few that are
   too short to ease. */
.fm-wire {
  fill: none; stroke: var(--line-2); stroke-width: 1.6;
  stroke-linejoin: round; stroke-linecap: round;
  transition: stroke .12s, opacity .12s;
}
.fm-wire.hot { stroke: var(--c, var(--accent)); stroke-width: 2.4; }
.fm-wire.lit { stroke: var(--accent); stroke-width: 2.4; }
.fmap.picked .fm-wire:not(.hot):not(.lit) { opacity: .22; }

/* Items are the spine: what the fortress is holding between one job and the
   next. They stay small so the workshops are what the eye lands on. */
.f-item {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .34rem .7rem; text-decoration: none;
  background: var(--panel-2); border: 1px solid var(--line-2);
  font-size: .92rem; font-weight: 600; color: var(--ink);
  transition: border-color .12s, background .12s;
}
.f-item:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--panel-2)); }
.f-item .sprite { width: 18px; height: 18px; }
.fi-link { display: inline-flex; align-items: center; gap: .4rem; color: inherit; text-decoration: none; }
.fi-link:hover { color: var(--accent); }
/* A job whose products are all dead ends collapses into one node listing them.
   It wraps at a readable measure rather than stretching its row to the width of
   whatever the carpenter can make. */
.f-many { display: flex; flex-wrap: wrap; gap: .1rem .7rem; max-width: 21rem; font-weight: 500; }
.f-many .fi-link::after { content: '·'; color: var(--ink-3); margin-left: .7rem; }
.f-many .fi-link:last-of-type::after { content: none; }
/* Dashed means it comes from outside this map; gold means the branch stops
   here. Between them they mark every point where a reader has to look
   somewhere else, which is the whole job of the tags. */
.f-item.src { border-style: dashed; font-weight: 500; color: var(--ink-2); background: var(--bg-2); }
.f-item.end { border-color: color-mix(in srgb, var(--gold) 60%, var(--line-2)); box-shadow: inset 0 -2px 0 color-mix(in srgb, var(--gold) 40%, transparent); }
.fi-tag { display: inline-flex; line-height: 0; margin-right: .1rem; }
.fi-tag .sym { width: 1.1em; height: 1.1em; }
.fi-tag.src { color: var(--ink-3); }
.fi-tag.end { color: var(--gold); }
.fm-legend .sym { width: 1.15em; height: 1.15em; vertical-align: -.22em; }

/* The step card. Everything the old Steps tab put on a recipe — workshop,
   skill, containers, fuel, the note — on the node itself, so picking a path
   does not mean reading a second list somewhere else on the page. */
/* `align-self: stretch` is what makes a row of cards one height: flexbox
   already knows which is tallest, so nothing has to measure anything. The note
   is what used to break it — three sentences on one card and none on the one
   beside it — so it lives behind the chevron, and the card is a column with the
   chevron pushed to the foot by `margin-top: auto`, which is what keeps the
   chevrons on a row lined up whatever is above them. */
.f-job {
  position: relative; flex: none; width: 258px; max-width: 100%;
  align-self: stretch; display: flex; flex-direction: column;
  background: var(--panel); border: 1px solid var(--line);
  padding: 1rem .9rem; text-align: center;
  transition: border-color .15s, box-shadow .15s, opacity .15s, filter .15s;
}
.f-job:hover { border-color: var(--accent); box-shadow: var(--shadow); }
/* The building's name caps the card above its own artwork, so it reads as the
   plate's label. It carries no icon: the plate underneath is already the
   picture of the building, and a second miniature of the same thing an inch
   above it only halves the room the name has to be read in. It is a link to the
   workshop page but is not styled as a chip — at the top of a card a bordered
   pill would compete with the step number sitting in the same corner. */
.fjob-shop {
  display: block; margin: -.15rem 0 .6rem; padding-bottom: .55rem;
  border-bottom: 1px solid var(--line);
  font-size: 1rem; font-weight: 600; letter-spacing: -.01em;
  color: var(--ink); text-decoration: none;
}
.fjob-shop:hover { color: var(--accent); }
.fjob-art {
  display: flex; justify-content: center; margin-bottom: .6rem;
  color: var(--ink-2); text-decoration: none;
}
.fjob-art:hover { color: var(--accent); }
.f-job:hover .fjob-art, .f-group:hover .fjob-art { color: var(--accent); }
.fjob-txt { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.f-job h3 { margin: 0 0 .55rem; font-size: .92rem; }
.f-job .needs { justify-content: center; }
.f-job .needs:empty { display: none; }


/* On a step card a spelled-out badge is most of the card's width, so the map
   draws the symbol alone and leaves the words to the tooltip. Everywhere else
   — a workshop page, an item page — has the room and keeps them. */
.need.only-sym { padding: .12rem .32rem; }
.need.only-sym .sym { width: 1.25em; height: 1.25em; }

/* One building, several jobs. A glass furnace makes green glass, clear glass,
   crystal glass and glass goods, and four cards of it is the same plate four
   times over for jobs that share one hearth. So the building gets one card and
   its jobs get another beside it: every wire on that stretch of the chain lands
   on the building, and the card next to it says what the building can be told
   to do. The two are separate cards rather than two halves of one, because the
   wires belong to the first and not to the second, and a single border around
   both would claim otherwise. */
.f-group {
  align-self: stretch; flex: none; max-width: 100%;
  display: flex; align-items: stretch; gap: .8rem;
}
.f-shopcard, .f-types {
  background: var(--panel); border: 1px solid var(--line);
  transition: border-color .15s, box-shadow .15s, opacity .15s, filter .15s;
}
.f-group:hover .f-shopcard, .f-group:hover .f-types {
  border-color: var(--accent); box-shadow: var(--shadow);
}
.f-shopcard {
  position: relative; flex: none; width: 158px;
  display: flex; flex-direction: column; align-items: center;
  padding: 1rem .8rem; text-align: center;
}
.f-shopcard .fjob-shop { width: 100%; }
/* The building card stretches to whatever the card of types beside it needs,
   so the plate takes the middle of that room rather than leaving all of it
   underneath. */
.f-shopcard .fjob-art { flex: 1; align-items: center; margin: 0; }
/* Where every job under this roof wants the same skill and the same containers,
   the badges say so once, at the foot of the building rather than on four lines
   that would each repeat it. */
.f-shopcard .needs { justify-content: center; margin-top: .7rem; }
/* Wide enough for a job title and its badges and no wider: the second column
   this card used to run — the job on the left, what the wiki said about it on
   the right — went with the notes. */
.f-types {
  list-style: none; margin: 0; padding: 1rem;
  flex: none; width: 260px; max-width: 100%; text-align: left;
}
.f-types > li + li { margin-top: .7rem; padding-top: .7rem; border-top: 1px solid var(--line); }

/* Inside the card of types a job is a line, not a card: no border, no
   background, the number beside the name rather than hanging off a corner it
   would have to share with its neighbour. */
.f-job.in-list {
  width: auto; border: 0; background: none; padding: 0; box-shadow: none;
  flex-direction: row; align-items: flex-start; gap: .5rem; text-align: left;
}
.f-job.in-list:hover { box-shadow: none; }
.f-job.in-list .fnum {
  position: static; flex: none; width: 1.45rem; height: 1.45rem; font-size: .74rem;
}
.f-job.in-list .fnum:empty { display: none; }
.f-job.in-list h3 { margin: 0 0 .35rem; }
.f-job.in-list .needs { justify-content: flex-start; }
.f-job.in-list .fjob-txt { flex: 1; min-width: 0; }
/* Faded by the chips only when the path skips every job under this roof. */
.f-group.dim .f-shopcard { opacity: .32; filter: grayscale(1); }

.fnum {
  position: absolute; top: -.72rem; left: -.72rem;
  width: 1.75rem; height: 1.75rem; display: grid; place-items: center;
  background: var(--accent); color: var(--accent-ink);
  font-family: var(--mono); font-size: .82rem; font-weight: 600;
}
.fnum:empty { display: none; }

/* Picking a path fades what it skipped rather than removing it: the point of
   keeping the map is that you can still see the branch you did not take.
   Hovering one brings it back. */
.fmap.picked .fnode:not(.on) { opacity: .32; filter: grayscale(1); }
.fmap.picked .fnode:not(.on):hover { opacity: .85; filter: none; }

/* ── About ────────────────────────────────────────────────────── */
.prose { max-width: 68ch; }
.prose h2 { font-size: 1.05rem; margin: 1.8rem 0 .5rem; }
.prose p, .prose li { color: var(--ink-2); }
.prose code { font-family: var(--mono); font-size: .85em; background: var(--panel-2); padding: .1rem .35rem; border-radius: 0; color: var(--ink); }
.prose pre { background: var(--panel); border: 1px solid var(--line); border-radius: 0; padding: .9rem 1rem; overflow-x: auto; }
.prose pre code { background: none; padding: 0; font-size: .8rem; line-height: 1.6; }
.prose ol, .prose ul { padding-left: 1.4rem; }
/* Steps carry their own code block, so they need more air between them than a
   plain bullet list does. */
.prose ol li { margin: .55rem 0; }
.prose li > pre { margin: .5rem 0 0; }

/* ── Footer ───────────────────────────────────────────────────── */
.foot {
  border-top: 1px solid var(--line); padding: 1.5rem clamp(.75rem, 3vw, 2rem);
  font-size: .8rem; color: var(--ink-3); text-align: center;
}
.foot p { margin: .2rem 0; }

@media (max-width: 720px) {
  .topbar { flex-wrap: wrap; row-gap: .5rem; }
  .brand-text { display: none; }
  .topnav { margin-left: auto; }
  .topnav a { padding: .4rem .5rem; font-size: .82rem; }
  .page-head h1, .item-head h1 { font-size: 1.35rem; }
  .ws-head { gap: .8rem; }
  .ws-icon.xxl { width: 44px; height: 44px; }
  /* Half scale — the one other ratio that divides the art evenly. */
  .ws-pic { width: 48px; height: 64px; gap: 4px; }
  .ws-pic img { width: 48px; height: 64px; }
  .ws-pic.small img { width: 32px; height: 64px; }
  /* The map falls back to a single column: the rows already wrap, and a card
     narrow enough to sit two-up here would have no room for its note. */
  .f-job { width: 100%; }
  .fmap-row { gap: 1rem; padding-bottom: 2rem; }
  /* A building and its card of types stay side by side — they are one thing
     read across — but at their own widths rather than the desktop's, or the
     pair hangs off the edge and the page scrolls sideways. */
  .f-group { width: 100%; gap: .6rem; }
  .f-shopcard { width: 96px; padding: .8rem .5rem; }
  .f-shopcard .fjob-shop { font-size: .82rem; }
  .f-types { flex: 1; width: auto; min-width: 0; padding: .8rem; }

  /* Picking a path fades the rest of the map on a wide screen, where the branch
     you did not take sits beside the one you did and is worth seeing. Here the
     map is one card wide, so that branch is not context — it is screenfuls of
     grey between the steps you are trying to read, and the step after this one
     is always somewhere below the fold. So the path takes the map with it:
     what it skipped leaves the flow, the rows it emptied close up, and what is
     left is the line, numbered 1..n down the page. The chips still hold the
     whole picture — Everything is one tap away and carries no step list, so it
     is never the thing being narrowed. */
  .fmap.picked .fnode:not(.on),
  .fmap.picked .f-group.dim,
  .fmap.picked .f-types > li:not(:has(.f-job.on)),
  .fmap.picked .fm-wire:not(.hot) { display: none; }
  /* A row with nothing left on it would otherwise hold its own height open. */
  .fmap.picked .fmap-row:not(:has(.fnode.on)) { display: none; }
  /* The divider between two jobs under one roof goes on the second of what is
     left, not the second of what was written — otherwise a card whose first job
     the path skipped opens with a rule across the top of nothing. */
  .fmap.picked .f-types > li { margin-top: 0; padding-top: 0; border-top: 0; }
  .fmap.picked .f-types > li:has(.f-job.on) ~ li:has(.f-job.on) {
    margin-top: .7rem; padding-top: .7rem; border-top: 1px solid var(--line);
  }
}

/* ── Symbols ──────────────────────────────────────────────────── */
/* The shared set from data/icons.js. Sized in em so a symbol scales with
   whatever it sits in — a heading, a badge, a table-of-contents chip — and
   inherits that context's colour through currentColor. */
.sym { width: 1.05em; height: 1.05em; flex: none; vertical-align: -.16em; }

.need {
  display: inline-flex; align-items: center; gap: .3rem;
  line-height: 1.5;
}
/* The badges set .7rem type; a symbol at 1.15em of that muddies into a blob,
   so it runs a shade larger than its words. */
.need .sym { width: 1.35em; height: 1.35em; vertical-align: baseline; }

/* Industry symbols take the industry's own colour rather than the body ink. */
.ind-card .ic { color: var(--c, var(--accent)); }
.sym.ind { width: 32px; height: 32px; }
.page-head h1 .sym { color: var(--accent); margin-right: .1em; }
.ws-inds .sym { color: var(--accent); margin-right: .1em; }
.toc a .sym, .ref-block h2 .sym { color: var(--accent); margin-right: .15em; }

.brand-mark svg { width: 20px; height: 20px; display: block; }
.icon-btn svg { width: 17px; height: 17px; display: block; margin: 0 auto; }

/* ── The Still's brewing picker ───────────────────────────────── */
.brew {
  display: grid; gap: 1rem; align-items: start;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  margin-bottom: 2rem;
}

.brew-pick { border: 1px solid var(--line); background: var(--panel); }

.brew-filters { padding: .7rem .8rem .5rem; border-bottom: 1px solid var(--line); }
/* The pickers' labels run to two words — "Found in", "Made of" — and the column
   has to hold the longest of them, or the label butts against the select. */
.brew-filters .flabel { width: 4.4rem; }
.frow { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem; }
.frow + .frow { margin-top: .4rem; }
.flabel {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3); width: 3.6rem; flex-shrink: 0; font-family: var(--mono);
}
.fchip {
  padding: .18rem .55rem; font: inherit; font-size: .74rem;
  border: 1px solid var(--line-2); border-radius: 0;
  background: var(--panel-2); color: var(--ink-2); cursor: pointer;
}
.fchip:hover { border-color: var(--accent); color: var(--ink); }
.fchip.on { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }

/* Every facet of a picker is a select. It borrows the calculator's appearance
   below rather than inventing a second one — the only difference is the width,
   which follows the widest option here instead of filling a column. `on` is
   what a chip's accent fill used to say: a filter that is doing something has
   to look different from one that is not, or the only clue that the list is
   short is that the list is short. */
.frow .fsel { width: auto; min-width: 9rem; max-width: 100%; }
.frow .fsel.on { border-color: var(--accent); color: var(--ink); font-weight: 600; }

/* The mode select names the table; the facets under it only narrow that table.
   A rule between them is what stops the smelter's Job reading as a third filter
   sitting alongside Metal and Found in. */
.mode-row { padding-bottom: .5rem; border-bottom: 1px dashed var(--line); }
.mode-row + .frow { margin-top: .5rem; }
.frow .msel { font-weight: 600; color: var(--ink); border-color: var(--line-2); }

/* The mark for the value in force, for a facet whose values carry one — a
   select cannot draw anything inside an option. It sits after the select, so
   the rows stay aligned whether or not the facet is set to anything. */
.fmark { display: inline-flex; align-items: center; margin-left: .1rem; }
.fmark .swatch { width: .8rem; height: .8rem; }

.brew-search {
  width: 100%; padding: .45rem .8rem;
  font: inherit; font-size: .82rem;
  background: var(--panel); color: var(--ink);
  border: 0; border-bottom: 1px solid var(--line);
  outline: none;
}
.brew-search:focus { border-bottom-color: var(--accent); }
.brew-search::placeholder { color: var(--ink-3); }

/* Tall enough to show the shape of the list, short enough that the answer
   panel stays on screen next to it. */
.brew-list { max-height: 26rem; overflow-y: auto; }
.brew-list button {
  display: flex; width: 100%; gap: .6rem; align-items: baseline;
  padding: .38rem .8rem; border: 0; border-radius: 0;
  background: none; color: inherit; font: inherit; font-size: .82rem;
  text-align: left; cursor: pointer;
}
.brew-list button:hover { background: var(--panel-2); }
.brew-list button.on { background: color-mix(in srgb, var(--accent) 14%, var(--panel)); font-weight: 600; }
.brew-list button.on .bi { color: var(--accent); }
.brew-list .bo { margin-left: auto; font-size: .76rem; color: var(--ink-3); font-weight: 400; }
.brew-list .none { padding: .9rem .8rem; }

.brew-panel { position: sticky; top: 4.6rem; }
.brew-out { border: 1px solid var(--line); background: var(--panel); padding: 1.1rem 1.2rem; }
.brew-flow { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.brew-flow .chip { font-size: .95rem; padding: .3rem .7rem; }
/* The connector points down everywhere else; here the flow runs across. */
.brew-arrow { line-height: 0; color: var(--ink-3); transform: rotate(-90deg); }
.brew-arrow .arrow-svg { width: 18px; height: 18px; }

.brew-meta { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; margin-top: .9rem; }
.need.kind { font-weight: 600; color: var(--ink); }
.need.kind[data-kind="Wine"]  { border-color: #9d5c7d; }
.need.kind[data-kind="Beer"]  { border-color: #b08a3a; }
.need.kind[data-kind="Cider"] { border-color: #8a9d5c; }
.need.kind[data-kind="Other"] { border-color: var(--line-2); }
.val {
  font-family: var(--mono); font-size: .72rem; font-weight: 600;
  padding: .12rem .45rem; border: 1px solid var(--gold);
  color: var(--gold); background: var(--bg-2);
}

.brew-job {
  margin: .9rem 0 0; padding-top: .7rem; border-top: 1px dashed var(--line);
  font-size: .82rem; color: var(--ink-2);
}
.brew-job strong { color: var(--ink); }

.brew-else { margin-top: .9rem; padding-top: .7rem; border-top: 1px dashed var(--line); }
.brew-else .col-head { margin-bottom: .5rem; }

@media (max-width: 780px) {
  .brew { grid-template-columns: 1fr; }
  .brew-panel { position: static; }
  .brew-list { max-height: 15rem; }
}

/* ── The still, animated ──────────────────────────────────────── */
/* The base rules are the FINISHED state — vessel full, plant already in. The
   keyframes run from empty up to that, so anyone who has asked for reduced
   motion simply sees the finished picture instead of nothing. */
.brew-anim {
  --brew: var(--accent);
  display: block; width: 100%; max-width: 280px; height: auto;
  margin: 0 auto .9rem; color: var(--ink-2);
}
.brew-anim[data-kind="Wine"]  { --brew: #9d5c7d; }
.brew-anim[data-kind="Beer"]  { --brew: #b08a3a; }
.brew-anim[data-kind="Cider"] { --brew: #8a9d5c; }

.brew-anim .liq  { fill: var(--brew); opacity: .62; }
.brew-anim .bub  { fill: var(--panel); opacity: 0; }
.brew-anim .drip { fill: var(--brew); opacity: 0; }
.brew-anim .seed { opacity: 0; }

@keyframes brew-fall {
  0%        { transform: translateY(0);    opacity: 0; }
  15%       { opacity: 1; }
  75%       { transform: translateY(13px); opacity: 1; }
  100%      { transform: translateY(18px); opacity: 0; }
}
@keyframes brew-fill { from { transform: translateY(46px); } to { transform: translateY(0); } }
@keyframes brew-keg  { from { transform: translateY(20px); } to { transform: translateY(0); } }
@keyframes brew-bub {
  0%   { transform: translateY(0);     opacity: 0; }
  25%  { opacity: .75; }
  100% { transform: translateY(-26px); opacity: 0; }
}
@keyframes brew-drip {
  0%   { transform: translateY(0);    opacity: 0; }
  20%  { opacity: .9; }
  100% { transform: translateY(13px); opacity: 0; }
}

.brew-anim .seed { animation: brew-fall .62s ease-in both; }
.brew-anim .liq  { animation: brew-fill 1.1s .38s cubic-bezier(.4, 0, .2, 1) both; }
.brew-anim .keg  { animation: brew-keg 1.2s 1.5s ease-out both; }
.brew-anim .bub  { animation: brew-bub 2s 1.1s ease-in-out infinite both; }
.brew-anim .b2   { animation-duration: 2.4s; animation-delay: 1.6s; }
.brew-anim .b3   { animation-duration: 1.7s; animation-delay: 2.1s; }
.brew-anim .drip { animation: brew-drip 1.5s 1.4s ease-in infinite both; }

@media (prefers-reduced-motion: reduce) {
  .brew-anim * { animation: none !important; }
  .brew-anim .seed, .brew-anim .bub, .brew-anim .drip { opacity: 0; }
}

/* ── Colour swatches ──────────────────────────────────────────── */
/* The hex behind these is an approximation of a name the game gives without a
   value, so the swatch is a hint next to the name, never a substitute for it. */
.swatch {
  display: inline-block; width: .8em; height: .8em; flex: none;
  margin-right: .4em; vertical-align: -.05em;
  background: var(--sw); border: 1px solid color-mix(in srgb, var(--ink) 35%, transparent);
}
.need .swatch { margin-right: .35em; }

/* ── The quern, animated ──────────────────────────────────────── */
.mill-anim { --brew: #c9ae72; }
.mill-anim[data-kind="Sugar"] { --brew: #e3d6b6; }
.mill-anim[data-kind="Dye"]   { --brew: #7a6fa8; }   /* overridden per dye inline */

.mill-anim .pow  { fill: var(--brew); opacity: .78; }
.mill-anim .eye  { fill: var(--brew); opacity: .5; }
.mill-anim .dust { fill: var(--brew); opacity: 0; }
.mill-anim .grain { opacity: 0; }

@keyframes mill-drop {
  0%   { transform: translateY(0);    opacity: 0; }
  15%  { opacity: 1; }
  80%  { transform: translateY(20px); opacity: 1; }
  100% { transform: translateY(26px); opacity: 0; }
}
/* A quern handle is a peg near the rim that you push round, so it travels the
   ellipse of the stone rather than spinning on the spot. */
@keyframes mill-turn {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-24px, 6px); }
  50%  { transform: translate(-48px, 0); }
  75%  { transform: translate(-24px, -6px); }
  100% { transform: translate(0, 0); }
}
@keyframes mill-dust {
  0%   { transform: translate(0, 0);      opacity: 0; }
  25%  { opacity: .85; }
  100% { transform: translate(9px, 22px); opacity: 0; }
}

.mill-anim .grain  { animation: mill-drop .7s ease-in both; }
.mill-anim .handle { animation: mill-turn 2.6s .3s linear infinite; }
.mill-anim .pow    { animation: brew-keg 1.4s 1s ease-out both; }
.mill-anim .dust   { animation: mill-dust 1.6s 1.1s ease-in infinite both; }
.mill-anim .d2     { animation-duration: 2s; animation-delay: 1.7s; }

.need.kind[data-kind="Flour"] { border-color: #b08a3a; }
.need.kind[data-kind="Sugar"] { border-color: #c2b28a; }
.need.kind[data-kind="Dye"]   { border-color: #7a6fa8; }

@media (prefers-reduced-motion: reduce) {
  .mill-anim * { animation: none !important; }
  .mill-anim .grain, .mill-anim .dust { opacity: 0; }
}

/* On an item page the card runs the full width, so the animation sits beside
   the answer rather than stacked on top of it. */
@media (min-width: 720px) {
  .brew-out.beside {
    display: grid; gap: 1.4rem; align-items: center;
    grid-template-columns: minmax(0, 240px) minmax(0, 1fr);
  }
  .brew-out.beside .brew-anim { margin: 0; max-width: 100%; }
  .brew-out.beside > *:not(.brew-anim) { grid-column: 2; }
  .brew-out.beside .brew-anim { grid-row: 1 / span 3; }
}

/* ── The dyer's vat, animated ─────────────────────────────────── */
.dye-anim { --brew: var(--accent); }
.dye-anim .bath   { fill: var(--brew); opacity: .8; }
.dye-anim .ripple { fill: none; stroke: var(--panel); stroke-width: 1.6; opacity: 0; }
.dye-anim .cloth.dyed { fill: var(--brew); opacity: 0; }
.dye-anim .cloth.raw  { opacity: 1; }

@keyframes dye-dip   { from { transform: translateY(0); }    to { transform: translateY(26px); } }
@keyframes dye-lift  { from { transform: translateY(26px); } to { transform: translateY(0); } }
@keyframes dye-hide  { to { opacity: 0; } }
@keyframes dye-show  { to { opacity: 1; } }
@keyframes dye-ripple {
  0%   { transform: scale(.3); opacity: 0; }
  30%  { opacity: .7; }
  100% { transform: scale(1.9); opacity: 0; }
}

/* Down, swap at the bottom of the dip, back up wearing the colour. */
.dye-anim .cloth.raw {
  animation: dye-dip .75s ease-in both, dye-hide .01s .75s linear both;
}
.dye-anim .cloth.dyed {
  animation: dye-lift .8s 1.15s ease-out both, dye-show .01s 1.15s linear both;
}
.dye-anim .ripple {
  transform-box: fill-box; transform-origin: center;
  animation: dye-ripple 1.8s .9s ease-out infinite both;
}
.dye-anim .r2 { animation-delay: 1.7s; }

.need.kind[data-fam] { font-weight: 600; color: var(--ink); }
.need.kind[data-fam="Blue"]   { border-color: #4a6f9d; }
.need.kind[data-fam="Green"]  { border-color: #5c8a4a; }
.need.kind[data-fam="Red"]    { border-color: #a8483c; }
.need.kind[data-fam="Pink"]   { border-color: #b96f8c; }
.need.kind[data-fam="Purple"] { border-color: #7a6fa8; }
.need.kind[data-fam="Orange"] { border-color: #c07a35; }
.need.kind[data-fam="Yellow"] { border-color: #b09a3a; }
.need.kind[data-fam="Brown"]  { border-color: #8a6a45; }
.need.kind[data-fam="Neutral"]{ border-color: var(--line-2); }

/* A name the graph has no item for: it reads as a chip but does not pretend to
   be a link. */
.chip.flat { cursor: default; }
.chip.flat:hover { border-color: var(--line-2); background: var(--panel-2); }

@media (prefers-reduced-motion: reduce) {
  .dye-anim * { animation: none !important; }
  .dye-anim .cloth.raw  { opacity: 0; }
  .dye-anim .cloth.dyed { opacity: 1; }
  .dye-anim .ripple { opacity: 0; }
}

/* A filter chip carrying a colour square, and the same square in the list. */
.fchip { display: inline-flex; align-items: center; }
.fchip .swatch { width: .72em; height: .72em; margin-right: .4em; }
/* On the selected chip the square sits on the accent, so its hairline needs to
   read against that rather than against the paper. */
.fchip.on .swatch { border-color: color-mix(in srgb, var(--accent-ink) 55%, transparent); }
.brew-list .bo .swatch { width: .7em; height: .7em; margin-right: .4em; vertical-align: -.02em; }

/* ── The furnace, animated ────────────────────────────────────── */
/* One picture for both of the smelter's jobs, told in the order the job
   happens: the feed drops, the melt brightens, the tap runs, the bar sets. The
   narrative parts play once — a panel that keeps re-melting its bar is a
   distraction next to the text explaining it — and only the ambience loops:
   the fire, the glow, the chimney and the sparks. */
.smelt-anim { --brew: var(--accent); }
.smelt-anim .fire  { fill: var(--accent); opacity: .9; transform-box: fill-box; transform-origin: bottom center; }
.smelt-anim .spark { fill: var(--accent); opacity: 0; }
.smelt-anim .puff  { fill: currentColor; opacity: 0; transform-box: fill-box; transform-origin: center; }
/* The melt inside the mouth, clipped to the arch, and the stream out of the
   tap: both are the metal being made, so both take its colour. */
.smelt-anim .glow  { fill: var(--brew); opacity: .3; }
.smelt-anim .pour  { stroke: var(--brew); stroke-dasharray: 5 4; opacity: 0; }
.smelt-anim .feed  { opacity: 0; }
/* Drawn at 1.31x by a nested transform, so the stroke is set below the ingot's
   1.4 to land on the same rendered weight; the feed's bars are at .62 and the
   second bar at .86, and each sets the stroke its own scale needs. */
.smelt-anim .bar path,
.smelt-anim .in-bar {
  fill: var(--out, var(--brew)); stroke: #000; stroke-opacity: .45;
  stroke-width: 1.1; stroke-linejoin: round;
}
.smelt-anim .in-bar { fill: var(--in, var(--brew)); stroke-width: 2.3; }
.smelt-anim .b2 path { stroke-width: 1.9; }
/* The ore's second bar is a chance, not a promise — galena pays silver half the
   time — so it is drawn as an outline barely filled in. */
.smelt-anim .bar.maybe path { fill-opacity: .45; stroke-dasharray: 2.4 2; stroke-opacity: .6; }
.smelt-anim .bar { opacity: 1; }

/* The drop lands inside the mouth of the furnace rather than beside it: the
   feed is drawn where a lump of ore sits in the hand, and the arch is a little
   to the left of that. */
@keyframes smelt-feed {
  0%   { transform: translate(0, 0);        opacity: 0; }
  15%  { opacity: 1; }
  80%  { transform: translate(-2px, 40px);  opacity: 1; }
  100% { transform: translate(-2px, 46px);  opacity: 0; }
}
@keyframes smelt-flare {
  0%, 100% { transform: scaleY(1)    scaleX(1);    opacity: .85; }
  50%      { transform: scaleY(1.18) scaleX(.92);  opacity: 1; }
}
@keyframes smelt-glow {
  0%, 100% { opacity: .26; }
  50%      { opacity: .66; }
}
@keyframes smelt-spark {
  0%   { transform: translateY(0);     opacity: 0; }
  30%  { opacity: .9; }
  100% { transform: translateY(-14px); opacity: 0; }
}
@keyframes smelt-puff {
  0%   { transform: translateY(0)     scale(.55); opacity: 0; }
  30%  { opacity: .38; }
  100% { transform: translateY(-15px) scale(1.3); opacity: 0; }
}
/* Opacity and flow are one animation apiece: the stream fades in, runs while it
   is there, and is gone by the time the bar sets. */
@keyframes smelt-tap {
  0%       { opacity: 0; }
  25%, 75% { opacity: .95; }
  100%     { opacity: 0; }
}
@keyframes smelt-flow { to { stroke-dashoffset: -18; } }
@keyframes smelt-cast {
  0%   { transform: translateY(-8px) scale(.85); opacity: 0; }
  60%  { opacity: 1; }
  100% { transform: translateY(0)    scale(1);   opacity: 1; }
}

.smelt-anim .feed { animation: smelt-feed .8s ease-in both; }
.smelt-anim .f2   { animation-delay: .22s; }
.smelt-anim .f3   { animation-delay: .44s; }
.smelt-anim .fire { animation: smelt-flare 1.3s .2s ease-in-out infinite both; }
.smelt-anim .glow { animation: smelt-glow 2.2s .2s ease-in-out infinite both; }
.smelt-anim .pour { animation: smelt-tap .9s 1.25s ease-in-out both,
                               smelt-flow .55s 1.25s linear infinite both; }
.smelt-anim .bar  { transform-box: fill-box; transform-origin: center;
                    animation: smelt-cast .7s 1.9s ease-out both; }
.smelt-anim .b2   { animation-delay: 2.35s; }
.smelt-anim .spark { animation: smelt-spark 1.6s 1.1s ease-out infinite both; }
.smelt-anim .s2    { animation-duration: 2.1s; animation-delay: 1.9s; }
.smelt-anim .puff  { animation: smelt-puff 2.6s .3s ease-out infinite both; }
.smelt-anim .u2    { animation-duration: 3.2s; animation-delay: 1.4s; }

.need.kind[data-use="Weapons-grade"] { border-color: #8fa9c4; }
.need.kind[data-use="Decorative"]    { border-color: var(--line-2); }

.brew-where { margin: .7rem 0 0; font-size: .82rem; color: var(--ink-2); }
.brew-where .col-head { display: inline; margin: 0 .35rem 0 0; }
.ore-list { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: 1.5rem; }
.brew-flow .plus { align-self: center; }
.brew-list .bi .ingot,
.brew-list .bo .ingot { width: 16px; margin-right: .4em; vertical-align: -.28em; }

/* ── Equipment sprites ────────────────────────────────────────── */
/* One 256×832 sheet of 32×32 cells; a row picks its cell with a background
   position rather than its own file. Drawn at 1:1 — pixel art at anything else
   drops rows out of the art — and reserved even when a piece has no cell, so
   the armour names stay in one column. */
.eq {
  display: block; flex: none; width: 32px; height: 32px;
  background-image: url(../img/item_equipment.png);
  background-size: 256px 832px; image-rendering: pixelated;
}
/* The sprite is a block in a row otherwise aligned on its text baseline, so
   the rows that carry one centre themselves instead. */
/* Without this the empty box would paint whatever sits at the sheet's origin,
   which is how a face veil ends up drawn as a shirt. */
.eq.blank { background-image: none; }
.brew-list button:has(.eq) { align-items: center; padding-block: .1rem; }
.brew-list .bi:has(.eq) { display: flex; align-items: center; gap: .5rem; }
/* Two cells for one entry sit tight against each other, so the pair reads as
   one item's row rather than as a sprite, a gap and another sprite. */
.eq + .eq { margin-left: -.35rem; }

@media (prefers-reduced-motion: reduce) {
  .smelt-anim * { animation: none !important; }
  /* What is left is the furnace and the bars it made — the things that were
     mid-flight when the motion stopped would otherwise hang in the air. */
  .smelt-anim .feed, .smelt-anim .spark,
  .smelt-anim .pour, .smelt-anim .puff { opacity: 0; }
}

/* ── The interactive marker ───────────────────────────────────── */
/* The workshop list is a wall of near-identical cards, so the buildings whose
   page is a picker rather than a list of jobs get a mark in the corner. It
   carries no words; the tooltip explains it. */
.ws-card { position: relative; }
.live-mark {
  position: absolute; top: .5rem; right: .55rem;
  line-height: 0; color: var(--accent); opacity: .8;
}
.live-mark .sym { width: 17px; height: 17px; }
.ws-card:hover .live-mark { opacity: 1; }


/* ── The armour page ──────────────────────────────────────────── */
/* The right-hand side of this picker is a dwarf rather than a result card, and
   the card sits under it — so the column is far too tall to pin. Sticky would
   park its top under the header and put the bottom of the card out of reach. */
#armor-pick .brew-panel { position: static; }

.body-wrap {
  border: 1px solid var(--line); background: var(--panel);
  padding: 1rem 1.2rem .9rem; margin-bottom: 1rem;
}

.body-fig { display: block; width: 100%; max-width: 196px; margin: 0 auto; }

/* The dwarf underneath: one flat silhouette, no strokes, and out of the way of
   the pointer so a click always lands on the region square over it. */
.body-shape > * { fill: var(--line); stroke: none; }
.body-fig .body-shape { pointer-events: none; }

/* One rule for every square in a region, and both halves of a pair light up
   together because they are one region. The fills are translucent so the dwarf
   still reads through whatever is lit; `pointer-events: all` is what makes an
   unfilled square catch the click across its whole area rather than on its
   outline alone. */
.body-fig .bp > * {
  fill: transparent; stroke: color-mix(in srgb, var(--ink-3) 55%, transparent);
  stroke-width: 1.2; stroke-dasharray: 3 3; pointer-events: all;
  transition: fill .14s, stroke .14s;
}
.body-fig .bp { cursor: pointer; }
.body-fig .bp:hover > * { fill: color-mix(in srgb, var(--accent) 25%, transparent);
                          stroke: var(--accent); stroke-dasharray: none; }
/* What the piece in the card below actually protects. */
.body-fig .bp.cov > * { fill: color-mix(in srgb, var(--accent) 40%, transparent);
                        stroke: var(--accent); stroke-dasharray: none; }
/* What the list is being filtered by. The strongest of the three, because it is
   a choice the reader made rather than something the page is telling them. */
.body-fig .bp.on > *  { fill: color-mix(in srgb, var(--accent) 68%, transparent);
                        stroke: var(--accent-2); stroke-dasharray: none; }
.body-fig .bp:focus { outline: none; }
.body-fig .bp:focus-visible > * { stroke: var(--accent-2); stroke-width: 3; stroke-dasharray: none; }

.body-cap {
  display: flex; align-items: center; gap: .6rem;
  margin-top: .8rem; padding-top: .7rem; border-top: 1px dashed var(--line);
}
.body-cap strong { font-size: .9rem; }
.body-all { margin-left: auto; }
.body-note { margin: .35rem 0 0; font-size: .78rem; color: var(--ink-3); min-height: 2.4em; }

.armor-covers {
  display: flex; flex-wrap: wrap; align-items: center; gap: .35rem;
  margin-top: .9rem; padding-top: .8rem; border-top: 1px dashed var(--line);
}
.armor-covers .flow-label { width: auto; margin-right: .2rem; }
.bp-chip { font: inherit; font-size: .8rem; }

/* The raw tokens. A definition list rather than prose because these are the
   numbers you compare between two pieces, and prose makes that a reading task. */
.armor-stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
  gap: .5rem .9rem; margin: .9rem 0 0; padding-top: .8rem;
  border-top: 1px dashed var(--line);
}
.armor-stats dt {
  font-size: .62rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3); font-family: var(--mono);
}
.armor-stats dd { margin: .1rem 0 0; font-size: .86rem; }
.armor-stats dd .muted { font-size: .76rem; }

.need.warnish { border-color: var(--gold); color: var(--ink); }
.need.kind[data-kind="Armor"]    { border-color: var(--accent); }
.need.kind[data-kind="Clothing"] { border-color: #8a9d5c; }
.need.kind[data-kind="Shield"]   { border-color: #5c7d9d; }
.need.kind[data-kind="Gear"]     { border-color: var(--line-2); }

@media (max-width: 780px) {
  /* On one column the figure would otherwise land under a 44-row list. */
  #armor-pick .brew { display: flex; flex-direction: column; }
  #armor-pick .brew-panel { order: -1; }
}

/* ── build routes ───────────────────────────────────────────────── */


/* The thread down the left margin. It runs from under one number to the next,
   so the last rung has none and the ladder ends rather than trailing off. */

/* A side job is off the count for a reason — it gets a hollow badge so the
   numbered spine still reads as 1, 2, 3 at a glance. */


@media (max-width: 560px) {
}

/* ── item sprites ───────────────────────────────────────────────── */
/* The game's own art, 15–31px wide and never the same shape twice, so every
   one sits in a fixed box and is scaled to fit rather than to fill — a tall
   thin amaranth and a squat sweet pod then read at the same visual weight. */
.sprite {
  flex: none; width: 22px; height: 22px;
  object-fit: contain; object-position: center bottom;
  image-rendering: pixelated;
}
/* Reserves the box in a list where only some rows have art, so the names keep
   one left edge — the same problem the equipment sheet's blank cell solves. */
.sprite.blank { display: inline-block; }
.sprite.big { width: 44px; height: 44px; margin-right: .55rem; }

.chip.has-sprite { padding-left: .4rem; gap: .2rem; }
.chip.has-sprite .sprite { width: 18px; height: 18px; }
.need .sprite { width: 17px; height: 17px; margin-right: .35em; vertical-align: -.35em; }
.brew-flow .chip.has-sprite .sprite { width: 24px; height: 24px; }

/* The picker lists put the sprite in its own column ahead of the name. */
.brew-list button:has(.sprite) { align-items: center; padding-block: .25rem; }
.brew-list .bi:has(.sprite) { display: flex; align-items: center; gap: .5rem; }

/* In a table the sprite is a marker beside the name, not a column of its own. */
td .sprite { vertical-align: -.45em; margin-right: .4rem; }

.item-head h1 .sprite { vertical-align: -.1em; }

/* ── The loom, animated ─────────────────────────────────────────── */
/* Same contract as the still and the quern: the base rules are the FINISHED
   state — the bolt woven, the shuttle parked — and the keyframes run up to it,
   so reduced motion gets the finished picture rather than a blank frame. The
   tone is the fibre's, since telling wool from silk is half of what the
   picture is for. */
.weave-anim { --brew: #c8a97e; max-width: 200px; }
.weave-anim[data-kind="Plant fibre"] { --brew: #c8a97e; }
.weave-anim[data-kind="Wool"]        { --brew: #ded3bd; }
.weave-anim[data-kind="Silk"]        { --brew: #cfd9de; }
.weave-anim[data-kind="Hair"]        { --brew: #8a6a45; }
.weave-anim[data-kind="Adamantine"]  { --brew: #79cfdd; }

.weave-anim .bolt     { fill: var(--brew); opacity: .85; }
.weave-anim .warp     { opacity: .34; }
.weave-anim .shuttle  { fill: var(--ink-2); }
.weave-anim .ink      { stroke: currentColor; }

@keyframes weave-grow  { from { transform: translateY(12px); } to { transform: none; } }
@keyframes weave-throw {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(17px); }
}
.weave-anim .bolt-wrap { animation: weave-grow 2.4s ease-out both; }
.weave-anim .shuttle   { animation: weave-throw 1.2s linear infinite; }

@media (prefers-reduced-motion: reduce) {
  .weave-anim * { animation: none !important; }
}

/* The fibre kinds, as chips. Same job as the drink and powder colours above:
   a row of grey chips makes the reader compare words instead of glancing. */
.need.kind[data-kind="Plant fibre"] { border-color: #8a9d5c; }
.need.kind[data-kind="Wool"]        { border-color: #b0a186; }
.need.kind[data-kind="Silk"]        { border-color: #7d95a8; }
.need.kind[data-kind="Hair"]        { border-color: #8a6a45; }
.need.kind[data-kind="Adamantine"]  { border-color: #57b6c6; }
.need.kind[data-kind="Goods"]       { border-color: var(--line-2); }

/* Four numbers about one fibre, read across rather than down — the armour
   page's stat block does the same thing and there is no reason for a second
   layout. */
.fibre-stats { grid-template-columns: repeat(4, 1fr); }

/* ── The clothier's value calculator ────────────────────────────── */
/* The controls sit inside the panel rather than beside it, because they are
   not filters — they do not change which garments are listed, only what the
   one on screen is worth. Keeping them next to the number they move is what
   makes that difference legible. */
/* The heading needs air above it: it sits under the garment's meta chips and
   without a gap the two read as one block. `.col-head` carries no top margin
   because everywhere else on the site it opens a section. */
.calc-head { margin-top: 1.4rem; }
.calc { margin: .5rem 0 1rem; display: grid; gap: .3rem; }
.calc-row { display: grid; grid-template-columns: 4.6rem minmax(0, 19rem); gap: .6rem; align-items: center; }
.calc-row .flabel { margin: 0; }

/* Styled rather than left native: a default select is the one control on the
   page that would arrive in the browser's colours instead of the theme's, and
   it sits directly above the number it sets. The chevron is an inline SVG
   background — a data URI cannot read a custom property, so it is painted in
   the one grey the two themes very nearly share (--ink-3 is #8b7f6c light and
   #8a7d68 dark), which is close enough that neither theme needs its own. */
.calc-sel, .fsel {
  width: 100%; padding: .3rem 1.6rem .3rem .5rem;
  font: inherit; font-size: .78rem; line-height: 1.4;
  color: var(--ink); background: var(--panel-2);
  border: 1px solid var(--line-2); border-radius: var(--radius);
  appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%238b7f6c' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right .45rem center; background-size: 11px;
}
.calc-sel:hover, .fsel:hover { border-color: var(--line-2); background-color: var(--panel); }
.calc-sel:focus-visible, .fsel:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The option list is drawn by the OS, so it needs telling twice. */
.calc-sel option, .fsel option { background: var(--panel); color: var(--ink); }
.calc-sel:disabled { color: var(--ink-3); background-color: var(--bg-2); cursor: not-allowed; }
.calc-row.off .flabel { color: var(--ink-3); opacity: .6; }

.calc-out { display: grid; gap: .1rem; margin: 0; }
.calc-line {
  display: grid; grid-template-columns: 7.5rem 1fr auto;
  gap: .5rem; align-items: baseline;
  padding: .22rem 0; border-bottom: 1px dashed var(--line);
}
.calc-line dt { font-size: .74rem; color: var(--ink-3); text-transform: uppercase; letter-spacing: .04em; }
.calc-line dd { margin: 0; }
.calc-sum { font-size: .78rem; color: var(--ink-2); font-family: var(--mono); }
.calc-val { font-family: var(--mono); font-size: .82rem; color: var(--ink); }

.calc-total {
  margin: .55rem 0 0; font-family: var(--mono);
  font-size: 1.5rem; font-weight: 700; color: var(--gold);
}
.calc-total .muted { font-size: .8rem; font-weight: 400; }

@media (max-width: 520px) {
  .fibre-stats { grid-template-columns: repeat(2, 1fr); }
  .calc-line { grid-template-columns: 1fr auto; }
  .calc-line dt { grid-column: 1 / -1; }
}

/* ── The forge's picker ─────────────────────────────────────────── */
/* Its list is the only one on the site drawing from two sets of art at once:
   armour comes out of the equipment sheet at 32px a cell, weapons and goods out
   of their own files at whatever size the wiki ships. One fixed box holds
   either, so ninety names keep a single left edge instead of stepping in and
   out — the same problem `.sprite.blank` solves for the plants, one size up. */
.fg-cell {
  display: flex; flex: none; width: 32px; height: 32px;
  align-items: center; justify-content: center;
}
.fg-cell .sprite { width: 32px; height: 32px; }
.brew-list button:has(.fg-cell) { align-items: center; padding-block: .1rem; }
.brew-list .bi:has(.fg-cell) { display: flex; align-items: center; gap: .5rem; }

/* Fourteen categories is too many to give each its own colour, so only the two
   that answer a different question get one: what goes on a soldier, and what
   goes on a caravan. Everything else keeps the default border. */
.need.kind[data-kind="Weapons"]      { border-color: #a8555a; }
.need.kind[data-kind="Armor"]        { border-color: #6f8ba8; }
.need.kind[data-kind="Furniture"]    { border-color: var(--line-2); }

/* Five numbers per attack, read across. Same reasoning as the fibre stats:
   comparing a war hammer's contact area with a battle axe's is the point, and
   a column of numbers is the only layout that lets you. */
.forge-stats { grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); }

table.atk { font-size: .78rem; }
table.atk .num { font-family: var(--mono); text-align: right; white-space: nowrap; }
table.atk .num.muted { color: var(--ink-3); }
table.atk .need { padding: .05rem .4rem; font-size: .68rem; }
/* Edge cuts, blunt bruises, and which one a weapon does is the first thing you
   need off the row — so the two are told apart by colour, not by reading. */
.atk-edge  { border-color: #a8555a; color: var(--ink-2); }
.atk-blunt { border-color: #7d7f88; color: var(--ink-2); }

@media (max-width: 520px) {
  .forge-stats { grid-template-columns: repeat(2, 1fr); }
}
