/* PandaQuest Hub -- one stylesheet, no build step, no CDN.
 *
 * Dark by default because the addon is dark and players read this next to the game.  Everything
 * is sized in rem so a phone browser's text zoom works, and every interactive element has a
 * visible :focus-visible ring: the guide pages are long and keyboard users need to see where
 * they are.  Contrast of the body text on the page background is ~13:1 (WCAG AAA).
 */

:root {
  color-scheme: dark;

  --bg: #12131a;
  --bg-raised: #1a1c26;
  --bg-inset: #0e0f15;
  --border: #2b2e3c;
  --border-strong: #3c4152;

  --text: #e7e9f2;
  --text-dim: #a7adc2;
  --text-faint: #7d8399;

  --accent: #6fd18c;         /* jade -- the addon's arrow colour */
  --accent-dim: #3f7f56;
  --accent-text: #0d1712;
  --link: #8fc7ff;
  --link-visited: #b9a7f0;

  --warn: #ffcc66;
  --danger: #ff8a8a;
  --info: #7fd3e8;

  --radius: 10px;
  --radius-sm: 6px;
  --gap: 1rem;
  --maxw: 78rem;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}

/* --maxw used to be frozen at 78rem, which meant the shell stopped growing at 1248px: measured
 * gutters were 96px a side at 1440 and 656px a side at 2560, so the page used 49% of a 2560 screen
 * while the leaderboard tables inside it were scrolling.  Two steps rather than a full-bleed
 * layout, because row scanning and line length both degrade past ~1700px -- 104rem is the honest
 * cap (.plans/responsive.md, rejected alternatives).  The 90rem step is also what makes the header
 * fit on one line: see the (min-width: 90rem) block under .site-nav.
 * Gutters after this: 32px a side at 1440, 464px a side at 2560. */
@media (min-width: 90rem) { :root { --maxw: 88rem; } }
@media (min-width: 120rem) { :root { --maxw: 104rem; } }

* { box-sizing: border-box; }

/* The `hidden` attribute has to keep working on elements this stylesheet gives a `display` to.
   An author rule beats the browser's own `[hidden] { display: none }` for the same property
   whatever its specificity, so `.btn { display: inline-block }` two hundred lines down was
   quietly showing every element that ships hidden. That is not cosmetic here: the passkey
   buttons are rendered `hidden` on purpose and un-hidden by passkey.js only once it has
   established that this browser and this deployment can actually perform a ceremony -- so on
   the hub as it is reached today (plain http on an IP address, where WebAuthn cannot work at
   all) the page was offering a "Sign in with a passkey" button that could only throw.
   `!important` is deliberate and is the one in this file: `hidden` means hidden. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  /* 1rem, not 16px: an absolute px here overrode the reader's own browser font-size preference
     while every padding around it stayed rem, so somebody who had set 20px got 16px text inside
     20px-scaled boxes.  It is also half of the iOS zoom fix -- see the font-size floor on the
     form controls below, which is `max(1rem, 16px)` and therefore follows this value up. */
  font-size: 1rem;
  line-height: 1.55;
  /* Realm and character names arrive from the game and are never hyphenated; one long one used to
     be able to push a table cell wider than the screen. */
  overflow-wrap: break-word;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

a { color: var(--link); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:visited { color: var(--link-visited); }
a:hover { color: #fff; }

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

h1, h2, h3 { line-height: 1.2; margin: 0 0 0.5rem; font-weight: 650; }
/* Fluid, because the shell now spans 78rem to 104rem and a flat 1.7rem h1 both dominated a 360px
 * phone and looked lost at 2560.  The clamp floor and ceiling are rem and only the middle term
 * carries vw, which is what keeps text-only zoom (WCAG 1.4.4) working: a pure-vw size ignores it.
 * Computed: h1 is 23.2px at 360 (was 27.2) and 33.6px from 1440 up. */
h1 { font-size: clamp(1.45rem, 1.15rem + 1.2vw, 2.1rem); }
h2 { font-size: clamp(1.15rem, 1.05rem + 0.45vw, 1.4rem); }
h3 { font-size: 1.05rem; }
p { margin: 0 0 0.75rem; }
/* The measure, and the thing that makes widening --maxw safe: without it the privacy and terms
 * cards render ~170-character lines at 1440.  Only prose is capped -- the three classes below are
 * <p> elements that are boxes or rows rather than sentences, and capping them would leave a
 * bordered callout or a button row stopping short in the middle of its container. */
p { max-width: 76ch; }
.empty-note, .btn-row, .warning-note { max-width: none; }

code, kbd, pre { font-family: var(--mono); font-size: 0.9em; }
pre {
  background: var(--bg-inset);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.75rem 0.9rem;
  overflow-x: auto;
  margin: 0 0 0.75rem;
}
code { background: var(--bg-inset); border-radius: 4px; padding: 0.1em 0.35em; }
pre code { background: none; padding: 0; }

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--accent);
  color: var(--accent-text);
  padding: 0.5rem 0.9rem;
  z-index: 20;
}
.skip-link:focus { left: 0; }

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

.shell { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 1rem; }

.site-header {
  border-bottom: 1px solid var(--border);
  background: var(--bg-raised);
  position: sticky;
  top: 0;
  z-index: 10;
}
.site-header .shell {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.25rem;
  padding-block: 0.6rem;
}
.brand {
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--text);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.brand:visited { color: var(--text); }
/* The mark is the addon's own icon (static/logo.svg, generated by tools/make_textures.py from
 * the same shapes as PandaQuest/Textures/icon.tga -- docs/09 B2).  It is a rounded badge with its
 * own dark edge, so it needs no background, no ring and no clipping from the stylesheet: sizing
 * it is the whole job.  `width`/`height` are also on the <img> element so the header does not
 * reflow if the stylesheet arrives second. */
.brand .mark {
  width: 1.75rem; height: 1.75rem;
  display: block;
  flex: 0 0 auto;
}
/* Two deliberate rows below 90rem, one row above it -- rather than whatever flex-wrap happened to
 * do.  Measured max-content widths of the header's three groups (DejaVu Sans standing in for
 * system-ui, so ~10% wide): brand 196px, realm form 257px, two 20px gaps, and a signed-in
 * navigation of 1001px at the padding and gaps set just below -- 1494px on one line against the
 * 1376px the shell offers from 90rem up, where --maxw steps to 88rem.  It does not fit at these
 * numbers; the one-row exception at :849 is where it is made to, by trimming the padding and the
 * gaps there and only there.  Below the breakpoint nothing would fit it at any padding.
 * Left to wrap on its own the realm form landed on a second flex line where `margin-left: auto`
 * no longer applies, i.e. orphaned at the far left under the brand, and at 1024 the nav did not
 * fit beside the brand either and the header silently became three rows.  So: the nav takes a
 * full-width row of its own and the realm form stays on the brand's row, pushed right.
 * The breakpoint is expressed as the min-width exception so it cannot disagree with the --maxw
 * ladder it is derived from, and so 1440 itself lands on exactly one side of it. */
.site-nav { order: 3; width: 100%; }
.site-nav ul { list-style: none; display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem; margin: 0; padding: 0; }
.site-nav a {
  display: block;
  padding: 0.35rem 0.6rem;
  border-radius: var(--radius-sm);
  color: var(--text-dim);
  text-decoration: none;
}
.site-nav a:hover { background: var(--bg-inset); color: var(--text); }
.site-nav a[aria-current="page"] { color: var(--text); background: var(--bg-inset); box-shadow: inset 0 -2px 0 var(--accent); }

main { flex: 1 0 auto; padding: 1.5rem 0 3rem; }

.site-footer {
  border-top: 1px solid var(--border);
  background: var(--bg-raised);
  color: var(--text-faint);
  font-size: 0.85rem;
  padding: 1rem 0 1.5rem;
}
.site-footer .shell { display: flex; flex-wrap: wrap; gap: 0.35rem 1.25rem; }
.site-footer a { color: var(--text-dim); }

.page-head { margin-bottom: 1.25rem; }
.page-head p.lede { color: var(--text-dim); margin: 0; max-width: 60ch; }

/* The class names are a lie the file has always told -- they name a track floor, not a column
 * count, and auto-fit collapses the empty tracks, so `.cols-2` renders three columns when three
 * cards are handed to it.  Left alone deliberately; renaming them costs ~15 template lines and
 * buys only the ability to read them (.plans/responsive.md, open questions).
 *
 * `min(100%, ...)` is the guard that stops an auto-fit grid overflowing a container narrower than
 * its own floor.  That is not theoretical here: quest_guide.html:139 puts `.grid.cols-2` inside a
 * `.card`, whose inner width at a 360px viewport was 328 - 35.2 padding - 2 border = 290.8px
 * against a 320px floor, so /quests/29410 pushed the whole document 29.2px wider than the screen
 * and scrolled sideways on a phone.
 * cols-4 drops from 11rem to 9.5rem so the four front-page totals sit 2-up at 360 (2 x 156px in
 * 328px) instead of stacking into four full-width rows; at every width where 11rem already fitted
 * the tracks are unchanged, because auto-fit stretches four cards across four tracks either way. */
.grid { display: grid; gap: var(--gap); }
.grid.cols-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); }
.grid.cols-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); }
.grid.cols-4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 9.5rem), 1fr)); }

/* A leaderboard card is sized by the table inside it, not by a generic number.  The board table
 * (partials/leaderboard.html:87-112) is five columns whose min-content width measures 416px with
 * DejaVu and ~375px in a real UI font once `th` is allowed to wrap -- so a 15rem floor, which gave
 * the card 284.8px of inner width at 1024, clipped the fifth header mid-word with no scrollbar to
 * say so.  A 28rem floor gives 2 tracks x 488px at 1024 (card inner 450.8px), one full-width track
 * at 768, and 3 x 448px at 1440 -- 410.8px of card inner, which clears the 375px a real UI font
 * needs and is 5px short of the pessimistic DejaVu figure, so 1440 is the one width where a board
 * may still scroll a little.  At 360 it is one track and the table certainly still scrolls, which
 * is correct on a phone; both cases are now announced rather than silently clipped, see
 * .table-wrap.
 *
 * The class is on the three board grids that need it (index.html, leaderboards.html twice), so the
 * `:has(> .card.board)` rule that stood in for them until the templates carried it is gone: a
 * browser that does not know `:has()` discards the whole rule an unknown selector appears in, and
 * there is no reason to keep a second spelling that some browsers silently drop. */
.grid.boards { grid-template-columns: repeat(auto-fit, minmax(min(100%, 28rem), 1fr)); }

.card {
  background: var(--bg-raised);
  /* Named so a `.table-wrap` at any depth inside the card knows what colour is behind it; see the
     scroll-shadow covers under .table-wrap, which have to match the surface or they show. */
  --surface: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
}
.card > :last-child { margin-bottom: 0; }
.card h2, .card h3 { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }

/* ------------------------------------------------------------- numbers */

.stat { display: flex; flex-direction: column; gap: 0.15rem; }
/* Fluid for the same reason as h1: 21.6px at 360, 30.4px from 1440 up. */
.stat .value { font-size: clamp(1.35rem, 1.1rem + 0.9vw, 1.9rem); font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat .label { color: var(--text-dim); font-size: 0.85rem; }
.stat .basis { color: var(--text-faint); font-size: 0.78rem; }

.basis, .muted { color: var(--text-faint); }
.dim { color: var(--text-dim); }
.nowrap { white-space: nowrap; }
.tabular { font-variant-numeric: tabular-nums; }

.badge {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--bg-inset);
  color: var(--text-dim);
  font-size: 0.75rem;
  line-height: 1.5;
  white-space: nowrap;
}
.badge.thin { border-color: var(--warn); color: var(--warn); }
.badge.good { border-color: var(--accent-dim); color: var(--accent); }
.badge.faction-Alliance { border-color: #4a7ec0; color: #9dc4f2; }
.badge.faction-Horde { border-color: #b45a4a; color: #f0a396; }

.warning-note {
  border-left: 3px solid var(--warn);
  background: rgba(255, 204, 102, 0.08);
  padding: 0.6rem 0.85rem;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--text);
}
.empty-note {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  padding: 1.25rem;
  color: var(--text-dim);
  text-align: center;
}

/* -------------------------------------------------------------- tables */

/* A table that has to scroll sideways must look as though it does.  Without an affordance the
 * clipped last column read as a broken page rather than a scrollable one -- which is exactly what
 * the leaderboard screenshot was.  This is Lea Verou's scrolling-shadow idiom: the two opaque
 * covers are `local`, so they travel with the table and sit precisely over a shadow when that end
 * is reached, and the two shadows are `scroll`, so they stay pinned to the wrapper's own edges.
 * The covers are the only thing painted -- there is no background-color -- so a wrapper with
 * nothing to scroll shows nothing at all and stays transparent over whatever surface it sits on.
 * That is what --surface is for: most wrappers in the template tree are inside a `.card`, which
 * sets --surface to its own background, and the few that sit straight on the page background
 * inherit the fallback `var(--bg)` written into each gradient below.  A cover has to be the colour
 * of the thing behind it or it would paint a bar across the table, and the rule is stated rather
 * than counted because every new table would otherwise make the count wrong.
 * Keyboard reach is added by pq.js rather than here, because it depends on whether the table is
 * actually overflowing; the focus ring is the file's own `:focus-visible` rule (:89). */
.table-wrap {
  overflow-x: auto;
  /* A sideways flick inside a table should not also drag the page or trigger the back gesture. */
  overscroll-behavior-x: contain;
  background:
    linear-gradient(to right, var(--surface, var(--bg)) 40%, rgba(0, 0, 0, 0)) left center / 2rem 100% no-repeat local,
    linear-gradient(to left, var(--surface, var(--bg)) 40%, rgba(0, 0, 0, 0)) right center / 2rem 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0)) left center / 0.7rem 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0)) right center / 0.7rem 100% no-repeat scroll;
}
table { border-collapse: collapse; width: 100%; font-size: 0.94rem; }
caption { text-align: left; color: var(--text-faint); font-size: 0.85rem; padding-bottom: 0.4rem; }
th, td { text-align: left; padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); vertical-align: top; }
/* No `white-space: nowrap`.  It was the single widest thing on a leaderboard card: the header
 * "DIFFERENT QUESTS" measures 169px and could not break, and letting it and "HOURS RECORDED" wrap
 * to two lines drops the board table's min-content width from 503px to 416px (453 -> 375 in a real
 * UI font).  Short headers (#, Realm, Runs) never need to wrap and so do not.  This touches all 23
 * tables in the tree, not only the boards, and that is the intended trade. */
th { color: var(--text-dim); font-weight: 600; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.03em; }
tbody tr:hover { background: var(--bg-inset); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
th a { color: inherit; text-decoration: none; display: inline-flex; gap: 0.25rem; }
th a:hover { color: var(--text); }
th[aria-sort] a { color: var(--accent); }

/* --------------------------------------------------------------- forms */

form.filters { display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); align-items: end; }
.field { display: flex; flex-direction: column; gap: 0.25rem; }
.field label { font-size: 0.82rem; color: var(--text-dim); }
input[type="text"], input[type="search"], input[type="number"], input[type="password"], select, textarea {
  background: var(--bg-inset);
  border: 1px solid var(--border-strong);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.6rem;
  font: inherit;
  /* After `font: inherit`, and the reason the site can be read on an iPhone at all.  iOS Safari
     zooms the viewport whenever a control it is focusing computes below 16px, and it does not zoom
     back out afterwards -- so tapping the /quests search box used to strand the page at a zoom the
     reader had not asked for and could not easily undo.  `max()` rather than a flat 16px so a
     reader who has raised their browser font-size still gets the larger of the two. */
  font-size: max(1rem, 16px);
  width: 100%;
  min-width: 0;
}
input::placeholder { color: var(--text-faint); }

.btn {
  display: inline-block;
  background: var(--accent);
  color: var(--accent-text);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.9rem;
  font: inherit;
  font-weight: 650;
  cursor: pointer;
  text-decoration: none;
}
.btn:hover { filter: brightness(1.08); color: var(--accent-text); }
.btn.secondary { background: transparent; border-color: var(--border-strong); color: var(--text); }
.btn.secondary:hover { background: var(--bg-inset); color: var(--text); }
/* A button that cannot be used yet has to look like one, hover included.  The sales consent on
   /settings/privacy is the case: its grant waits for auction data consent and says so beside it. */
.btn:disabled { opacity: 0.55; cursor: not-allowed; filter: none; }
.btn-row { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }

.keybox {
  display: flex;
  gap: 0.5rem;
  align-items: stretch;
  flex-wrap: wrap;
}
.keybox code {
  flex: 1 1 20rem;
  padding: 0.6rem 0.7rem;
  background: var(--bg-inset);
  border: 1px solid var(--accent-dim);
  border-radius: var(--radius-sm);
  word-break: break-all;
}
.copy-ok { color: var(--accent); font-size: 0.85rem; }

/* ---------------------------------------------------------------- feed */

.feed { list-style: none; margin: 0; padding: 0; max-height: 26rem; overflow-y: auto; }
.feed li {
  display: flex;
  gap: 0.6rem;
  align-items: baseline;
  padding: 0.4rem 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.92rem;
}
.feed li:last-child { border-bottom: 0; }
.feed .when { color: var(--text-faint); font-size: 0.78rem; white-space: nowrap; min-width: 5.5rem; }
.feed li.is-new { animation: flash 1.4s ease-out; }
@keyframes flash { from { background: rgba(111, 209, 140, 0.18); } to { background: transparent; } }

.live-dot {
  width: 0.55rem; height: 0.55rem; border-radius: 50%;
  background: var(--text-faint);
  display: inline-block;
}
.live-dot[data-state="live"] { background: var(--accent); }
.live-dot[data-state="polling"] { background: var(--warn); }
.live-dot[data-state="off"] { background: var(--text-faint); }

/* ------------------------------------------------------------- guide */

.guide-head { display: flex; flex-wrap: wrap; gap: 0.75rem 1.5rem; align-items: flex-start; justify-content: space-between; }
.objective {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-raised);
  padding: 1rem 1.1rem;
  display: grid;
  gap: 1rem;
  grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
}
.objective > .obj-body > :last-child { margin-bottom: 0; }
.obj-rank {
  display: inline-grid; place-items: center;
  width: 1.7rem; height: 1.7rem;
  border-radius: 50%;
  background: var(--accent-dim);
  color: #fff;
  font-weight: 700;
  font-size: 0.9rem;
}
.map-figure { margin: 0; }
.map-figure canvas {
  width: 100%;
  height: auto;
  display: block;
  background: var(--bg-inset);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  aspect-ratio: 3 / 2;
}
.map-figure figcaption { color: var(--text-faint); font-size: 0.8rem; padding-top: 0.35rem; }
.legend { list-style: none; display: flex; flex-wrap: wrap; gap: 0.25rem 0.9rem; margin: 0.4rem 0 0; padding: 0; font-size: 0.8rem; color: var(--text-dim); }
.legend li { display: flex; align-items: center; gap: 0.35rem; }
.legend .swatch { width: 0.7rem; height: 0.7rem; border-radius: 50%; display: inline-block; border: 1px solid rgba(0,0,0,0.4); }
.legend .swatch.square { border-radius: 2px; }

.tips { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.tips li {
  border-left: 3px solid var(--accent-dim);
  background: var(--bg-inset);
  padding: 0.55rem 0.8rem;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.tips .evidence { display: block; color: var(--text-faint); font-size: 0.8rem; margin-top: 0.15rem; }

/* The quest page's "how precise is this guide" checklist: one rung per analyser gate. */
.ladder { list-style: none; margin: 0.6rem 0 0; padding: 0; display: grid; gap: 0.35rem; }
.ladder li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.5rem; }
.ladder li > span[aria-hidden] { width: 1.1rem; text-align: center; font-weight: 700; }
.ladder li.done > span[aria-hidden] { color: var(--accent); }
.ladder li.todo { color: var(--text-dim); }
.ladder li.todo > span[aria-hidden] { color: var(--text-faint); }
.ladder .basis { flex-basis: 100%; margin: 0 0 0 1.6rem; }

.steps { counter-reset: step; list-style: none; padding: 0; margin: 0; display: grid; gap: 1rem; }
.steps > li { counter-increment: step; position: relative; padding-left: 2.4rem; }
.steps > li::before {
  content: counter(step);
  position: absolute; left: 0; top: 0;
  width: 1.7rem; height: 1.7rem;
  border-radius: 50%;
  background: var(--accent); color: var(--accent-text);
  display: grid; place-items: center; font-weight: 700; font-size: 0.9rem;
}

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

.htmx-indicator { opacity: 0; transition: opacity 120ms ease-in; }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }

/* --- the player's own character pages (/me) -------------------------------------------
 * docs/07 section C.  Three things here are load-bearing rather than decorative:
 *
 *  - The paper doll reads like the game's character panel, and on a phone it stacks into one
 *    list: the slots are a *list* in the markup, so losing the columns loses nothing but the
 *    resemblance.  Its layout is the character sheet block further down (.look-doll).
 *  - Item quality is a colour and a word.  The colours below are the game's own; the word is
 *    in the tooltip, because a fact carried only by hue is not carried to everybody.
 *  - The tooltip opens on :hover and on :focus-within, so a keyboard reaches it, and its text
 *    is in the document either way -- nothing here is the only copy of anything.
 */

.realm-picker { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.5rem; }
.realm-picker form { display: contents; }
.realm-chip {
  background: var(--bg-inset);
  border: 1px solid var(--border-strong);
  color: var(--text);
  border-radius: 999px;
  padding: 0.35rem 0.85rem;
  font: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  display: inline-flex;
  gap: 0.4rem;
  align-items: baseline;
}
.realm-chip:hover { background: var(--bg-raised); }
.realm-chip.is-current { border-color: var(--accent-dim); color: var(--accent); box-shadow: inset 0 -2px 0 var(--accent-dim); }
.btn.small { padding: 0.25rem 0.6rem; font-size: 0.85rem; font-weight: 600; }
.visibility-form { margin: 0; }

.doll-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.35rem; }
.doll-item {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  padding: 0.3rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-inset);
  position: relative;
  min-width: 0;
}
.doll-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.doll-item.is-empty { opacity: 0.55; }
.doll-icon {
  width: 40px; height: 40px; flex: 0 0 40px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--bg);
  display: grid; place-items: center;
  font-size: 0.72rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.04em;
}
.doll-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.doll-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Item quality, the game's own colours.  Each one is at least 4.5:1 on --bg-inset.  The name in
   the details card takes the same colour, as the game's tooltip does. */
.q0 .doll-item-name, .q0 .tip-name { color: #b0b0b0; }
.q1 .doll-item-name, .q1 .tip-name { color: #f2f3f8; }
.q2 .doll-item-name, .q2 .tip-name { color: #4ce04c; }
.q3 .doll-item-name, .q3 .tip-name { color: #4ea6ff; }
.q4 .doll-item-name, .q4 .tip-name { color: #c88bf5; }
.q5 .doll-item-name, .q5 .tip-name { color: #ff9d33; }
.q6 .doll-item-name, .q6 .tip-name { color: #e6cc80; }
.q7 .doll-item-name, .q7 .tip-name { color: #4fd6ff; }

.doll-tip {
  position: absolute;
  left: 0; top: calc(100% + 0.3rem);
  z-index: 5;
  display: none;
  min-width: 14rem; max-width: 22rem;
  padding: 0.6rem 0.7rem;
  background: var(--bg-raised);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
  flex-direction: column; gap: 0.2rem;
}
.doll-item:hover .doll-tip, .doll-item:focus-within .doll-tip, .doll-item:focus .doll-tip { display: flex; }
.doll-tip dl { margin: 0.35rem 0 0; display: grid; gap: 0.1rem; }
.doll-tip dl div { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.85rem; }
.doll-tip dt { color: var(--text-dim); }
.doll-tip dd { margin: 0; font-variant-numeric: tabular-nums; }

.stat-list { margin: 0; display: grid; gap: 0.2rem; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }
.stat-list div { display: flex; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--border); padding: 0.25rem 0; }
.stat-list dt { color: var(--text-dim); }
.stat-list dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: 600; }

/* --- the character sheet: what a character looks like and wears (partials/character_look.html)
 *
 * The in-game arrangement -- eight slots down the left, eight down the right, the weapons along the
 * bottom -- around Blizzard's full-body render.  The render is a 1600x1200 PNG with a transparent
 * background and the character standing in its middle third (on the live copy of Bananaroot the
 * opaque pixels are x 594-964, y 517-995), so it is shown in a 3:4 portrait.  The hub measures where
 * the figure stands when it copies a render (blizzard/figure.py), and a measured render is framed on
 * it: .look-crop is the frame and the picture is placed inside at the offsets the page computed
 * (character_look.render_crop), because `object-fit: cover` keeps the picture's whole height and drew
 * that goblin at 40% of the frame under an empty sky.  A render with no measurement -- a copy taken
 * before measuring, or one that could not be read -- is the frame itself, cropped with `object-fit:
 * cover`: the sides that go are empty on every render, and nothing is cut from the top or the
 * bottom, where a taller race than a goblin would reach.  The dark ground behind it is ours; the
 * picture brings none.
 *
 * Three widths.  From 75rem the stats stand beside the doll.  Down to 30rem (480px) the three
 * columns stay and narrow around a smaller render, slot names shortening with an ellipsis -- the
 * full name is in the details card.  Below that the render comes first and the slots stack into one
 * list in the same order, because two columns of 140px cannot hold a name and an icon at 360px.
 */
.look-switch { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0.25rem 0 0.6rem; }
.look-switch a.realm-chip { text-decoration: none; }
.look-source { display: flex; flex-wrap: wrap; gap: 0.25rem 0.5rem; align-items: baseline; }
.look-source p { margin: 0; }
.look-notice { margin: 0.5rem 0; }

.look-head {
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-end;
  gap: 0.5rem 1.5rem; margin: 0.9rem 0 1rem;
}
.look-who p { margin: 0; }
.look-name { font-size: 1.5rem; font-weight: 700; line-height: 1.15; }
.look-line { color: var(--text-dim); }
.look-who .btn-row { margin-top: 0.35rem; }
.look-guild { white-space: normal; }
.look-ilvl { display: flex; gap: 1.25rem; margin: 0; }
.look-ilvl div { display: flex; flex-direction: column-reverse; align-items: flex-end; }
.look-ilvl dt { color: var(--text-faint); font-size: 0.78rem; }
.look-ilvl dd { margin: 0; font-size: 1.6rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }

.look-doll {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(9rem, 1.1fr) minmax(0, 1fr);
  grid-template-areas: "left figure right" "bottom bottom bottom";
  gap: 0.75rem 1rem;
  align-items: center;
}
.look-left { grid-area: left; }
.look-right { grid-area: right; }
/* One column per square, however many the record has: the hands always, a ranged slot only when the
   record puts an item in it (character_look.DRAWN_WHEN_WORN).  A fixed three would leave the two
   hands off centre beside an empty track. */
.look-bottom { grid-area: bottom; grid-auto-flow: column; grid-auto-columns: minmax(0, 15rem); justify-content: center; }
.look-figure { grid-area: figure; margin: 0; min-width: 0; }
.look-render,
.look-crop {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background:
    radial-gradient(ellipse 60% 40% at 50% 74%, rgba(111, 209, 140, 0.12), rgba(0, 0, 0, 0) 70%),
    var(--bg-inset);
}
.look-crop { position: relative; overflow: hidden; }
/* Percentages of the frame, set on .look-crop by the page: `left` and `width` resolve against its
   width and `top` against its height, and the picture keeps its own proportions -- a render's 4:3
   until it has loaded, its natural ones after. */
.look-crop > .look-render {
  position: absolute;
  left: var(--crop-left, 0%);
  top: var(--crop-top, 0%);
  width: var(--crop-width, 100%);
  aspect-ratio: auto 4 / 3;
  border: 0;
  border-radius: 0;
  background: none;
}
.look-render.is-absent {
  display: grid; place-items: center;
  padding: 1rem;
  border-style: dashed;
  color: var(--text-dim); font-size: 0.88rem; text-align: center;
}
.look-render.is-absent p { margin: 0; }

/* The right column mirrors the left, icon on the outside edge, as the game draws it. */
.look-right .doll-item { flex-direction: row-reverse; text-align: right; }
.look-right .doll-text { align-items: flex-end; }
.look-right .doll-tip { left: auto; right: 0; text-align: left; }
.look-doll .doll-tip { min-width: min(14rem, calc(100vw - 3rem)); max-width: min(22rem, calc(100vw - 3rem)); }

/* An empty square is an outline and a word, not a missing picture. */
.doll-slot.is-empty .doll-icon { border-style: dashed; background: transparent; }
.doll-empty-word { color: var(--text-faint); font-size: 0.75rem; }

.tip-lines { display: grid; gap: 0.05rem; margin-top: 0.3rem; font-size: 0.85rem; }
.tip-line { display: flex; justify-content: space-between; gap: 1rem; }
.tip-line.is-bonus { color: #4ce04c; }
.tip-right { color: var(--text-dim); }

.look-stats {
  display: grid; gap: 0.75rem 1rem; align-content: start;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  margin-top: 1rem;
}
.card .look-group h3 { font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); margin-bottom: 0.25rem; }
.look-stats .stat-list { grid-template-columns: minmax(0, 1fr); }
.look-stats .stat-list .basis { font-size: 0.75rem; font-weight: 400; }

.look-blizzard-note { margin: 0.75rem 0 0; }
.look-refresh { display: flex; flex-wrap: wrap; gap: 0.4rem 0.75rem; align-items: center; margin-top: 0.75rem; }
.look-refresh .basis { flex-basis: 100%; margin: 0; font-size: 0.82rem; }
/* Hidden from sight and from a screen reader until htmx marks the request in flight; opacity alone
   (the site's `.htmx-indicator`) would leave "Fetching" readable to a screen reader all along. */
.look-fetching { visibility: hidden; color: var(--text-dim); }
.look-fetching.htmx-request { visibility: visible; }

@media (min-width: 75rem) {
  .look-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(15rem, 19rem); gap: 1.5rem; align-items: start; }
  .look-stats { grid-template-columns: minmax(0, 1fr); margin-top: 0; }
}
@media (max-width: 46rem) {
  .look-doll { grid-template-columns: minmax(0, 1fr) minmax(6.5rem, 0.8fr) minmax(0, 1fr); gap: 0.5rem; }
  .look-doll .doll-icon { width: 32px; height: 32px; flex-basis: 32px; }
  .look-doll .doll-item { gap: 0.35rem; padding: 0.2rem; }
  .look-doll .doll-text { font-size: 0.85rem; }
  .look-bottom { grid-auto-columns: minmax(0, 1fr); }
  .look-ilvl dd { font-size: 1.3rem; }
}
@media (max-width: 30rem) {
  .look-doll { grid-template-columns: minmax(0, 1fr); grid-template-areas: "figure" "left" "right" "bottom"; }
  .look-figure { justify-self: center; width: min(100%, 15rem); }
  .look-bottom { grid-auto-flow: row; grid-template-columns: minmax(0, 1fr); }
  .look-right .doll-item { flex-direction: row; text-align: left; }
  .look-right .doll-text { align-items: flex-start; }
  .look-right .doll-tip { left: 0; right: auto; }
}

.plain-list { list-style: none; margin: 0; padding: 0; }
.plain-list li { display: flex; justify-content: space-between; gap: 1rem; padding: 0.3rem 0; border-bottom: 1px solid var(--border); }

.bar-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; }
.bar-list li { display: grid; grid-template-columns: minmax(7rem, 14rem) 1fr auto; gap: 0.75rem; align-items: center; }
.bar-track { background: var(--bg-inset); border-radius: 999px; height: 0.7rem; overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--accent-dim); border-radius: 999px; }
.bar-value { font-size: 0.9rem; color: var(--text-dim); }

/* The activity heatmap.  Five steps rather than a continuous ramp, because a reader compares
   cells against a legend and not against a gradient; every step keeps the row header readable. */
table.heatmap { font-size: 0.75rem; }
table.heatmap th { padding: 0.2rem 0.35rem; }
table.heatmap td.heat { padding: 0; width: 1.1rem; height: 1.1rem; border-bottom: 1px solid var(--bg); }
.heat-0 { background: var(--bg-inset); }
.heat-1 { background: #1f3b2a; }
.heat-2 { background: #2f6b45; }
.heat-3 { background: #479a5f; }
.heat-4 { background: #5fbd76; }
.heat-5 { background: var(--accent); }
/* The legend reuses the same five swatches, so the scale on the picture and the scale in
   the key cannot drift apart. */
.heat-legend .swatch { border-color: var(--border); }

@media (max-width: 46rem) {
  /* `body { font-size: 15px }` used to be here.  It was the other half of the iOS zoom: every
     control inherited it through `font: inherit`, 15px is under Safari's 16px threshold, and so
     every field on the site zoomed the page on focus.  Removing it makes all phone text ~7%
     larger, which is the most visible single change in this set and is deliberate. */
  .objective { grid-template-columns: minmax(0, 1fr); }
  .bar-list li { grid-template-columns: minmax(0, 1fr) auto; }
  .bar-list .bar-track { grid-column: 1 / -1; }
  /* Measured at 360px: seven nav links wrap to three rows, and with the brand row and the
     full-width realm form the sticky header stood 184px tall -- 28% of a 640px phone viewport,
     permanently, on every page.  A header that big is worth more as scrollable content than as a
     fixture.  (The alternative that keeps it sticky is a <details> disclosure nav in base.html;
     it needs template lines this change does not own.) */
  .site-header { position: static; }
  .site-header .shell { padding-block: 0.5rem; }
  .site-nav ul { gap: 0.15rem 0.35rem; }
  /* Nearly 13px of table width back per card, which is 13px the board tables do not have to
     scroll for. */
  .card { padding: 0.85rem 0.9rem; }
  main { padding-top: 1rem; }
}

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

/* There used to be an advertising section here: the slot frame and its reserved height, the
 * two-column right rail that /quests and the quest guide put a slot in, and the house banner.
 * It is gone with the feature (Blizzard's Developer API Terms 2(h)/(i), docs/07 B4).  Those two
 * pages are single-column again, which is what they had been rendering as all along -- the rail
 * was a flex row, the slot element was never emitted on a live page, and a flex row with one
 * child is a full-width block.  So no measured layout changes with this deletion.
 */


/* ------------------------------------------------- the navigation's realm selector (docs/09 B3)
 *
 * Always in the header, on every page.  On a page the choice cannot affect it is disabled rather
 * than removed, and the reason sits next to it -- a menu that disappears on some pages reads as a
 * bug, and greying out without saying why only moves the question.
 *
 * The whole thing is a plain form: a <select>, a submit button, and (over ten realms) a filter
 * input that ships `hidden` and is revealed by realm.js.  Nothing here is needed for it to work.
 */
.realm-select {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  /* Right-aligned, and second of the three header groups: this is the realm form's half of the
     two-row header described under .site-nav.  `order` is what puts it above the nav row while
     base.html still writes it after the nav. */
  margin: 0 0 0 auto;
  order: 2;
  /* Never the shrink victim.  The <select> below is the only shrinkable thing in the header and it
     inherits `min-width: 0` from the shared form rule (:363), so a flex item with no floor
     truncated its own realm name rather than the row giving way. */
  flex: 0 0 auto;
}
.realm-select-label {
  color: var(--text-dim);
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.realm-select select,
.realm-select .realm-filter {
  background: var(--bg-inset);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  /* 0.9rem was 14.4px, under Safari's 16px zoom threshold -- and this control is in the header of
     every page, so it was the one field a reader could not avoid.  Same floor as the shared form
     rule (:361). */
  font-size: max(1rem, 16px);
  padding: 0.3rem 0.5rem;
  /* A floor as well as a ceiling, undoing the `min-width: 0` these inherit from :363. */
  min-width: 9rem;
  max-width: 14rem;
}
.realm-select .realm-go { padding: 0.3rem 0.7rem; font-size: 0.85rem; }
/* Disabled, not gone: still readable, visibly inert, and the reason is right there. */
.realm-select.is-disabled select,
.realm-select.is-disabled .realm-go { opacity: 0.55; cursor: not-allowed; }
.realm-why {
  color: var(--text-faint);
  font-size: 0.8rem;
  max-width: 22rem;
}
/* The one-row header, and the only place the two-row default is undone.  See .site-nav for where
 * 90rem comes from: it is the width at which --maxw steps to 88rem and the shell's 1376px of inner
 * width finally holds the header on one line.  `margin-left: 0` on the realm form matters -- two
 * auto margins on one flex line split the free space between them instead of pushing both groups to
 * the right edge.
 *
 * Re-derived when "Auctions" joined the navigation (docs/08 D4), from the navigation as it actually
 * is: nine links signed in -- not the seven the file's old "819px" figure was measured from, which
 * predates /nodes and so was already one item short before Auctions made it two.  The sum that has
 * to hold is brand 196 + 20 + nav + 20 + realm form 257 <= 1376, so the navigation may be 883px
 * wide, and at the padding and gaps of :183 it is 1001px.  The two rules below buy the 118px back
 * where the header is on one line and nowhere else: 0.3rem of horizontal padding per link instead
 * of 0.6rem (9 x 9.6px = 86px) and 0.4rem between them instead of 0.75rem (8 x 5.6px = 45px),
 * which brings the navigation to 869px and the line to 1362px -- 14px of slack rather than a
 * deficit.  Every figure is DejaVu Sans standing in for system-ui, which is ~10% wide, so a real
 * UI font leaves more room than this says; the point of re-deriving was that the line should not
 * depend on that allowance.
 * Below the breakpoint the navigation has a full row to itself and wraps, so none of it applies. */
@media (min-width: 90rem) {
  .site-nav { order: 0; width: auto; margin-left: auto; }
  .site-nav ul { gap: 0.25rem 0.4rem; }
  .site-nav a { padding-inline: 0.3rem; }
  .realm-select { order: 0; margin-left: 0; }
}
/* Was 60rem, which put the realm form on a row of its own from 960px down and so never let the
 * two-row header exist: brand, realm and nav made three rows all the way from 960 to 736.  The
 * form needs brand + gap + itself = 473px measured, so it sits beside the brand comfortably at
 * 47rem (720px of inner width) and the honest boundary is the phone one the file already has. */
@media (max-width: 46rem) {
  .realm-select { width: 100%; }
  .realm-select select { flex: 1 1 8rem; max-width: none; }
  /* On a phone the reason would push the navigation off the screen; it stays in the document for
   * a screen reader (aria-describedby points at it) and is folded away visually. */
  .realm-why { flex-basis: 100%; }
}

/* ---------------------------------------------------------- leaderboards (docs/09 C)
 *
 * The window chips reuse .realm-chip, because they are the same control in the same visual
 * language -- a row of choices where exactly one is current.  The only thing an <a> needs that a
 * <button> did not is the underline turned off.
 */
.realm-picker a.realm-chip { text-decoration: none; }
.board-controls { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: baseline; margin: 0.75rem 0 1rem; }
.board-controls .basis { margin: 0; }
/* The tallest cell in a board is the caption -- the self-reported sentence, on every table -- so
 * the cards are aligned on their headings rather than stretched to the tallest caption. */
.board-summary .card, .grid .card.board { align-self: start; }

/* ------------------------------------------------- where the data came from (docs/09 E)
 *
 * The source marker is deliberately quiet: it sits beside a heading or a table caption at the
 * size of the sample-size text, in the same faint colour, and it is only the reader's eye that
 * has to find it -- not their attention.  A marker that shouted would make every block on the
 * site look like a warning.
 *
 * It is a <details>, so all of this is styling a disclosure widget: the arrow is removed (the
 * `ⓘ` is the affordance) and the open state gets the one sentence underneath.  No JavaScript is
 * involved in opening it, which is why the marker still works on a printed page and in a reader
 * that has scripts off.
 */
.source-mark { display: inline-block; vertical-align: baseline; }
.source-mark > summary {
  display: inline-flex;
  align-items: baseline;
  gap: 0.2rem;
  list-style: none;
  cursor: pointer;
  color: var(--text-faint);
  font-size: 0.78rem;
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 0 0.35rem;
}
/* Safari and older WebKit draw their own triangle through this pseudo-element. */
.source-mark > summary::-webkit-details-marker { display: none; }
.source-mark > summary:hover,
.source-mark[open] > summary {
  color: var(--text-dim);
  border-color: var(--border);
  background: var(--bg-inset);
}
.source-mark > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.source-glyph { font-size: 0.9em; }
.source-note {
  display: block;
  max-width: 34rem;
  margin: 0.35rem 0 0.15rem;
  padding: 0.4rem 0.6rem;
  border-left: 3px solid var(--border-strong);
  background: var(--bg-inset);
  color: var(--text-dim);
  font-size: 0.8rem;
  line-height: 1.45;
}
/* On a block-level marker (one that owns its own line above a table) the note is already on its
 * own line; inline markers get the note as a block underneath, which is the whole reason the
 * summary is inline-flex rather than the details itself being inline. */
.source-mark.block { display: block; margin: 0.25rem 0; }
/* A row of markers where a block has more than one source.  inline-flex so a lone row of them
 * does not stretch, and wrapping so three markers on a phone do not push the layout sideways. */
.source-marks { display: inline-flex; flex-wrap: wrap; gap: 0.25rem; align-items: baseline; }
.observation { margin: 0.4rem 0 0; }

/* /sources: the definition list that carries origin, licence and removal for each source. */
.source-facts { margin: 0.75rem 0 0; }
.source-facts dt { color: var(--text-dim); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; }
.source-facts dd { margin: 0.15rem 0 0.7rem; }

/* ------------------------------------------------ consents on /settings/privacy (docs/07 B1)
 *
 * The consent card holds three separate consents.  Auction data is a block of its own under a
 * rule, and the sales consent is indented inside it with a bar down its side, because it can only
 * be given while auction data consent stands: the nesting on the page is the nesting in the rule.
 */
.consent-block { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.consent-block.nested { margin: 1rem 0 0 0.25rem; padding: 0 0 0 0.9rem; border-top: 0; border-left: 2px solid var(--border-strong); }

/* ------------------------------------------------------------ auctions (docs/08 D4)
 *
 * Almost nothing, on purpose.  An auction house is a row of chips (.realm-picker, already here) and
 * a table (.table-wrap, already here), so what is left is the three things those two do not carry:
 * the third faction, the colour of a price that moved, and a box for the one chart on the site.
 */

/* The neutral auction house.  Filed under the scanning character's own faction because the addon
 * cannot recognise a neutral auctioneer (docs/08 0.2) -- but a scan may name it, and a badge with
 * no rule of its own would have rendered in the plain grey that means "no faction stated".  The
 * game's neutral gold, computed at 14:1 on --bg-inset, which is the surface a badge sits on. */
.badge.faction-Neutral { border-color: #b49a4a; color: #f0dc96; }

/* A price that moved.  The colour is a second channel and never the only one: every one of these
 * cells prints a signed number, so the direction survives a monochrome screen, a printed page and
 * the eight per cent of readers for whom red and green are one colour. */
.change-up { color: var(--accent); font-variant-numeric: tabular-nums; }
.change-down { color: var(--danger); font-variant-numeric: tabular-nums; }

/* The chips carry two or three badges each (faction, "yours", the item count), so they need to
 * breathe a little more than the realm and period chips this class is shared with -- and they are
 * allowed to wrap inside themselves.  A .realm-chip is an inline-flex box with no floor and no
 * shrink, so "Aggra (Portugues) [Horde] [yours] 6,120" at 360px (measured at about 330px against
 * 328px of shell) would have pushed the whole document sideways rather than breaking; wrapping
 * drops the badges onto a second line inside the chip instead. */
.auction-houses .realm-chip { gap: 0.35rem; padding: 0.3rem 0.7rem; flex-wrap: wrap; }

/* The chart's box.  An explicit height rather than an aspect ratio: chart.js measures the element
 * with getBoundingClientRect and sizes the bitmap to devicePixelRatio, and a canvas with no CSS
 * height would be measured at the element's default 150px whatever the page wanted.  16rem holds
 * five gridlines and their labels without crowding; the phone step is still above the script's own
 * 160px floor, so a narrow screen loses width and not the axis. */
.chart-figure { margin: 0.75rem 0 1rem; }
.chart-figure canvas { display: block; width: 100%; height: 16rem; }
.chart-figure figcaption { margin-top: 0.4rem; }
@media (max-width: 46rem) {
  .chart-figure canvas { height: 12rem; }
}
