/* ComfyCraft - one stylesheet, no build step, no framework.
   Warm dark ground, parchment text, gold rules. */

:root {
  --ink:        #ece0c8;   /* parchment, on dark */
  --ink-dim:    #a2917a;
  --ink-faint:  #7a6b58;
  --ground:     #14100c;
  --ground-2:   #1c1711;
  --sheet:      #1a150f;   /* the page's own surface, lighter than the ground */
  --panel:      #221c15;
  --panel-2:    #2a231a;
  --line:       #3a3024;
  --gold:       #c9a253;
  --gold-lit:   #e6c076;
  --gold-dim:   #8a6f38;
  --good:       #7fa860;
  --bad:        #c0553f;
  --alliance:   #6f8fc4;
  --horde:      #b4564a;

  /* ONE TYPEFACE, not two. A serif display over a sans UI is the pairing that
     made this page feel assembled from parts: the ornate gold frame, a
     bookish headline and a system-UI paragraph are three different voices in
     one box. Everything textual is the serif now, at every size, and the mono
     is kept only for things you literally type -- realmlist lines, commands,
     item entries -- where it is doing a job rather than setting a tone.
     No web font: the site has no build step and must work offline. */
  --serif: Georgia, "Palatino Linotype", "Book Antiqua", Palatino, serif;
  --mono: "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;

  --wrap: 1080px;
  /* Square. A 3px radius is a web convention, and next to a frame with mitred
     corner ornaments it reads as a different toolkit. */
  --radius: 0;
}

* { box-sizing: border-box; }

/* The `hidden` ATTRIBUTE is only a UA-stylesheet rule, so ANY author rule that
   sets display beats it and the element stays on screen with el.hidden === true
   -- which reads as a JavaScript bug and is not one. Caught on the icon
   browser's "Show more" button, whose own `display: block` kept it visible
   after every result had been drawn. Global and !important so no future
   component can reintroduce it. */
[hidden] { display: none !important; }

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

body {
  margin: 0;
  /* The ground the sheet lies on. A background image goes here. */
  background-color: var(--ground);
  /* A slow warm glow behind the header, so the page has a top without a
     banner image to load. */
  background-image:
    radial-gradient(1100px 420px at 50% -140px, rgba(201, 162, 83, 0.18), transparent 70%);
  background-repeat: no-repeat;
  background-attachment: fixed;
  color: var(--ink);
  font: 16.5px/1.7 var(--serif);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main { flex: 1 0 auto; }

/* ------------------------------------------------------------------- sheet */

/* The whole page is one sheet lying on the ground colour: the client's gold
   frame runs round the site rather than round every card, and .wow-grain gives
   it the parchment fibre. body keeps the ground and its glow, so a background
   image later goes there and this sits on top of it.

   border/background here are the FALLBACK for a checkout with no public/art/;
   frames.css loads after this file and overrides them. */
.sheet {
  /* WIDTH 100%, NOT JUST MAX-WIDTH. The sheet is a flex item in a column
     container with auto horizontal margins, and auto cross-axis margins switch
     OFF the align-items:stretch that would otherwise size it -- so without this
     it is shrink-to-fit and every page ends up whatever width its own content
     happens to want. Measured before the fix: the homepage 1144, /play 1074,
     /icons 799. Three pages, three page widths, on a site whose whole visual
     idea is one continuous sheet. */
  width: 100%;
  max-width: 1180px;
  margin: 26px auto 40px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background-color: var(--sheet);
  display: flex; flex-direction: column;
  flex: 1 0 auto;
}

.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 20px; }
.narrow { max-width: 620px; }

a { color: var(--gold); text-decoration-color: var(--gold-dim); text-underline-offset: 2px; }
a:hover { color: var(--gold-lit); }

code { font-family: var(--mono); font-size: 0.92em; }

.dim { color: var(--ink-dim); }
.bad { color: var(--bad); }
.nowrap { white-space: nowrap; }

.skip {
  position: absolute; left: -9999px;
  background: var(--panel); color: var(--ink); padding: 10px 14px; z-index: 10;
}
.skip:focus { left: 12px; top: 12px; }

/* ------------------------------------------------------------------ rules */

/* THE ONE DIVIDER. A 1px grey hairline is a table border -- it belongs to a
   spreadsheet, not to a page inside a gold frame, and having several of them
   at different weights is most of what made this look thrown together. Every
   division on the site is now the same gold gradient, fading out at both ends
   so it reads as ornament rather than as structure. */
.rule,
hr {
  border: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
  margin: 0;
}

/* The same thing as an element's own edge. border-image needs a slice of 1 to
   take a gradient, and it paints every side -- which is harmless here because
   only one side has any width. */
.edge-top { border-top: 1px solid transparent; }
.edge-bottom { border-bottom: 1px solid transparent; }
/* `.hero-art` was briefly in this list, to close the banner at the bottom the
   way the topbar's rule closes the header at the top. Tried and taken out on
   2026-09-08: the banner's whole treatment is a mask that dissolves the picture
   into the sheet with NO edges at all, and a hard line across the bottom argues
   with that -- it is the "bolted on" look the mask exists to avoid, just drawn
   in gold. The top edge does not have the same problem because there is a real
   boundary there anyway: the nav. */
.edge-top,
.edge-bottom {
  border-image: linear-gradient(90deg, transparent, var(--gold-dim) 20%, var(--gold-dim) 80%, transparent) 1;
}

/* ------------------------------------------------------------------ header */

/* Not sticky any more, and no background of its own. Inside the sheet it is
   the top of a document, and a bar floating over a framed page reads as a
   different site pasted on top of this one. */
.topbar {
  border-bottom: 1px solid transparent;
  border-image: linear-gradient(90deg, transparent, var(--gold-dim) 20%, var(--gold-dim) 80%, transparent) 1;
}
.topbar-in {
  display: flex; align-items: center; gap: 22px;
  min-height: 62px; flex-wrap: wrap;
}
.brand {
  display: inline-flex; align-items: baseline; gap: 8px;
  font-family: var(--serif); font-size: 1.24rem; letter-spacing: 0.02em;
  color: var(--ink); text-decoration: none;
}
.brand:hover .brand-text { color: var(--gold-lit); }

.mainnav { display: flex; gap: 18px; margin-right: auto; flex-wrap: wrap; }
.mainnav a {
  color: var(--ink-dim); text-decoration: none; font-size: 0.94rem;
  padding: 4px 0; border-bottom: 2px solid transparent;
}
.mainnav a:hover { color: var(--ink); }
.mainnav a.on { color: var(--gold); border-bottom-color: var(--gold); }

/* The nav drop-downs: Community, and Development. A <details> wearing the same
   clothes as the links beside it, so the bar reads as one row of five things
   and not as three links and two widgets. */
.navmenu { position: relative; }
.navmenu-btn {
  /* BOTH of these kill the disclosure triangle -- `list-style` covers Firefox
     and modern Chrome, the pseudo-element covers older WebKit. The marker is
     a bullet on a summary and it sat left of the word, breaking the row. */
  list-style: none;
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 7px;
  color: var(--ink-dim); font-size: 0.94rem;
  padding: 4px 0; border-bottom: 2px solid transparent;
}
.navmenu-btn::-webkit-details-marker { display: none; }
.navmenu-btn:hover { color: var(--ink); }
.navmenu-btn.on { color: var(--gold); border-bottom-color: var(--gold); }

/* A chevron drawn from two borders rather than an SVG or a glyph: it inherits
   the colour of the word for free, so it goes gold with it. */
.navmenu-btn::after {
  content: ""; width: 5px; height: 5px;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
}
.navmenu[open] .navmenu-btn::after { transform: translateY(1px) rotate(-135deg); }

.navmenu-panel {
  position: absolute; top: calc(100% + 9px); left: 0; z-index: 40;
  min-width: 168px;
  display: flex; flex-direction: column;
  background: var(--panel); border: 1px solid var(--line);
}
/* Panel links are NOT bar links: no underline-on-current, a real hit area, and
   they must not wrap. The selector carries `.mainnav` so it outweighs the
   `.mainnav a` rules above without needing !important. */
.mainnav .navmenu-panel a {
  padding: 9px 14px; border-bottom: 0; white-space: nowrap;
}
.mainnav .navmenu-panel a + a { border-top: 1px solid var(--line); }
.mainnav .navmenu-panel a:hover { background: var(--panel-2); color: var(--ink); }
.mainnav .navmenu-panel a.on { color: var(--gold); }

.userbar { display: flex; align-items: center; gap: 12px; }
.ghost { color: var(--ink-dim); text-decoration: none; font-size: 0.94rem; }
.ghost:hover { color: var(--ink); }
.linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--ink-faint); font: inherit; font-size: 0.94rem;
}
.linkish:hover { color: var(--bad); }
.inline { display: inline; }

/* ------------------------------------------------------------------- flash */

.flash {
  margin: 16px 0 0; padding: 11px 16px;
  border-left: 3px solid var(--good); border-radius: var(--radius);
  background: var(--panel);
}
.flash-bad { border-left-color: var(--bad); }

/* -------------------------------------------------------------------- hero */

/* The top padding is smaller than it was (68px) because the medallion below
   now supplies the air above the title itself. */
.hero { padding: 44px 0 40px; text-align: center; }
.hero-in { display: flex; flex-direction: column; align-items: center; }

/* --------------------------------------------------------- hero medallion */

/* THE ONE CURVED THING ON THE SITE, and deliberately so: --radius is 0 and
   everything else here is square, but a portrait in a square is a mugshot and
   this drawing is a face. The closed shape is what makes it read as a device or
   a seal rather than a picture someone pasted in.

   A SQUIRCLE, AND `corner-shape: squircle` DOES NOT PRODUCE ONE. Measured in
   Chrome 152: the keyword parses -- CSS.supports says true and the declaration
   survives in cssRules -- and computes to `superellipse(2)`, which is `round`,
   the initial value. So the shape on screen was border-radius and nothing else,
   a rounded square wearing a squircle's name. `superellipse(4)` computes to
   itself and actually renders. If this ever looks like a plain rounded box
   again, read the COMPUTED value before changing anything else.

   Two numbers, two jobs: `superellipse(4)` is how square the curve is, the
   40% is how much of the edge it eats. The radius went 30% -> 40% on 2026-09-08
   because the corners wanted to be softer; the exponent did not move, because
   lowering it is how you stop having a squircle at all.

   The wrapper does the clipping, and the drawing PEEKS OVER THE BOTTOM rather
   than filling the frame. That is the whole composition: the source is a
   610x610 PNG cropped at its own bottom edge, mid-face, with no chin and no
   shoulders, so there is nothing to fill a frame WITH. Enlarged to fill one it
   turns into a face pressed against glass; sat small on the bottom edge, the
   missing half of the head reads as being below the rim, which is what the
   drawing was already doing.

   So: the full width, horizontally centred, bottom flush. A squircle's nearly
   straight bottom edge is a real bonus here that a circle did not give -- the
   face rests on a line instead of a tangent point, so the mouth and both cheeks
   survive the crop instead of being eaten by the curve.

   The ring is two rings. The 1px gold-dim border is the edge; the box-shadow's
   first, spread-only layer is a band of the sheet colour outside it, which
   holds the medallion off the banner underneath so the gold does not have to
   compete with whatever the photograph is doing at that spot. */
.hero-avatar {
  position: relative;
  width: 128px; height: 128px;
  margin: 0 0 26px;
  border-radius: 40%;
  corner-shape: superellipse(4);
  overflow: hidden;
  border: 1px solid var(--gold-dim);
  background: var(--panel);
  box-shadow: 0 0 0 5px var(--sheet), 0 6px 20px rgba(0, 0, 0, 0.55);
}
.hero-avatar img {
  position: absolute;
  display: block;
  width: 100%; height: auto;
  left: 50%; bottom: 0;
  transform: translateX(-50%);
}
.eyebrow {
  margin: 0 0 12px; text-transform: uppercase; letter-spacing: 0.16em;
  font-size: 0.72rem; color: var(--gold-dim);
}
/* line-height MATTERS HERE, and not for the reason it usually does. The title
   is one line, so tightening it changes nothing about the title -- what it
   changes is the size of its BOX. Inherited from body, the line-height was 1.7
   against a 64px font: 108.8px of box around 64px of letters, which is ~22px of
   empty half-leading hanging below the word before any margin starts. The gap
   under the title was therefore 10px of margin and 32px of actual space, and no
   amount of reading the margins would have said so. At 1.05 the box hugs the
   letters and the numbers below mean what they look like. */
.hero h1 {
  font-weight: 400;
  font-size: clamp(2.6rem, 7vw, 4rem);
  line-height: 1.05;
  margin: 0; letter-spacing: 0.01em;
  color: var(--ink);
}
.lede { color: var(--ink-dim); font-size: 1.1rem; margin: 10px 0 0; }
/* DELIBERATELY UNEQUAL, and it was equal first. Equal air read as three
   unrelated things stacked at even intervals; the tagline belongs TO the title
   -- it finishes the sentence the name starts -- and the buttons are the next
   thing you do. So the tagline is pulled up under the name and the buttons are
   pushed away from both. Rendered: ~20px above the tagline, ~36px below.

   These are only readable as numbers because .hero h1 lost its inherited 1.7
   line-height above; before that, 22 and 22 were 32 and 26 on screen. Scoped to
   .hero -- .lede is used by eighteen other views that are not laying out a
   banner. */
.hero .lede { margin-top: 12px; }
.hero .hero-actions { margin-top: 30px; }
/* Still named for the hero because it is still the `hero_body` setting, edited
   in one field on the admin page -- but it renders in its own band BELOW the
   banner now, so the 26px that used to separate it from the lede is gone and
   the band's own padding does the spacing.

   NO max-width. The 640px column was right when this sat inside a centred hero
   and had to look like part of a stack; in its own band it just looked inset,
   with the "What is here" heading below starting further left than the
   paragraph above it. It fills .wrap like every other band now, so the page has
   one left edge. The measure is long for body copy at this width -- if it ever
   reads badly, the fix is a max-width here and not a narrower .wrap. */
/* --ink-dim, the same colour as .card-body below it, not the --ink it inherited
   from body. The two are the only prose in this part of the page, they sit one
   above the other, and at two different weights of parchment the About block
   read as the important one and the cards as a footnote to it -- which is
   backwards: the cards are what the page is for. Size is deliberately NOT
   matched; this is the lead paragraph and it stays at body size. */
.hero-body { text-align: left; color: var(--ink-dim); }
/* The roadmap button under the About paragraph. Its own <p> so it sits on the
   text's left edge and takes a margin from it rather than from the band. */
.about-more { margin: 22px 0 0; }
.hero-body p { margin: 0 0 14px; }
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 26px; }

/* ------------------------------------------------------------- hero banner */

/* A picture behind the title, the lede and the realmlist. OPTIONAL: routes/
   pages.js looks for website/public/banner.jpg (or .jpeg/.png/.webp) and only
   then does home.html add .hero-art and set --banner. Nothing below runs on a
   machine with no banner file, which is why the plain .hero rules above are
   still the complete description of the default homepage.

   THE TREATMENT IS THE WHOLE JOB, and it is not one knob. A daylight WoW
   screenshot is bright, green and busy; this page is parchment text on a warm
   near-black sheet. Dropped in raw the picture does not read as a background,
   it reads as a second website showing through a hole in this one, and the
   lede goes illegible over the sky. Four things fix it, and taking any one out
   is visible:

     - filter reduces the image to a suggestion of a place. BRIGHTNESS IS THE
       ONE NUMBER THAT FOLLOWS THE PICTURE, and it goes in whichever direction
       the picture needs: the banner in use is a dusk shot with a mean
       luminance of 37.5 of 255, so it is LIFTED, not knocked back. A bright
       daylight screenshot would want about 0.45 instead. Aim for roughly
       55-70 after the filter - far enough above --sheet (26) to read as a
       picture, dark enough for parchment text - so brightness ~= 60 / mean.
       docs/ai/website.md has a one-liner that measures the mean in a browser
       console. saturate stops the scene fighting the gold, and the small
       sepia pulls it toward the page's warm cast rather than leaving a cool
       blue rectangle in a warm room.
     - blur softens it, and separately hides that the source is a 640x480
       screenshot stretched over a 1180px sheet. inset is NEGATIVE to give the
       blur something to sample past each edge - at inset 0 it smears
       transparency inward and you get a grey halo round all four sides that
       looks like a botched vignette.
     - the mask fades the top and bottom to nothing, so the banner has no
       edges at all. A hard edge under the realmlist is what makes a banner
       look bolted on; dissolving into the sheet colour is what makes it look
       like the page is lit from behind.
     - ::after is the scrim: a flat warm wash plus a vignette, so contrast
       under the text does not depend on which part of the screenshot happens
       to be pale. This is the layer to reach for first if text is hard to
       read - raise the middle stop, not the brightness, or the picture
       disappears entirely.

   isolation:isolate makes .hero-art a stacking context, so the two z-index:-1
   pseudo-elements sit above the section's own background but below the words.
   Without it the -1 escapes to the sheet and the banner paints behind THAT,
   i.e. nowhere visible. */
/* THE PICTURE RUNS OUT TO THE GOLD LINE, not to the content box.

   .wow-frame--gold is a 32px border-image, and the ornament is drawn only in
   the outer 11px of that band -- measured off a screenshot, constant down the
   whole edge: the line sits 6-10px in and the remaining 21px is transparent.
   The sheet's own background runs under all of it (border-box, see .wow-frame),
   so the ground was already right; the banner is painted by a pseudo-element on
   a CHILD, which starts at the content box and left that 21px reading as
   unexplained padding round the picture.

   It stops AT the line rather than under it because it cannot do otherwise: a
   descendant always paints above its ancestor's border, so a full 32px bleed
   would lay the picture over the gold and break the frame along the hero.

   Negative margin plus equal padding, so the BOX grows and the words inside do
   not move -- .wrap still centres on the same axis. The bleed is a variable
   because the frame halves to 16px under 720px and the number has to follow it;
   both values are (border width - the ornament's own width). */
.hero-art {
  --hero-bleed: 21px;
  position: relative;
  isolation: isolate;
  /* Clips the two pseudo-elements to the widened box, which is what keeps the
     -14px on ::before (blur allowance) off the nav above and the section below. */
  overflow: hidden;
  margin-inline: calc(var(--hero-bleed) * -1);
  /* A little more room than the plain hero: the picture wants air above the
     title, and the bottom fade needs somewhere to happen below the status
     line rather than across it. */
  padding: 68px var(--hero-bleed) 62px;
}

.hero-art::before,
.hero-art::after {
  content: '';
  position: absolute;
  z-index: -1;
  pointer-events: none;
}

.hero-art::before {
  inset: -14px;
  /* Extension-free and fixed. lib/banner.js maps this one path onto whichever
     of banner.jpg/.jpeg/.png/.webp is actually there, because a stylesheet
     cannot ask for "whichever exists" and the site's CSP (style-src 'self')
     rules out handing the real filename over in an inline style. */
  background-image: url('/static/banner');
  background-repeat: no-repeat;
  background-size: cover;
  /* Below centre, not centred. A landscape shot puts sky in its top third and
     the thing worth seeing in the middle; the title sits over the middle. */
  background-position: center 58%;
  filter: brightness(1.55) saturate(0.6) contrast(0.94) sepia(0.22) blur(2px);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 20%, #000 66%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 20%, #000 66%, transparent 100%);
}

.hero-art::after {
  inset: 0;
  background-image:
    radial-gradient(120% 95% at 50% 42%, transparent 12%, rgba(20, 16, 12, 0.5) 100%),
    linear-gradient(to bottom,
      rgba(26, 21, 15, 0.46) 0%,
      rgba(26, 21, 15, 0.26) 40%,
      rgba(26, 21, 15, 0.84) 100%);
}

/* `.hero-art .realmbox` used to live here -- an opaque panel and a gold edge so
   the realmlist did not have to trust the scrim. It went out with the realmbox
   itself: `.hero-art` is only ever on the homepage hero and the homepage hero
   no longer holds one, so the rule could never match again. The plain
   `.realmbox` further up is alive and used by /play and /account. */

/* Same problem, quieter: .btn-quiet is deliberately transparent, which over a
   picture leaves its label floating on whatever is behind it. It keeps the
   quiet LOOK - dim text, plain border, no gold fill - and gains a ground. */
.hero-art .btn-quiet { background-color: rgba(34, 28, 21, 0.82); }

/* The small type is the first thing to go illegible - the lede is --ink-dim, a
   mid-tone with nowhere to hide. A shadow costs nothing and works whatever the
   picture turns out to be, which a hand-picked colour does not.

   This list was three selectors longer. `.statusline` and `.hero-body` came off
   with the elements: the status line is the topbar dot now and the paragraph
   renders below the banner, so neither is ever over the picture again. Only the
   lede and the title still are. */
.hero-art .lede { text-shadow: 0 1px 6px rgba(0, 0, 0, 0.85); }
.hero-art h1 { text-shadow: 0 2px 22px rgba(0, 0, 0, 0.7); }

/* -------------------------------------------------------------- realmlist */

/* A plain border, not a frame kit. This wore the tooltip frame for a while and
   it was the wrong call twice over: a second ornate border inside the sheet's
   own is the thing the whole redesign was moving away from, and the same box
   on /play and /account never had one, so the homepage was the odd page out.
   Padding is real again now that there is no 16px border standing in for it. */
.realmbox {
  display: inline-flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 30px; padding: 10px 14px;
  border: 1px solid var(--line);
  background-color: var(--panel);
}
.realmbox-inline { margin-top: 12px; }
.realmbox-label {
  text-transform: uppercase; letter-spacing: 0.12em;
  font-size: 0.66rem; color: var(--gold-dim);
}
.realmbox code { font-size: 1rem; color: var(--gold-lit); }
.copy {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink-dim);
  padding: 5px 12px; border-radius: var(--radius); cursor: pointer;
  font: inherit; font-size: 0.82rem;
}
.copy:hover { border-color: var(--gold-dim); color: var(--ink); }
.copy.done { color: var(--good); border-color: var(--good); }

/* `.statusline` was here. It was the homepage's spelled-out "the realm is up"
   line, and it had exactly one use; the topbar dot replaced it on every page
   at once, so the rules went rather than sitting here matching nothing. */

.dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-right: 6px; vertical-align: middle;
}
.dot-up { background: var(--good); box-shadow: 0 0 7px rgba(127, 168, 96, 0.75); }
.dot-down { background: var(--bad); }

/* The same light, in the nav. A SEPARATE class from .dot rather than a modifier
   on it, because the two differ in the only two things .dot sets: this one is a
   notch smaller so it does not out-weigh 0.94rem nav text, and its margin is on
   the LEFT - it trails "Connect" instead of leading a sentence. The colours are
   shared, so .navdot always ships with .dot-up or .dot-down. */
.navdot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-left: 7px; vertical-align: middle;
}

/* And once more on the homepage's "How to connect" button. Trailing like the
   nav one, but at .dot's 8px: against a 1rem button the nav's 7px vanishes.
   Same rule as the others - always with .dot-up or .dot-down. */
.btndot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  margin-left: 9px; vertical-align: middle;
}

/* ------------------------------------------------------------------- bands */

/* VERTICAL ONLY, AND THAT IS THE WHOLE POINT OF THE LONGHAND. `.band` and
   `.page-head` are worn by a `.wrap`, and `.wrap` is where this site's 20px
   gutter is defined -- once. Written as the shorthand `padding: 40px 0` they
   also set the horizontal padding, to zero, which silently cancelled that
   gutter on all 91 sections wearing one of these two classes.

   The result was a page misaligned with its own furniture: the topbar brand and
   the footer sit at the gutter, and every band of content sat 20px to the left
   of them. It showed up worst on the database detail pages, where a heading in
   a `.wrap db-crumb` (gutter kept) sits directly above a body in a `.wrap band`
   (gutter cancelled) and the 20px step between them is a straight edge to
   compare against. Longhand here means the gutter stays defined in exactly one
   place. */
.band { padding-top: 40px; padding-bottom: 40px; }
.band-last { padding-bottom: 68px; }
.page-head { padding-top: 54px; padding-bottom: 6px; }
.page-head h1 {
  font-weight: 400; font-size: clamp(2rem, 5vw, 2.7rem);
  margin: 0;
}
/* A chapter heading, not an underlined one: the rule runs from the words out
   to the margin and fades, so it frames the title instead of boxing it. */
.section-h {
  display: flex; align-items: center; gap: 16px;
  font-weight: 400; font-size: 1.4rem;
  margin: 0 0 22px;
  color: var(--ink);
}
.section-h::after {
  content: ""; flex: 1 1 auto; height: 1px;
  background: linear-gradient(90deg, var(--gold-dim), transparent);
}
/* A line of context under a .section-h, on the roadmap. The negative top margin
   is eating part of the heading's own 22px bottom margin rather than fighting
   it: the blurb belongs to the heading above it, not to the cards below, and at
   the full 22px it floated between the two looking like neither. */
.group-blurb {
  color: var(--ink-dim); font-size: 0.95rem;
  margin: -12px 0 22px;
}

/* ---------------------------------------------------------------- gallery */

/* Player screenshots on the homepage, and the admin list that manages them.
   Same auto-fit grid as .cards, at a bigger minimum: a WoW screenshot at 300px
   is a smear, and 340 is about where the UI in the corners stops being mush.

   UNIFORM TILES, cropped to 16:10 with object-fit: cover. This was the other
   way round first -- uncropped, on the reasoning that people chose their
   framing and a grid should not throw it away -- and a mixed wall of 4:3, 16:9
   and one square drawing read as a mess rather than as a wall of photographs.
   A common shape is what makes it a gallery.

   The crop is affordable BECAUSE of the lightbox: the tile shows the middle of
   the picture and clicking gives back the whole of it, so the framing is not
   lost, only deferred. Take the lightbox out and this decision goes back the
   other way. 16:10 rather than 16:9 because a WoW screenshot is as likely to be
   4:3 as widescreen, and the shallower ratio takes less off the top and bottom
   of the older ones. */
/* 320 AND NOT 340, AND THE 20px IS ARITHMETIC RATHER THAN TASTE. Three columns
   need 3 * min + 2 * gap, and a `.wrap` is --wrap minus its two 20px gutters =
   1040px. At 340 that is 1068, which is 28px more than there is, so auto-fit
   quietly gave two columns of 508 instead of three -- a gallery of three
   screenshots drawn as 2 + 1.

   It fitted until 2026-09-09, and not for a good reason: `.band` was written
   `padding: 40px 0`, whose shorthand zeroed the horizontal padding and
   cancelled the page gutter, so this grid was measuring 1080px. Putting the
   gutter back (see the note on .band) took the 40px away and this was the one
   place in the site that had been depending on it.

   3 * 320 + 48 = 1008, so it fits with 32px to spare rather than failing on a
   rounding difference or a scrollbar. The tiles still RENDER at (1040-48)/3 =
   330px, because the 1fr maximum takes all the width back -- the minimum only
   decides how many fit. 330 against the 340 the note below wanted, and the
   thing that note is really guarding against is 300. */
.gallery {
  display: grid;
  gap: 26px 24px;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  align-items: start;
}
.shot { margin: 0; }
.shot a { display: block; cursor: zoom-in; }
.shot img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  /* The middle horizontally, but ABOVE centre vertically: a WoW screenshot puts
     the action in the upper half and the action bar along the bottom, so
     centring the crop reliably keeps the UI and loses the sky. */
  object-position: 50% 40%;
  border: 1px solid var(--line);
  background: var(--panel);
}
.shot a:hover img { border-color: var(--gold-dim); }

/* ------------------------------------------------------------- lightbox */

/* Built by app.js, not by any template - see the comment there. It is fixed to
   the viewport rather than positioned in the page, so it does not care where
   the gallery is or how far down it has been scrolled. */
.lightbox {
  position: fixed; inset: 0; z-index: 50;
  display: flex; align-items: center; justify-content: center;
  padding: 40px;
  /* Nearly opaque, not fully: a sliver of the page behind is what says "this is
     on top of where you were" rather than "you have navigated somewhere". */
  background: rgba(10, 8, 6, 0.93);
}
.lightbox-fig {
  margin: 0; max-width: 100%; max-height: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
}
/* min-height: 0 is not decoration. Without it the image refuses to shrink below
   its intrinsic height inside a flex column and a tall screenshot runs off the
   bottom of the screen with no way to see it. */
.lightbox-fig img {
  display: block; min-height: 0;
  max-width: 100%; max-height: calc(100vh - 140px);
  width: auto; height: auto;
  border: 1px solid var(--gold-dim);
}
.lightbox-fig figcaption {
  text-align: center; display: flex; flex-direction: column; gap: 4px;
}
.lightbox-caption { color: var(--ink); font-size: 0.98rem; }
.lightbox-credit {
  color: var(--gold-dim);
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.14em;
}
.lightbox-close {
  position: absolute; top: 18px; right: 22px;
  background: none; border: 0; cursor: pointer;
  color: var(--ink-dim); font: 400 2rem/1 var(--serif);
  padding: 6px 12px;
}
.lightbox-close:hover { color: var(--gold-lit); }
/* Set on <html> while the overlay is up, so the page behind cannot scroll
   under it. */
.lightbox-open, .lightbox-open body { overflow: hidden; }
.shot-caption { color: var(--ink-dim); font-size: 0.94rem; margin: 10px 0 0; }
/* The name, in gold and small caps, so it reads as an attribution rather than
   as a second line of the caption. */
.shot-credit {
  color: var(--gold-dim); margin: 3px 0 0;
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.14em;
}

/* The admin side: a thumbnail, its fields, and a Remove that is deliberately
   the far column rather than next to Save. */
.shot-admin { display: flex; flex-direction: column; gap: 26px; }
.shot-admin-row {
  display: grid;
  grid-template-columns: 180px 1fr auto;
  gap: 20px; align-items: start;
  border-top: 1px solid var(--line); padding-top: 22px;
}
.shot-admin-row:first-child { border-top: 0; padding-top: 0; }
.shot-admin-thumb img {
  display: block; width: 100%; height: auto; border: 1px solid var(--line);
}
.shot-admin-fields .fieldrow { margin-bottom: 10px; }
/* One column under 700px. Three columns of 180px + fields + a button is not a
   layout a phone can hold, and the admin page is the one place on this site
   somebody might genuinely be editing on one, standing in the world. */
@media (max-width: 700px) {
  .shot-admin-row { grid-template-columns: 1fr; }
}

.backlink { margin-top: 22px; }

.smallprint {
  border-left: 2px solid var(--gold-dim);
  padding: 2px 0 2px 18px;
  color: var(--ink-dim); font-style: italic;
}
.smallprint p { margin: 0 0 10px; }
.smallprint p:last-child { margin-bottom: 0; }

/* ------------------------------------------------------------------- cards */

/* Cards are not objects any more -- they are sections of the one sheet,
   divided by rules. That is what makes the page read as a single document
   rather than a tray of boxes, which is the whole point of moving the frame
   outward. No fill, no border box, no radius.

   The status signal goes back to being the card's TOP RULE, which is where it
   was before the frames arrived and where it belongs: with no box to own, the
   rule is the card's only edge, so colouring it costs nothing and carries
   status in colour as well as in the tag pill's words. */
.cards {
  display: grid;
  gap: 36px 44px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  /* STRETCH, deliberately - the opposite of what these wanted when they were
     framed. A frame around empty space is a mistake; a column of text that
     ends early is not, and equal heights put every "Learn more" on the same
     baseline across the row. That only works because the card is a flex column
     with the link pushed down by margin-top:auto, so the slack collects in one
     place instead of stretching the text block. */
  align-items: stretch;
}
/* No rule across the top -- that was a table border by another name. The
   status colour moves to a short bar above the title: ornament rather than
   division, and it still carries the signal in colour as well as in the tag's
   words. */
.card { padding: 0; display: flex; flex-direction: column; }
.card::before {
  content: ""; display: block;
  width: 30px; height: 2px; margin-bottom: 15px;
  background: var(--gold-dim);
}
.card-live::before { background: var(--gold); }
.card-planned::before { background: var(--ink-faint); }
.card-h { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.card h3 { font-weight: 400; font-size: 1.22rem; margin: 0; }
.card h3 a { text-decoration: none; color: var(--ink); }
.card h3 a:hover { color: var(--gold-lit); }
.card-tag { color: var(--gold-dim); margin: 7px 0 12px; font-size: 0.92rem; }
.card-body { color: var(--ink-dim); font-size: 0.94rem; margin: 0 0 16px; }

/* Pushed to the bottom of the card, which is what makes a row of them line up
   however long the summaries are. */
.card-more {
  margin-top: auto;
  align-self: flex-start;
  font-size: 0.88rem;
  color: var(--gold);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
}
.card-more:hover { color: var(--gold-lit); border-bottom-color: var(--gold-dim); }
.card-more span { display: inline-block; transition: transform 0.15s ease; }
.card-more:hover span { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) {
  .card-more span { transition: none; }
  .card-more:hover span { transform: none; }
}

/* Squared. A pill is a web-app affordance and it fought everything round it. */
.tag {
  display: inline-block; padding: 3px 9px 2px;
  font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.16em;
  border: 1px solid var(--line); color: var(--ink-dim); white-space: nowrap;
}
.tag-live { border-color: var(--good); color: var(--good); }
.tag-in-progress, .tag-experimental { border-color: var(--gold); color: var(--gold); }
.tag-planned { border-color: var(--line); color: var(--ink-faint); }
.tag-staff { border-color: var(--gold-dim); color: var(--gold); }
.tag-bad { border-color: var(--bad); color: var(--bad); }

/* ------------------------------------------------------------------ panels */

.panel {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 22px;
}
.panel h2, .panel h3 {
  font-weight: 400; margin: 0 0 12px; font-size: 1.15rem;
}
.prose p { margin: 0 0 12px; }
.prose p:last-child { margin-bottom: 0; }

.row-between { display: flex; align-items: center; justify-content: space-between; gap: 18px; flex-wrap: wrap; }
.row-between p { margin: 0; }

.twocol { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); align-items: start; }
.stack { display: flex; flex-direction: column; gap: 18px; }

.posts { display: flex; flex-direction: column; gap: 20px; }
.post { border-left: 2px solid var(--gold-dim); padding-left: 18px; }
.post h3 { font-weight: 400; margin: 0; font-size: 1.15rem; }
.post-date { color: var(--ink-faint); font-size: 0.8rem; margin: 2px 0 10px; }

/* Rules above and below rather than a box, for the same reason the cards lost
   theirs: inside a framed sheet, another border is a second frame. */
/* One ornament above it, nothing below -- the footer's own rule closes the
   page, and two rules 200px apart is the stacking that read as clutter. */
.cta { text-align: center; padding: 34px 20px 10px; position: relative; }
.cta::before {
  content: ""; position: absolute; top: 0; left: 15%; right: 15%; height: 1px;
  background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
}
.cta h2 { font-weight: 400; margin: 0 0 8px; }
.cta p { color: var(--ink-dim); margin: 0 0 20px; }

.steps { list-style: none; counter-reset: step; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 26px; }
.steps > li { counter-increment: step; padding-left: 52px; position: relative; }
.steps > li::before {
  content: counter(step);
  position: absolute; left: 0; top: 0;
  width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid var(--gold-dim); color: var(--gold);
  display: grid; place-items: center;
  font-size: 1rem;
}
.steps h2 { font-weight: 400; font-size: 1.2rem; margin: 4px 0 8px; }

/* ------------------------------------------------------------------ status */

.statusgrid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.stat {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px 16px; display: flex; flex-direction: column; gap: 4px;
}
.stat-k { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.11em; color: var(--gold-dim); }
.stat-v { font-family: var(--serif); font-size: 1.4rem; }

/* `.wip` AND ITS NINE PARTS WERE HERE for about an hour on 2026-09-09 -- a
   bordered notice for an unfinished page, with a state sentence and a
   { what, done, why } checklist of which parts were built. /armory and
   /charts wore it and the owner's call was that a placeholder should say it
   is a placeholder and stop, which is a title and one line and needs no rules
   of its own. Deleted rather than left, because a stylesheet nothing matches
   is how this file accumulated four copies of the creature row grid; it is in
   the history if a page ever wants a checklist. */

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

.table { width: 100%; border-collapse: collapse; font-size: 0.94rem; }
.table th {
  text-align: left; font-weight: 400; font-size: 0.7rem;
  text-transform: uppercase; letter-spacing: 0.11em; color: var(--gold-dim);
  padding: 0 12px 8px 0; border-bottom: 1px solid var(--line);
}
.table td { padding: 11px 12px 11px 0; border-bottom: 1px solid var(--ground-2); vertical-align: top; }
.table tbody tr:hover { background: rgba(255, 255, 255, 0.018); }
.side-Alliance strong, .side-Alliance { color: var(--alliance); }
.side-Horde strong, .side-Horde { color: var(--horde); }

.empty { color: var(--ink-faint); font-style: italic; }

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

.form { display: flex; flex-direction: column; gap: 16px; }
.form h3 { margin: 0; }
.field { display: flex; flex-direction: column; gap: 5px; }
.field-l { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.09em; color: var(--gold-dim); }
.field-h { font-size: 0.84rem; color: var(--ink-faint); line-height: 1.5; }
.fieldrow { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }

/* type="search" WAS MISSING FROM THIS LIST, which is why /icons and /objects
   each grew their own copy of these five declarations: an unstyled search box
   renders as a white pill on a near-black page and looks like a stylesheet that
   failed to load. Adding it here is the root fix; both of those pages set their
   own background and border further down the file and still win on load order,
   so nothing about them changes. */
input[type="text"], input[type="password"], input[type="email"], input[type="number"],
input[type="search"], input:not([type]), select, textarea {
  background: var(--ground); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 9px 11px; font: inherit; font-size: 0.95rem; width: 100%;
}
textarea { resize: vertical; font-family: var(--serif); line-height: 1.6; }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--gold-dim);
  box-shadow: 0 0 0 2px rgba(201, 162, 83, 0.16);
}

/* The banner upload on the admin page. A file input is NOT in the selector
   list above and must not be: `width: 100%` on one stretches the native button
   across the panel. It is styled here instead, and only the ::file-selector
   part is made to match .btn - the rest of the control is the browser's, which
   is the right call for the one widget on the site that opens an OS dialog. */
input[type="file"] {
  font: inherit; font-size: 0.9rem; color: var(--ink-dim);
  padding: 8px 0; max-width: 100%;
}
input[type="file"]::file-selector-button {
  font: inherit; font-size: 0.88rem;
  margin-right: 12px; padding: 7px 14px; cursor: pointer;
  background: var(--panel-2); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
}
input[type="file"]::file-selector-button:hover {
  border-color: var(--gold-dim); color: var(--gold-lit);
}

/* The two banner forms sit inside a .form panel, which is a flex COLUMN with a
   16px gap - a bare <form> child of it would collapse its own contents onto one
   line. Restating the column keeps the file field, its help text and the button
   stacked the way every other field on the page is. */
.banner-form { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }

/* Shown at a size worth judging, since the point of the preview is deciding
   whether the picture works. object-fit crops rather than squashes, and the
   ratio is roughly the strip the homepage actually shows. */
.banner-preview {
  display: block; width: 100%; max-height: 220px;
  object-fit: cover; object-position: center 58%;
  border: 1px solid var(--line);
}

.check { display: flex; align-items: flex-start; gap: 9px; font-size: 0.94rem; }
.check input { width: auto; margin-top: 3px; }
.check-inline { align-items: center; white-space: nowrap; }

.formerror {
  margin: 0; padding: 10px 14px; border-radius: var(--radius);
  background: rgba(192, 85, 63, 0.12); border-left: 3px solid var(--bad);
}
.warnbox {
  padding: 13px 16px; border-radius: var(--radius);
  background: var(--ground-2); border-left: 3px solid var(--gold-dim);
  font-size: 0.9rem; color: var(--ink-dim);
}
.warnbox strong { color: var(--ink); }
.warnbox-bad { border-left-color: var(--bad); }

.searchbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.searchbar input[name="q"] { max-width: 260px; }
.stackform { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }

/* ------------------------------------------------------------------ buttons */

.btn {
  display: inline-block; cursor: pointer;
  background: var(--gold); color: #221a0c;
  border: 1px solid var(--gold); border-radius: var(--radius);
  padding: 9px 18px; font: inherit; font-size: 0.94rem; font-weight: 600;
  text-decoration: none; text-align: center;
}
.btn:hover { background: var(--gold-lit); border-color: var(--gold-lit); color: #221a0c; }
.btn-lg { padding: 12px 26px; font-size: 1rem; }
.btn-sm { padding: 5px 11px; font-size: 0.82rem; font-weight: 400; }
.btn-quiet {
  background: transparent; color: var(--ink-dim); border-color: var(--line);
  font-weight: 400;
}
.btn-quiet:hover { background: var(--panel-2); color: var(--ink); border-color: var(--gold-dim); }
.btn-bad { background: transparent; color: var(--bad); border-color: var(--bad); font-weight: 400; }
.btn-bad:hover { background: var(--bad); color: #fff; border-color: var(--bad); }

.actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* ------------------------------------------------------------------- admin */

.subnav {
  display: flex; gap: 16px; flex-wrap: wrap;
  padding: 14px 0 0; margin-bottom: -14px;
}
.subnav a {
  color: var(--ink-faint); text-decoration: none; font-size: 0.88rem;
  padding: 4px 0; border-bottom: 2px solid transparent;
}
.subnav a:hover { color: var(--ink); }
.subnav a.on { color: var(--gold); border-bottom-color: var(--gold); }

.rowmenu summary {
  cursor: pointer; color: var(--ink-dim); font-size: 0.86rem;
  list-style: none; user-select: none;
}
.rowmenu summary::-webkit-details-marker { display: none; }
.rowmenu summary::before { content: "\25B8  "; color: var(--gold-dim); }
.rowmenu[open] summary::before { content: "\25BE  "; }
.rowmenu summary:hover { color: var(--gold); }
.rowmenu-body {
  margin-top: 10px; padding: 12px; min-width: 230px;
  background: var(--ground-2); border: 1px solid var(--line); border-radius: var(--radius);
}
.table-accounts td { vertical-align: top; }

/* ------------------------------------------------------------------ footer */

.foot {
  border-top: 1px solid transparent;
  border-image: linear-gradient(90deg, transparent, var(--gold-dim) 20%, var(--gold-dim) 80%, transparent) 1;
  margin-top: 40px; padding: 26px 0;
}
.foot-in { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.foot p { margin: 0; font-size: 0.86rem; color: var(--ink-faint); }
.foot-links { display: flex; gap: 10px; flex-wrap: wrap; }

/* ----------------------------------------------------------------- narrow */

@media (max-width: 720px) {
  /* Half-width frame on a narrow screen -- 32px of border each side takes a
     fifth of a phone. Same sprite and slice, drawn smaller; this is what
     .wow-frame--sm does, applied here by width instead of by class. */
  .sheet {
    margin: 8px;
    border-width: 16px;
    border-image-width: 16px;
  }
  .topbar-in { min-height: 0; padding-top: 12px; padding-bottom: 12px; }
  .mainnav { order: 3; width: 100%; margin-right: 0; gap: 14px; }
  .userbar { margin-left: auto; }
  .hero { padding: 44px 0 30px; }
  /* .hero-art sets its own roomier padding and beats .hero on specificity, so
     it has to be brought down here by name or the banner keeps 92px of desktop
     air on a phone. Its picture is cover-cropped hard at this width, so most of
     that air was buying nothing anyway.

     THE BLEED DOES NOT SHRINK HERE, though the rule above it says the frame
     does. `.sheet { border-width: 16px }` in this block never takes effect:
     frames.css loads after this file, and its `.wow-frame--gold` sets
     border-width AND border-image-width to 32px at equal specificity, so the
     later sheet wins at every width. Measured at 600px wide -- sheet margin 8,
     gold drawn at x=13..18, content box at x=40 -- which is a 32px band, the
     same as desktop. So 21px stays right, and a bleed of 10 left 11px of the
     dead band showing.

     The side padding still has to be restated: `padding: 56px 0 40px` would zero
     it while margin-inline stayed negative, dragging the words 21px off centre. */
  .hero-art { padding: 56px var(--hero-bleed) 40px; }
  .realmbox { width: 100%; }
  .realmbox code { word-break: break-all; }
}

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

/* ------------------------------------------------------------------- icons */

/* The icon browser at /icons. A reference tool rather than a page anybody
   reads, so it is denser than the rest of the site -- but it keeps the same
   vocabulary: one typeface, gold gradients instead of hairlines, no radius. */

.icon-tool {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: 34px;
  align-items: start;
}

.icon-search {
  display: flex; align-items: baseline; gap: 16px;
  margin-bottom: 8px;
}
.icon-search input {
  flex: 1 1 auto;
  font: 1rem var(--serif);
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 14px;
}
.icon-search input:focus {
  outline: none;
  border-color: var(--gold-dim);
  box-shadow: 0 0 0 1px var(--gold-dim);
}
/* HOW MANY RESULTS, on both reference pages. One rule because it is one thing:
   a line under the search bar, over the grid it counts. min-height so the grid
   does not jump between an empty count and a filled one -- there is a moment
   before the first search lands where it says nothing.
   Colour comes from the `dim` class in the markup, not from here. */
.icon-count, .obj-count { font-size: 0.9rem; margin: 0 0 14px; min-height: 1.3em; }

/* auto-fill, not auto-fit: a search with three results should leave the cells
   at their real size on the left, not stretch three icons across the page. */
.icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, 64px);
  gap: 6px;
  align-content: start;
}
.icon-cell {
  width: 64px; height: 64px;
  padding: 0; margin: 0;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--panel);
  cursor: pointer;
  line-height: 0;
}
.icon-cell img { width: 100%; height: 100%; display: block; }
.icon-cell:hover { border-color: var(--gold-dim); }
/* The selected cell must read as selected while the mouse is elsewhere, so it
   gets its own treatment rather than borrowing the hover one. */
.icon-cell.is-on {
  border-color: var(--gold-lit);
  box-shadow: 0 0 0 2px var(--gold-dim);
}
.icon-cell:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: 2px; }

.icon-more { margin: 16px 0 0; font-size: 0.9rem; }
.icon-empty { margin: 0; }

.icon-detail {
  position: sticky; top: 20px;
  background: var(--panel);
  border-top: 2px solid var(--gold-dim);
  padding: 20px;
  min-height: 120px;
}
.icon-detail-head { display: flex; gap: 14px; align-items: center; margin-bottom: 18px; }
.icon-detail-head h2 { font-weight: 400; font-size: 1.15rem; margin: 0; word-break: break-all; }
.icon-detail-head p { margin: 2px 0 0; font-size: 0.85rem; }
.icon-detail-img {
  width: 64px; height: 64px; flex: 0 0 auto;
  border: 1px solid var(--line);
}

/* A label, not a heading: it names the field below it and should not compete
   with the icon's own name for weight. */
.icon-field-label, .obj-field-label {
  margin: 18px 0 6px;
  font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-faint);
}
.icon-path, .icon-ids, .obj-path { margin: 0; }
/* break-all, because these are paths and ids with no spaces to break at, and a
   column this narrow has to break them somewhere. Smaller than body text so a
   64-character path takes two lines rather than three. */
.icon-path code, .icon-ids code, .obj-path code {
  display: block; word-break: break-all;
  font-size: 0.82rem; line-height: 1.5;
}

/* Room for the scrollbar on the right, or it sits on top of the entry ids and
   eats a digit -- which on a page whose whole job is copying exact values is
   worse than it sounds. */
.icon-items, .obj-items {
  list-style: none; margin: 0; padding: 0 10px 0 0;
  max-height: 320px; overflow-y: auto;
}
.icon-items li, .obj-items li {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 3px 0;
  font-size: 0.9rem;
}
.icon-item-name, .obj-item-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.icon-item-id, .obj-item-id { color: var(--ink-faint); font-family: var(--mono); font-size: 0.8rem; flex: 0 0 auto; }

.small { font-size: 0.82rem; }

/* Copyable things say so by responding. The click handler is on the panel, so
   this covers the path and the display ids alike. */
code.cmd { cursor: pointer; }
code.cmd.copied { color: var(--good); }

@media (max-width: 820px) {
  .icon-tool { grid-template-columns: minmax(0, 1fr); }
  .icon-detail { position: static; }
}

/* Both an observer target and a real button: scrolling appends automatically,
   and clicking does the same where IntersectionObserver is unavailable. */
.icon-sentinel {
  display: block; margin: 18px auto 0;
  font: 0.92rem var(--serif); color: var(--ink-dim);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 8px 18px; cursor: pointer;
}
.icon-sentinel:hover { color: var(--ink); border-color: var(--gold-dim); }

/* ------------------------------------------------------------ object browser

   The icon browser's shape at a larger scale: a grid on the left, a panel on
   the right. The panel is a SELECTION rather than a detail view, because the
   thing you want from this page is a list of ids to paste at a generator, and
   collecting six of them means six different searches. */

.obj-tool {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 34px;
  align-items: start;
}

.obj-bar {
  display: flex; align-items: center; gap: 14px;
  margin-bottom: 8px;
}
/* input[type=search] is not covered by the bare element selectors above, which
   is why the icon browser styles its own too. */
.obj-bar input[type="search"] {
  /* A REAL FLEX BASIS, not auto. The global rule below sets width:100% on every
     input and select, and with `flex: 1 1 auto` both items resolve their basis
     from that -- so the select claimed the whole bar and the search box
     collapsed to a 30px square. Both need an explicit basis here. */
  flex: 1 1 240px; min-width: 0;
  font: 1rem var(--serif);
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 14px;
}
.obj-bar input[type="search"]:focus {
  outline: none;
  border-color: var(--gold-dim);
  box-shadow: 0 0 0 1px var(--gold-dim);
}
/* width:auto undoes the global width:100%; without it `flex: 0 0 auto` sizes
   the select to the whole row. */
.obj-bar select { flex: 0 0 auto; width: auto; min-width: 190px; }
/* .obj-count is styled with .icon-count, up in the icon browser's block --
   the same line doing the same job on the sibling page. */

/* auto-fill, NOT auto-fit: a three-result search should leave three cards on the
   left in tracks the same size as a full row's, not stretch three models across
   the page. minmax rather than a fixed 128 so the tracks divide the column
   exactly -- fixed tracks left ~100px of slack at the right, and the search bar
   above ran on past the last card, which is what read as "not lining up". The
   picture stays 112 whatever the track does, so nothing is scaled up. */
.obj-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: 10px;
  align-content: start;
}

.obj-card {
  position: relative;
  padding: 6px 4px 8px;
  margin: 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  background: var(--panel);
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--ink);
  font: 0.82rem/1.25 var(--serif);
  text-align: center;
  cursor: pointer;
}
.obj-card:hover { border-color: var(--gold-dim); }
/* Picked must read as picked while the mouse is elsewhere, so it gets its own
   treatment rather than borrowing the hover one. */
.obj-card.on {
  border-color: var(--gold-lit);
  box-shadow: 0 0 0 2px var(--gold-dim);
}
.obj-card:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: 2px; }

.obj-img { width: 112px; height: 112px; display: block; object-fit: contain; }
/* 1,633 models have no render. Keep the space so the grid stays aligned and
   the name still sits where the eye expects it. */
.obj-img.missing { visibility: hidden; }

/* TWO LINES, at a fixed height so the rows stay aligned. One line ellipsised
   most of these away -- "Arcane Force Shie...", "Armor Breastplate..." -- and
   the trailing number is usually the only thing telling two models apart. */
.obj-name {
  display: block; width: 100%;
  height: 2.5em; line-height: 1.25;
  overflow: hidden;
}
.obj-id { color: var(--ink-faint); font: 0.78rem var(--mono); }
/* Tighter than a normal .tag. "AVAILABLE" is nine characters where the site's
   other tags are one short word, and at the shared letter-spacing it spanned
   the whole card and read as a banner across the model rather than a mark on
   it. Only the spacing and padding give; the colour and border stay, so it is
   still recognisably the same tag as everywhere else. */
.obj-sold {
  position: absolute; top: 4px; right: 4px;
  font-size: 0.58rem; letter-spacing: 0.08em; padding: 2px 5px 1px;
}

.obj-more { margin: 16px 0 0; font-size: 0.9rem; }
.obj-sentinel { display: block; margin: 18px auto 0; }

/* The same treatment .icon-detail has, and for the same reason: without a
   ground of its own the panel reads as text loose on the page rather than as a
   column beside the grid. */
.obj-detail {
  position: sticky; top: 20px;
  background: var(--panel);
  border-top: 2px solid var(--gold-dim);
  padding: 20px;
  min-height: 120px;
  /* Some models are spawned in twenty maps. Cap it and scroll rather than
     letting the panel run past the fold, which on a sticky column means the
     bottom of it can never be reached. */
  max-height: calc(100vh - 60px);
  overflow-y: auto;
}
.obj-detail-head { display: flex; gap: 14px; align-items: center; margin-bottom: 4px; }
.obj-detail-head h2 { font-weight: 400; font-size: 1.1rem; margin: 0; }
.obj-detail-head p { margin: 2px 0 0; font-size: 0.85rem; }
.obj-detail-img {
  width: 72px; height: 72px; flex: 0 0 auto;
  border: 1px solid var(--line);
  object-fit: contain;
}
.obj-avail { margin: 14px 0 0; font-size: 0.88rem; color: var(--good); }
.obj-avail code { font-size: 0.85rem; }
.obj-empty { margin: 0; }

@media (max-width: 820px) {
  .obj-tool { grid-template-columns: minmax(0, 1fr); }
  .obj-picked { position: static; }
}

/* ------------------------------------------------------------ bug tracker */

/* The tracker at /bugs. A list of reports is the one thing on this site that
   is genuinely a LIST - not cards, not a table - so it gets the treatment the
   cards already use: no boxes, a rule between items, and the status carried in
   a short coloured edge rather than in a filled block. The status word is
   still there in the tag; the colour is a second channel, not the only one. */

.bug-topline { align-items: flex-start; }
.bug-counts { font-size: 1.05rem; }
.bug-counts strong { color: var(--gold); font-size: 1.3rem; }

/* -------------------------------------------------------------- filtering */

/* Squared like .tag and sharing its scale, because a tag you can click and a
   tag you cannot should not look like two different things. */
.tagstrip { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 14px; }
.tagpick {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 4px 11px 3px;
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.13em;
  border: 1px solid var(--line); color: var(--ink-dim);
  text-decoration: none; white-space: nowrap;
}
.tagpick:hover { border-color: var(--gold-dim); color: var(--ink); }
.tagpick.on { border-color: var(--gold); color: var(--gold); }
.tagpick-n { color: var(--ink-faint); letter-spacing: 0; font-size: 0.78rem; }
.tagpick.on .tagpick-n { color: var(--gold-dim); }

.bug-search { margin-top: 6px; }
.bug-kind {
  color: var(--ink-faint); text-decoration: none; font-size: 0.84rem;
  border-bottom: 1px solid transparent; padding-bottom: 1px;
}
.bug-kind:hover { color: var(--ink); }
.bug-kind.on { color: var(--gold); border-bottom-color: var(--gold-dim); }

/* ------------------------------------------------------------- the status */

/* Four words, four colours. Open is gold because it is the default state and
   gold is this site's "here", not its "warning"; confirmed is red because it
   means somebody reproduced it and it is really broken. */
.tag-open { border-color: var(--gold); color: var(--gold); }
.tag-confirmed { border-color: var(--bad); color: var(--bad); }
.tag-fixed { border-color: var(--good); color: var(--good); }
.tag-declined { border-color: var(--line); color: var(--ink-faint); }
.tag-untagged { border-style: dashed; color: var(--ink-faint); }

/* ---------------------------------------------------------------- the list */

.buglist { list-style: none; margin: 0 0 22px; padding: 0; }
.bugrow {
  display: flex; gap: 18px; align-items: flex-start; justify-content: space-between;
  padding: 16px 0 15px 15px;
  border-bottom: 1px solid var(--ground-2);
  border-left: 2px solid var(--line);
}
.bugrow:first-child { border-top: 1px solid var(--ground-2); }
.bugrow:hover { background: rgba(255, 255, 255, 0.018); }
.bugrow-open { border-left-color: var(--gold-dim); }
.bugrow-confirmed { border-left-color: var(--bad); }
.bugrow-fixed { border-left-color: var(--good); }
.bugrow-declined { border-left-color: var(--line); }

.bugrow-main { min-width: 0; }
.bugrow-h { font-weight: 400; font-size: 1.1rem; margin: 0; line-height: 1.4; }
.bugrow-h a { color: var(--ink); text-decoration: none; }
.bugrow-h a:hover { color: var(--gold-lit); }
/* Monospace, because it is a number you quote at somebody. */
.bugrow-id { font-family: var(--mono); font-size: 0.82rem; color: var(--ink-faint); margin-right: 6px; }
.bugrow-meta { display: flex; flex-wrap: wrap; gap: 6px; margin: 9px 0 6px; }
.bugrow-by { font-size: 0.86rem; margin: 0; }
.bugrow-game { color: var(--gold-dim); }
.bugrow-side { flex: 0 0 auto; text-align: right; }
.bugrow-comments {
  color: var(--ink-dim); text-decoration: none; font-size: 0.86rem; white-space: nowrap;
}
.bugrow-comments:hover { color: var(--gold); }

/* --------------------------------------------------------------- one report */

.bug-title { line-height: 1.25; }
.bug-title .bugrow-id { font-size: 1rem; vertical-align: 0.15em; }
.bug-tagline { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 14px; }
.bug-by { font-size: 0.95rem; }
.bug-body { font-size: 1.02rem; }
.bug-editlink { margin: 16px 0 0; font-size: 0.86rem; }

/* Three forms stacked in one disclosure. They are separate <form>s because
   they post to separate routes, so a rule between them is what says so. */
.bug-controls { display: flex; flex-direction: column; gap: 20px; min-width: 300px; max-width: 640px; }
.bug-controls > form + form { border-top: 1px solid var(--line); padding-top: 18px; }
.tagchecks { display: flex; flex-wrap: wrap; gap: 8px 20px; }

/* ----------------------------------------------------------- the timeline */

/* Comments and status changes in one column. A note is prose with a name over
   it; an event is one dim line. The difference in weight IS the information -
   you can skim past six retaggings to find the person who explained it. */
.timeline { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; }
.tl { padding: 0 0 0 18px; border-left: 2px solid var(--line); }
.tl-note { padding-top: 4px; padding-bottom: 20px; border-left-color: var(--gold-dim); }
.tl-note:last-child { padding-bottom: 4px; }
.tl-head { margin: 0 0 6px; font-size: 0.9rem; }
.tl-head strong { color: var(--ink); font-weight: 400; }
.tl-del { color: var(--ink-faint); font-size: 0.78rem; margin-left: 8px; }
.tl-del:hover { color: var(--bad); }
.tl-event { padding-top: 6px; padding-bottom: 14px; font-size: 0.86rem; color: var(--ink-dim); }
.tl-event p { margin: 0; }

/* ------------------------------------------------------------- the form */

.bug-newcols { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); }
.bug-tips { margin: 0; padding-left: 20px; color: var(--ink-dim); font-size: 0.92rem; }
.bug-tips li { margin-bottom: 9px; }
.bug-tips li:last-child { margin-bottom: 0; }
.bug-tips strong { color: var(--ink); }

@media (max-width: 860px) {
  .bug-newcols { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
  .bugrow { flex-direction: column; gap: 8px; }
  .bugrow-side { text-align: left; }
}

/* =====================================================================
   The database (/database, /database/item/:entry)
   ===================================================================== */

/* ITEM QUALITY. The one place this site uses colour to carry meaning rather
   than tone, because in WoW quality colour IS the item's headline and a player
   reads it before the name. These are NOT the client's values: #1eff00 for
   uncommon is a pure green that on parchment-over-charcoal reads as a browser
   default showing through the page -- the same failure an untreated screenshot
   has behind the homepage banner. Each hue is pulled towards the palette until
   it belongs here, keeping the instant recognition and losing the glare.
   Common is deliberately just --ink: "common" should look like ordinary text. */
.q0 { color: #8c8577; }
.q1 { color: var(--ink); }
.q2 { color: #86b45c; }
.q3 { color: #6f9ed6; }
.q4 { color: #a97ed8; }
.q5 { color: #e08a3c; }
.q6 { color: #e6cc80; }

/* ------------------------------------------------------------- filters */

.db-filters { margin: 0 0 20px; }

/* The search bar's own band, above the tabs. Its own rule rather than `.band`
   so the gap under it is tighter than a section break -- the tabs belong to it. */
.db-searchbar { padding-top: 26px; padding-bottom: 14px; }

/* ------------------------------------------------- the filter expando */

/* SHUT BY DEFAULT, and a <details> so it needs no script and no inline style.
   A LINK, NOT A BOXED BUTTON. It sits directly under the tab strip, which is
   itself a row of text links, and a bordered control there read as a second,
   competing piece of navigation -- the box was the loudest thing on a page whose
   whole point was to get quieter. As a link it takes the site's own anchor
   styling and reads as what it is: a small thing you can click to see more. */
.db-filterbox { margin-bottom: 20px; }
.db-filtertoggle {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  color: var(--gold);
  font-size: 0.88rem;
  text-decoration: underline;
  text-decoration-color: var(--gold-dim);
  text-underline-offset: 2px;
  /* Both spellings: the standard one and the -webkit- pseudo-element older
     builds still use. Left in, the browser draws its own triangle beside ours. */
  list-style: none;
  width: fit-content;
}
.db-filtertoggle::-webkit-details-marker { display: none; }
.db-filtertoggle::after {
  content: "\25BE";
  font-size: 0.7em;
  color: var(--gold-dim);
  /* Not underlined with the words: a chevron with a rule under it reads as a
     glyph that lost its font. */
  text-decoration: none;
}
.db-filterbox[open] .db-filtertoggle::after { content: "\25B4"; }
.db-filtertoggle:hover { color: var(--gold-lit); }
.db-filtertoggle:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: 3px; }

/* How many the panel is holding, so a closed panel still says it is doing
   something. Counts the panel's own filters only -- not the search box above the
   tabs, and not the sort, which is the column headings.

   Styled as `.db-tab-n`, the count beside a tab name, because it is the same
   kind of thing one line further up: a quiet number attached to a link. Now the
   label itself is gold, a gold number beside it would not read as subordinate. */
.db-filtern {
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--ink-faint);
  text-decoration: none;
}

.db-filterbox[open] .db-selects { padding-top: 16px; }
.db-filterclear { margin: 12px 0 0; font-size: 0.86rem; }

/* RELATIVE, because the dropdown is absolute against it. Positioning that panel
   from JavaScript is the obvious way and it is the wrong one here: the site's
   CSP is style-src 'self', so the style="" attribute it would need is dropped
   without a word. Anchoring it in the stylesheet needs no script at all. */
.db-search { display: flex; gap: 10px; align-items: stretch; position: relative; }
.db-search input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;                 /* or the flex basis wins and it never shrinks */
  font-family: var(--serif);
  font-size: 1.02rem;
  padding: 10px 13px;
}
.db-search .btn { flex: 0 0 auto; }

/* ---------------------------------------------------- the search dropdown */

/* Sits under the input, over everything else. Above the hover card's z-index 40
   so a tooltip drawn from the list below cannot cover the thing you are typing
   into. Capped in height and scrollable: ten rows plus three group headings is
   more than a short viewport has under the box. */
.db-suggest {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 60;
  margin-top: 4px;
  max-height: 60vh;
  overflow-y: auto;
  background: #0f0c08;
  border: 1px solid var(--gold-dim);
  padding: 6px 0;
}

.db-sg-group {
  margin: 0;
  padding: 8px 14px 4px;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-faint);
}
.db-sg-n { font-family: var(--mono); margin-left: 6px; }

/* The row: picture, name, and what kind of thing it is, out to the right. */
.db-sg-row {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) minmax(0, 14em);
  gap: 12px;
  align-items: center;
  padding: 5px 14px;
  text-decoration: none;
  color: inherit;
}
.db-sg-row:hover,
.db-sg-row.on { background: rgba(255, 255, 255, 0.05); }
/* The arrow-key selection has to be visible without a pointer, so it gets a
   marker of its own rather than borrowing the hover shade alone. */
.db-sg-row.on { box-shadow: inset 2px 0 0 var(--gold); }

.db-sg-ico { width: 28px; height: 28px; display: block; }
.db-sg-ico img { display: block; width: 100%; height: 100%; object-fit: contain; }
.db-sg-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.db-sg-type {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
  color: var(--ink-faint);
  font-size: 0.82rem;
}
.db-sg-none { margin: 0; padding: 10px 14px; color: var(--ink-faint); font-style: italic; }

@media (max-width: 620px) {
  /* The type column is the first thing to go: the name is what you are reading. */
  .db-sg-row { grid-template-columns: 28px minmax(0, 1fr); }
  .db-sg-type { display: none; }
}

.db-selects {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 18px;
  align-items: flex-end;
  margin-top: 14px;
}
.db-sel { display: flex; flex-direction: column; gap: 4px; }
.db-sel > span {
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--gold-dim);
}
/* Selects are width:100% globally, which inside a flex row collapses them to
   nothing -- the same trap the object browser's search box hit. */
.db-sel select { width: auto; min-width: 132px; padding: 7px 10px; font-size: 0.9rem; }
/* The quest Where dropdown carries 146 zones and 34 categories, and the names
   in it are the long kind -- "Eastern Plaguelands (133)". At the shared minimum
   the closed control truncates most of them. */
.db-sel-wide select { min-width: 210px; }
.db-range { display: flex; align-items: center; gap: 6px; }
.db-range input { width: 62px; padding: 7px 8px; font-size: 0.9rem; text-align: center; }
.db-dash { color: var(--ink-faint); }

/* `.db-check` styled the one checkbox in a filter row, "Only ours". That filter
   came out 2026-09-09 and no template wears the class any more, so the rules
   went too rather than waiting around for a checkbox that may never come. */

/* THE FILTER TOGGLE AND THE COUNT SHARE ONE LINE, at the two ends of the table
   they describe: what you can narrow it by on the left, what the narrowing left
   you with on the right. They were two stacked blocks and the space between the
   tab strip and the table was mostly air.

   THE SECTION IS THE GRID, and the two sit in the same cell row ON PURPOSE.
   The obvious shape -- a flex row of [form][count] -- puts the filter panel in
   a column ~210px narrower than the page, and the quest panel already wraps to
   two lines at the full width. So the form keeps the whole width (column
   1/-1) and the count is placed over the right of that same row. They cannot
   collide: the form's first row is the one-line summary at the far left, and
   the panel opens underneath it. Baseline on both, so the count sits on the
   toggle's line rather than near it.

   Everything else in the section -- the list, the empty message, the pager --
   spans both columns and auto-flows underneath in document order. */
.db-listgrid { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
.db-listgrid > * { grid-column: 1 / -1; }
.db-listgrid > .db-filters { grid-column: 1 / -1; grid-row: 1; align-self: baseline; }
.db-listgrid > .db-count { grid-column: 2; grid-row: 1; align-self: baseline; }

/* ONE GAP UNDER THE BAR, NOT THREE STACKED. The form and the <details> inside
   it each carried 20px below, which was right when the toggle was a block of
   its own with the count underneath it and wrong the moment both became one
   line: 40px of margin hanging off a 24px row, and the table looked adrift
   from the thing that filters it. The row's height is the form's, so the form
   is the one that sets the gap and the count contributes nothing.
   Open, the panel needs a little more air under its last control. */
.db-listgrid > .db-filters { margin-bottom: 0; }
.db-listgrid > .db-filters .db-filterbox { margin-bottom: 12px; }
.db-listgrid > .db-filters .db-filterbox[open] { margin-bottom: 20px; }
.db-listgrid > .db-count { margin-bottom: 0; }

/* Right, so it sits over the end of the table rather than competing with the
   first column heading directly under it. The padding is .db-row's own right
   padding plus the table's 1px border, so it ends exactly where the last
   column does rather than 11px past it. */
.db-count {
  font-size: 0.9rem;
  margin: 0 0 12px;
  min-height: 1.3em;
  text-align: right;
  padding-right: 11px;
}

/* ------------------------------------------------------------- the list */

/* COLUMN LABELS, because a bare number in a box is a riddle. The level badge
   was asked about directly -- "what is the rectangle with a number inside it?"
   -- and the answer was only ever available by inferring it. The item list gets
   away without labels because its columns say what they are ("ilvl 92") or
   carry meaning in colour; the creature and quest lists have a naked figure in
   the leftmost column and two kinds of tag on the right.

   IT IS THE FIRST <li> OF THE LIST, not a separate block above it, so it
   inherits whichever `.db-row` grid the list defines and cannot drift out of
   step with the rows the way a copied grid would. public/database.js therefore
   clears the rows around it rather than emptying the list. */
/* THE SMALL TYPE GOES ON THE CELLS, NOT ON THE ROW, and that is not a style
   preference. Every .db-row is its own grid container, and the column tracks
   are written in `em` -- so shrinking the font on the header row shrinks its
   columns with it. Set on the row, 4.6em resolved to 51px here against 74px in
   the rows below, and the labels sat left of the data they name. */
.db-listhead .db-row { padding-bottom: 8px; }
.db-listhead .db-row > * {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-faint);
}
.db-listhead .db-row:hover { background: none; }
/* `.db-lvl-h` centred the Level heading over the boxed badge under it. The
   badge is plain text in a left-aligned column now and the heading needs no
   class of its own, so the rule went rather than sitting here matching
   nothing. */

/* A SORTABLE HEADING HAS TO LOOK CLICKABLE WITHOUT SHOUTING. It inherits the
   label's dim colour rather than the site's gold link colour -- a row of gold
   headings reads as navigation out of the page, which is the opposite of what
   these do -- and brightens on hover, which is the affordance. The one in force
   is the bright one and carries the arrow. */
.db-sort { color: inherit; text-decoration: none; }
.db-sort:hover { color: var(--gold); }
.db-sort.on { color: var(--ink); }
.db-sortdir { margin-left: 4px; font-family: var(--mono); }

/* IT IS A TABLE, SO IT IS DRAWN AS ONE. The rules between rows were
   --ground-2 (#1c1711) laid on a --sheet of #1a150f -- two points apart, which
   is to say invisible. A row here is wide, its three or four columns are small
   type with a lot of air between them, and there was nothing to follow across
   from a name on the left to a level on the right. --line is the border colour
   the rest of the site puts round panels and it is the one that shows.

   The box round the whole thing, the filled header strip and the alternating
   row tint are the other three cues a table gets for free and a list of <li>
   does not. Kept light on purpose: this is a reference page you read for a
   while, and a grid drawn in full-contrast rules fights the text in it. */
.db-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--line);
}
/* On the separator rather than a bottom border per row, so the last row does
   not draw a line on top of the box's own. */
.db-list li + li { border-top: 1px solid var(--line); }
.db-list li:nth-child(even) { background: rgba(255, 255, 255, 0.017); }
.db-listhead { background: var(--panel); }

.db-row {
  display: grid;
  grid-template-columns: 36px minmax(0, 1fr) minmax(0, 15em) 6.5em;
  align-items: center;
  gap: 14px;
  padding: 7px 10px 7px 6px;
  text-decoration: none;
  color: inherit;
}
.db-row:hover { background: rgba(255, 255, 255, 0.025); }
.db-row:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: -2px; }

.db-ico {
  width: 36px; height: 36px;
  display: block;
  border: 1px solid var(--line);
  background: var(--panel);
}
.db-ico img { display: block; width: 100%; height: 100%; }

.db-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.db-row:hover .db-name { text-decoration: underline; text-underline-offset: 3px; }

/* Items in the 100000-109999 block are this server's own. A quiet tag rather
   than another colour -- quality already owns colour in this list. */
.db-ours {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 5px;
  border: 1px solid var(--gold-dim);
  color: var(--gold);
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  vertical-align: 2px;
}

.db-kind, .db-lvl {
  color: var(--ink-faint);
  font-size: 0.84rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.db-lvl { text-align: right; }
/* `.db-entry` was the entry-id column. It came off the listings on 2026-09-09
   -- the id lives on the thing's own page -- but the set list on an item page
   kept emitting the cell, and with no fifth grid track it wrapped underneath
   each row as a stray "168...". Invisible until the table got its borders. */
.db-lvl abbr { text-decoration: none; border: 0; }

.db-pager { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 24px; }
.db-page {
  display: inline-block;
  padding: 5px 11px;
  border: 1px solid var(--line);
  color: var(--ink-dim);
  text-decoration: none;
  font-size: 0.86rem;
}
.db-page:hover { border-color: var(--gold-dim); color: var(--ink); }
.db-page.on { border-color: var(--gold); color: var(--gold); }
.db-gap { color: var(--ink-faint); padding: 0 2px; }

.db-foot { font-size: 0.86rem; margin: 0; }

/* ------------------------------------------------------- the item page */

.db-crumb { padding-top: 34px; }
.db-back { font-size: 0.88rem; color: var(--ink-dim); text-decoration: none; }
.db-back:hover { color: var(--gold); }

.db-head { display: flex; gap: 18px; align-items: center; padding: 16px 20px 4px; }
.db-head-ico {
  flex: 0 0 auto;
  width: 64px; height: 64px;
  border: 1px solid var(--gold-dim);
  background: var(--panel);
}
.db-head-ico img { display: block; width: 100%; height: 100%; }
.db-head-text { min-width: 0; }
.db-head h1 {
  font-weight: 400;
  font-size: clamp(1.7rem, 4vw, 2.4rem);
  line-height: 1.15;
  margin: 0;
}
.db-head-meta { margin: 6px 0 0; font-size: 0.9rem; color: var(--ink-dim); }
/* THE RANK RIDES THE NAME. It qualifies what the creature is called -- the
   client puts it on the portrait frame for the same reason -- so it sits beside
   the h1 rather than first on a dotted line of unrelated facts underneath.
   Nudged up because a 0.7rem tag on a 2.4rem baseline sits low. */
.db-head-rank,
.db-head h1 .db-ours { margin-left: 12px; vertical-align: 0.45em; }
.db-head-entry { font-family: var(--mono); font-size: 0.84rem; }

.db-two {
  display: grid;
  gap: 34px;
  grid-template-columns: minmax(0, 22em) minmax(0, 1fr);
  align-items: start;
}
/* THE ITEM RENDER sits at the top of the sources column, above the heading,
   and it takes the WHOLE WIDTH of that column. The panel behind it is the same
   guard the hover card's box has: a mostly-transparent picture on the bare page
   reads as floating.

   A WEAPON IS A LONG THIN THING and the column is wide -- 643px against a
   render 232px across, which left two thirds of the box empty. Measured over
   all 2,668 item renders: the median is 3.0 times as wide as it is tall and the
   upper quarter past 4.6, so width is the dimension there is nothing to lose by
   spending.

   THE HEIGHT CAP IS WHAT STOPS A SHIELD BECOMING A POSTER. Shields and tabards
   come out roughly square, and full width would make one 643px tall -- taller
   than the stat block beside it. At 340px, 77% of the catalogue still reaches
   the full width, and raising the cap to 400 buys another 2%: the curve is flat
   past here because what is left is square, not merely small. A capped picture
   is centred by object-fit rather than stretched, so nothing is ever the wrong
   shape. */
.db-sources .db-model { text-align: left; margin: 0 0 22px; }
/* THE LINK HAS TO BE A BLOCK FOR THE WIDTH BELOW TO MEAN ANYTHING. `.db-model a`
   is an inline-block so the creature page's centred figure shrinks to its
   picture; an inline-block sizes to its content, so `width: 100%` on the image
   inside it resolves to the image's own width and quietly does nothing -- 480px
   of a 643px column, which is the intrinsic size plus the padding and looks
   enough like an answer to be missed. */
.db-sources .db-model a { display: block; }
.db-sources .db-model img {
  margin: 0;
  width: 100%;
  height: auto;
  max-height: 340px;
  object-fit: contain;
  background: var(--panel);
  border: 1px solid var(--line);
  padding: 10px;
  box-sizing: border-box;
}

/* The tooltip block. Tight line spacing on purpose: in the client these lines
   are a single object, and normal paragraph rhythm pulls them apart into a
   list of unrelated facts. */
.db-tip { padding: 20px 22px; }
.db-tip-line { margin: 0 0 5px; font-size: 0.95rem; line-height: 1.45; }
.db-tip-line:last-child { margin-bottom: 0; }
.db-stat { color: var(--good); }
.db-tip-dmg { display: flex; justify-content: space-between; gap: 18px; align-items: flex-start; }
.db-tip-dmg-lines { min-width: 0; }
.db-tip-dmg .db-tip-line { margin-bottom: 5px; }
/* Speed must not wrap: it is two short words and a number, and letting it fold
   pushes the damage column narrow enough to break "16 - 30 Nature Damage". */
.db-tip-dmg .db-tip-right { flex: 0 0 auto; white-space: nowrap; }
.db-tip-right { color: var(--ink-dim); }
.db-tip-set { color: var(--gold); margin-top: 10px; }
/* A SET BONUS IS GREY, NOT GREEN, and flush left with everything else. In the
   client grey is what an inactive bonus looks like -- it turns green only once
   you are wearing enough pieces -- and a database page is always the inactive
   case: nobody reading it is wearing anything. Green would be claiming the
   bonus is live. The indent went with it; every other line in this block starts
   at the same place and one that does not reads as a sub-item of the line
   above rather than as one of a list. */
.db-tip-bonus { color: var(--ink-dim); }
.db-tip-need { color: var(--ink-dim); }
.db-tip-spell { color: #7fa860; margin-top: 8px; }
.db-tip-spell strong { font-weight: 400; }
.db-tip-flavour { color: var(--gold-dim); font-style: italic; margin-top: 10px; }
.db-tip-price { margin-top: 12px; font-size: 0.88rem; }
.coin-g { color: #d4b054; }
.coin-s { color: #c8c8c8; }
.coin-c { color: #c88a5a; }
.db-tip-warn {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  font-size: 0.85rem;
  color: var(--gold);
}

/* ------------------------------------------------- the hover card

   The same lines as the block above, in the shape the client draws them: a
   small dark card that appears beside the pointer. It is DARKER than the panel
   it floats over and outlined in gold, because the one thing it must never do
   is read as part of the page underneath -- a tooltip that blends in is a page
   that has visibly broken.

   position: fixed and coordinates set from JS. Everything else is here, which
   is the site's rule; only the two numbers that change every frame are not. */
.db-tipbox {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 40;                  /* over the page, under the lightbox at 50 */
  width: max-content;
  max-width: 25em;
  padding: 12px 14px;
  background: #0f0c08;
  border: 1px solid var(--gold-dim);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.55);
  /* It follows the pointer, so it must never be under it: a card that can take
     a mouseover steals the one belonging to the row it describes, and the two
     then fight over which of them the pointer is on. */
  pointer-events: none;
}

.db-tip-head {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin-bottom: 8px;
}
/* Fixed size whether or not the icon exists, so a missing picture leaves the
   name where it would have been rather than sliding it left. */
.db-tip-ico {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  background: var(--panel);
  border: 1px solid var(--line);
}
.db-tip-ico img { display: block; width: 40px; height: 40px; }
/* The weapon or tabard render on the hover card. A fixed height, for the
   reason in db-tip.html: the card is measured before the image loads. A weapon
   is wide and short, a tabard tall and narrow; contain fits either into the
   same box, and a plain panel behind it keeps a mostly-transparent picture
   from reading as a hole in the card. */
.db-tip-art {
  height: 140px;
  margin: 0 0 10px;
  background: var(--panel);
  border: 1px solid var(--line);
}
.db-tip-art img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.db-tip-name {
  font-size: 1.02rem;
  line-height: 1.25;
  /* The name is the headline of a tooltip and quality is what it says first,
     so it gets the .q* colour and nothing else competes with it. */
}

/* ------------------------------------------------------------- sources */

.db-sources .section-h { margin-top: 0; }
.db-src { margin-bottom: 26px; }
.db-src:last-child { margin-bottom: 0; }
.db-src-h { font-weight: 400; font-size: 1rem; margin: 0 0 4px; color: var(--ink); }
.db-src-n { color: var(--ink-faint); font-size: 0.82rem; font-family: var(--mono); margin-left: 6px; }
.db-src-note { font-size: 0.84rem; margin: 0 0 8px; }
.db-src-list { list-style: none; margin: 0; padding: 0; }
.db-src-list li {
  display: flex;
  gap: 12px;
  align-items: baseline;
  padding: 4px 0;
  border-bottom: 1px solid var(--ground-2);
  font-size: 0.9rem;
}
.db-src-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.db-src-meta { flex: 0 0 auto; color: var(--ink-dim); font-size: 0.82rem; }
.db-src-id { flex: 0 0 auto; color: var(--ink-faint); font-family: var(--mono); font-size: 0.76rem; }

/* --------------------------------------------------------- the raw row */

.db-raw {
  display: grid;
  grid-template-columns: minmax(0, 14em) minmax(0, 1fr);
  gap: 6px 20px;
  margin: 0;
  font-size: 0.9rem;
}
.db-raw dt { color: var(--gold-dim); font-family: var(--mono); font-size: 0.82rem; }
.db-raw dd { margin: 0; color: var(--ink-dim); }
/* `.db-raw-foot` was a line under each Raw data list saying the page is read
   live from the world DB, with the useful command trailing it. The sentence was
   explaining something the heading already implies; the command became a row of
   the list, where every other click-to-copy value already lives. */

@media (max-width: 900px) {
  .db-two { grid-template-columns: 1fr; }
  .db-row { grid-template-columns: 36px minmax(0, 1fr); }
  /* The type and level columns go first on a narrow screen: the name, the
     picture and the id are what a phone-width row is for. */
  .db-kind, .db-lvl, .db-rankcell { display: none; }
}
@media (max-width: 620px) {
  .db-raw { grid-template-columns: 1fr; gap: 2px 0; }
  .db-raw dd { margin-bottom: 10px; }
  .db-head { flex-wrap: wrap; }
}

.db-more { display: block; margin: 20px auto 0; }
/* `.db-list-ours` used to hide the per-row "ours" tag while the "Only ours"
   filter was on, since a badge every row carries says nothing. The filter went
   2026-09-09 and the rule went with it -- a selector nothing can ever match is
   how this stylesheet accumulated four copies of the creature row grid. */

/* ------------------------------------------------------ the quest list */

/* No picture column: a quest has neither an icon nor a model, so the NAME
   carries the left edge, with the level beside it. The "where" column is wider
   than the item list's Kind because zone names are long -- "Eastern
   Plaguelands", "Stranglethorn Vale" -- and truncating those loses the half
   that identifies them. */
.db-list-quest .db-row { grid-template-columns: minmax(0, 1fr) 4.6em minmax(0, 15em) 11em; }
/* THE KIND COLUMN IS TAGS, NOT "Dungeon · Alliance". Faction is already a
   colour everywhere else on this site -- the armory, a creature's page -- and
   a list you scan for Horde quests answers faster in red than in a word at the
   end of a dotted pair. The type takes the gold tag, the same gold the elite
   rank uses one list over.

   The tags nowrap and the column is 11em, which is what "ALLIANCE" beside
   "WORLD EVENT" measures -- the widest pair, and only 8 quests are World
   Events, but a column that wraps on eight rows out of 6,708 puts a bump in an
   otherwise even table. */
.db-qkind { min-width: 0; display: flex; flex-wrap: wrap; gap: 5px; }
.db-qtag { white-space: nowrap; }
.qtag-alliance { border-color: var(--alliance); color: var(--alliance); }
.qtag-horde { border-color: var(--horde); color: var(--horde); }
.qtag-type { border-color: var(--gold-dim); color: var(--gold); }

/* A CATEGORY IS NOT A PLACE, and the Where column shows both because
   ZoneOrSort does. Dotted rather than a different colour: 1,141 rows carry one
   and a second colour in that column would compete with quality colour
   elsewhere on the site for no gain. */
.db-q-cat { border-bottom: 1px dotted var(--line); }

@media (max-width: 900px) {
  .db-list-quest .db-row { grid-template-columns: minmax(0, 1fr) 4.6em; }
  /* Hidden HERE rather than by adding .db-qkind to the shared .db-kind/.db-lvl
     rule further up: that rule comes before `.db-qkind { display: flex }` in
     the file, so it would lose. This block is after it. Covers the heading
     too, since the header row is an <li> of the same list. */
  .db-list-quest .db-qkind { display: none; }
}

/* ------------------------------------------------------------- a quest */

/* THE PROSE GETS THE WIDE COLUMN. .db-two is built for the item page, where
   the narrow side is a tooltip and the wide side is a list of droppers. A quest
   is the other shape round -- the words ARE the page and the questgivers are
   the aside -- so the same grid is flipped rather than a second one written.
   Below 900px .db-two collapses to one column for both, and the flip puts the
   quest text first there too, which is the right order on a phone. */
.db-two-flip { grid-template-columns: minmax(0, 1fr) minmax(0, 22em); }

/* Quest prose is read as prose, unlike a tooltip, so it gets ordinary
   paragraph rhythm and a measure that stops at about 68 characters. Full-width
   lines on a 1400px monitor are the fastest way to make a page nobody finishes
   reading. */
.db-quest-main { max-width: 44em; }
.db-q-text p { margin: 0 0 0.9em; line-height: 1.65; }
.db-q-text p:last-child { margin-bottom: 0; }

/* The one-line summary the client puts under the title in your quest log. */
.db-q-lead {
  margin: 0 0 6px;
  font-size: 1.05rem;
  line-height: 1.55;
  color: var(--ink);
}

/* A SUBSTITUTION WITH NOBODY TO SUBSTITUTE. "$N" cannot become a name on a
   database page, so lib/questtext.js renders "<name>" -- and it has to be
   visibly a slot rather than more prose, because the same quests contain
   "<snort>" and "<cough>" as stage directions and the two would otherwise look
   identical. Dotted underline, dimmed, with the explanation on hover. */
.db-q-sub {
  color: var(--ink-faint);
  border-bottom: 1px dotted var(--line);
  cursor: help;
}

.db-q-h3, .db-q-sub-h {
  font-weight: 400;
  font-size: 0.92rem;
  color: var(--ink-dim);
  margin: 16px 0 6px;
}

.db-q-list { list-style: none; margin: 0 0 4px; padding: 0; }
.db-q-list li {
  padding: 5px 0;
  border-bottom: 1px solid var(--ground-2);
  font-size: 0.92rem;
  line-height: 1.5;
}
.db-q-list li:last-child { border-bottom: none; }
/* The count in front of an objective. Monospaced so "10x" and "3x" line up
   down a list rather than wandering with the digit width. */
.db-q-n { font-family: var(--mono); font-size: 0.84rem; color: var(--ink-faint); }
.db-q-spell { color: var(--gold-dim); }

.db-q-rewards { list-style: none; margin: 0 0 10px; padding: 0; }
.db-q-reward {
  display: flex;
  gap: 10px;
  align-items: center;
  padding: 5px 0;
  text-decoration: none;
}
.db-q-reward:hover .db-name { text-decoration: underline; text-underline-offset: 3px; }

/* THE CHAIN. An <ol> because it is an ordered series and the numbers are the
   part numbers, but the markers are ours: the list can start at part 3 when the
   line through this quest does, and a browser's own counter would restart at 1
   and quietly renumber the series. */
.db-q-chain { list-style: none; margin: 0 0 4px; padding: 0; counter-reset: none; }
.db-q-chain li {
  display: flex;
  gap: 10px;
  align-items: baseline;
  padding: 5px 0;
  border-bottom: 1px solid var(--ground-2);
  font-size: 0.92rem;
}
.db-q-chain li:last-child { border-bottom: none; }
.db-q-part {
  flex: 0 0 auto;
  min-width: 1.4em;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--ink-faint);
  text-align: right;
}

/* The quest you are on. Not a link -- see views/db-quest.html -- so it needs to
   look deliberate rather than like a link that failed to render: brighter than
   the rest of the list, which is the opposite of how a disabled thing usually
   reads, because this one is not disabled, it is the answer. */
.db-q-chain li.on { background: rgba(255, 255, 255, 0.03); }
.db-q-here { color: var(--gold); font-weight: 600; }
.db-q-chain li.on .db-q-part { color: var(--gold-dim); }

.db-q-fork { font-size: 0.86rem; margin: 8px 0 0; }
.db-q-fork a { margin-right: 10px; }

/* The part number on a list row. Quiet: it is a disambiguator, not the name. */
.db-q-partn {
  margin-left: 8px;
  color: var(--ink-faint);
  font-size: 0.78rem;
  font-family: var(--mono);
  white-space: nowrap;
}

.db-q-note {
  margin: 14px 0 0;
  padding: 10px 12px;
  background: var(--ground-2);
  border-left: 3px solid var(--gold-dim);
  font-size: 0.88rem;
  color: var(--ink-dim);
}
.db-q-end { font-size: 0.88rem; margin: 16px 0 0; }

/* THE SPACE ABOVE A GROUP HEADING WAS WHATEVER THE BLOCK ABOVE HAPPENED TO
   LEAVE, and that is the whole bug. `.section-h` carries `margin: 0 0 22px` --
   no top margin at all -- so on this page the gap above each heading came from
   the previous block's bottom margin and measured 6px after the lead paragraph,
   0px after a body of prose (`.db-q-text p:last-child` zeroes its own margin)
   and 4px after a list. Three different gaps, none of them deliberate.

   It also had the typographic relationship backwards. A heading belongs to what
   comes AFTER it, so the space above must be larger than the space below;
   at 0-6px above and 22px below, every heading was glued to the group it had
   just finished and floating away from the one it introduces.

   So the heading owns both sides: 34 above, 22 below, and nothing else in the
   column contributes. Scoped to these two columns rather than set on
   `.section-h` itself, because that class is worn all over the site and a top
   margin on the first heading inside a `.band` would add to the 40px the band
   already has. The aside had half of this already; now both columns share it. */
.db-quest-main .section-h,
.db-quest-side .section-h { margin-top: 34px; }
.db-quest-main > :first-child,
.db-quest-side > :first-child { margin-top: 0; }

/* A HEADING FOLLOWED BY A LIST HAD A BIGGER OPTICAL GAP THAN ONE FOLLOWED BY A
   PARAGRAPH, even though both measured 22px between the boxes: a paragraph's
   text starts at the top of its box, and a list row adds 5px of its own padding
   first. Trimming the outer edge of each list makes every block hug its content,
   so the 22px is the whole distance in every case. The inner rows keep their
   padding -- it is what separates the rows from each other. */
.db-q-list li:first-child,
.db-q-chain li:first-child,
.db-q-rewards li:first-child .db-q-reward { padding-top: 0; }
.db-q-list li:last-child,
.db-q-chain li:last-child,
.db-q-rewards li:last-child .db-q-reward { padding-bottom: 0; }

@media (max-width: 900px) {
  .db-two-flip { grid-template-columns: 1fr; }
  .db-quest-main { max-width: none; }
}

/* ------------------------------------------------- the database tab strip */

/* One nav entry, two listings. Underlined like the topbar's current page
   rather than boxed: a tab bar with borders next to the gold frame is a
   different toolkit, and the site has no other pill-shaped control. */
.db-tabs {
  display: flex;
  gap: 26px;
  align-items: baseline;
  margin: 18px 0 -6px;
  border-bottom: 1px solid var(--ground-2);
  padding-bottom: 10px;
}
.db-tab {
  color: var(--ink-dim);
  text-decoration: none;
  font-size: 1.02rem;
  padding-bottom: 8px;
  margin-bottom: -11px;
  border-bottom: 2px solid transparent;
}
.db-tab:hover { color: var(--ink); }
.db-tab.on { color: var(--gold); border-bottom-color: var(--gold); }
.db-tab-n { color: var(--ink-faint); font-size: 0.8rem; font-family: var(--mono); margin-left: 3px; }

/* ----------------------------------------------------- creature list rows */

/* THE LEVEL IS A NUMBER IN A COLUMN, NOT A BADGE. It was a bordered, filled
   box back when it held the left edge of the row and had to read as a thing
   rather than a stray figure -- "what is the rectangle with a number inside
   it?" was asked about exactly that. It sits after the name now, under a
   column heading that names it, so the box is answering a question nobody has
   any more, and sixty outlined boxes down a table that has just been given its
   own rules is one grid too many. The column is still 4.6em because a creature
   range ("34 - 35") has to fit.
   THE GRID ITSELF IS DEFINED ONCE, further down under "the creature row's
   columns"; it used to be redefined here and twice more, each widening the
   previous, which is how the rank column ended up as it did. */
.db-lvl-badge {
  display: block;
  color: var(--ink-dim);
  font-family: var(--mono);
  font-size: 0.8rem;
  white-space: nowrap;
}
.db-sub { color: var(--ink-faint); font-style: italic; }

/* Elite, rare and boss. Colour carries the rank the way quality carries an
   item's -- and for the same reason it is pulled towards the palette rather
   than copied off the client's portrait borders.
   ONE DEFINITION, TWO LISTS: the quest list's faction and type tags are the
   same object one column over, so they share the shape here and differ only in
   colour below. Two copies of these six declarations is how the creature row
   grid ended up with four. */
.db-rank, .db-qtag {
  display: inline-block;
  padding: 1px 6px;
  border: 1px solid var(--line);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}
.rank-elite { border-color: var(--gold-dim); color: var(--gold); }
.rank-rare { border-color: #6f9ed6; color: #6f9ed6; }
.rank-boss { border-color: var(--bad); color: var(--bad); }

/* `.db-nospawn` marked an unspawned creature on a list row. The Presence column
   came out 2026-09-09 -- whether a creature is placed in the world, and what
   reaches it if not, is a paragraph on its own page rather than a word on a list
   row -- and the rule went with it rather than matching nothing. */

/* `.db-head-lvl` was the level in a big gold box beside the creature's name,
   where an item page puts its icon. It went 2026-09-09: it made a number that
   means nothing on its own the second-loudest thing on the page, and it read as
   a different KIND of fact from health and armor, which it is not. The level is
   the first row of the stat card now. */

/* THE STAT CARD IS A LABELLED LIST, not a stack of sentences. It used to read
   "3095 armor", "290 attack power", "Stands still" -- the way the CLIENT writes
   a tooltip, where the lines are one object and the reader already knows the
   shape. A creature's page is a data sheet instead: a dozen values of different
   kinds, and unlabelled they were reported as squished and unsorted. Two
   columns, a name on every value, and the numbers line up. */
.db-statcard { padding: 20px 22px; }
.db-stats {
  display: grid;
  grid-template-columns: minmax(0, 8.5em) minmax(0, 1fr);
  gap: 9px 14px;
  align-items: baseline;
  margin: 0;
}
/* The same voice the listings put on a column heading, not `.db-raw`'s mono --
   those labels are literally database column names and these are English. */
.db-stats dt {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-faint);
}
.db-stats dd { margin: 0; min-width: 0; font-size: 0.92rem; line-height: 1.45; }
.db-stats-good { color: var(--good); }
/* A qualifier on its own line under the value it qualifies: "every 2.4s" is not
   part of the damage number and putting it inline made the row wrap mid-figure. */
.db-stats-sub { display: block; font-size: 0.82rem; color: var(--ink-faint); }
.db-stats code { font-size: 0.82rem; }
/* `.db-head-roles` was the creature's roles on their own line under the name.
   They are the card's "Does" row now, with the type, family, faction and entry
   that shared that header with them, so the rule went rather than matching
   nothing. `.db-head-entry` is still worn by the item and quest heads. */

/* A linked source row still reads as a row, not as a paragraph of blue -- and
   `inherit` is load-bearing: the quality colour is on the SPAN around the link,
   so an item in a drop list keeps being purple or green while a creature keeps
   the page's ordinary ink. Setting a colour here would throw all of that away.

   The underline is the affordance. Without it these read as plain text and
   nothing suggests clicking, which is how the first version shipped; a hairline
   in --line is quiet enough to sit under forty rows without becoming a texture. */
.db-src-name a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--line);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.db-src-name a:hover { text-decoration-color: currentColor; }

/* A FOURTH COPY OF THE CREATURE ROW GRID lived here, for max-width 900, and it
   was dead: the same media query further down declares it again, later in the
   file and at the same specificity, so this one never applied. It said
   `4.6em minmax(0, 1fr) 5.5em` -- three columns for a row that has four at
   that width -- which is what a copy nobody edits drifts into. Deleted rather
   than corrected; the definition under "the creature row's columns" is the
   only one. */

/* ------------------------------------------------- rendered creature models */

/* A creature has no icon, so the picture is the model itself, rendered by
   tools/model-browser/npc-thumbs.js. No frame around it: these are full figures
   on transparent ground with a lot of empty margin, and a box drawn round that
   emphasises the empty space rather than the animal. */
.db-art { width: 44px; height: 44px; display: block; }
.db-art img { display: block; width: 100%; height: 100%; object-fit: contain; }

/* The model on a creature's own page. Shown at its natural size and never
   scaled up -- the renders are cropped to the figure, so the picture is as tall
   as the creature is and a kobold does not sit in a wolf's worth of empty box.
   max-height keeps a tall model from pushing the stat block off the screen. */
.db-left { min-width: 0; }
.db-model { margin: 0 0 18px; text-align: center; }
.db-model img {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  max-height: 260px;
  width: auto;
  height: auto;
}
.db-model figcaption { margin-top: 8px; font-size: 0.8rem; }

/* The model figure carries .shot so it can reuse the gallery's lightbox handler
   in app.js -- which means it also inherits the gallery's TILE styling, and that
   crops to 16:10 with object-fit: cover. On a screenshot that is the whole
   point; on a full-body model it cuts the head off. So the three rules that make
   a tile a tile are undone here, explicitly rather than by hoping load order
   sorts it out. */
.db-model img {
  aspect-ratio: auto;
  object-fit: contain;
  border: 0;
  background: none;
}
.db-model a { display: inline-block; cursor: zoom-in; }

/* The caption is the creature's name, and it is here for the LIGHTBOX -- which
   reads it out of the markup -- not for the page, where the same name is in the
   heading four lines above. Hidden rather than removed: textContent is still
   readable by the script, and a duplicate name under a picture of the thing it
   names is noise. */
.db-model .shot-caption { display: none; }
/* .shot-credit is uppercase and letterspaced because on the homepage it is a
   photographer's byline. Here it is a footnote about a file. */
.db-model .shot-credit {
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.78rem;
  color: var(--ink-faint);
}

/* The overlay showing a MODEL rather than a screenshot: no frame, no panel
   behind it. See the note in app.js -- a border round a figure on transparent
   ground frames the empty air next to it. */
.lightbox-plain .lightbox-fig img { border: 0; background: none; }

/* ------------------------------------------------------ spawn zone maps */

/* One image per ZONE with the creature's spawns marked on it. The map is the
   client's own, composed from its tiles and its explored-area overlays by
   tools/model-browser/zone-maps.js, so it is the map a player already knows. */
.db-where { margin-bottom: 30px; }
.db-map {
  position: relative;
  margin-top: 10px;
  border: 1px solid var(--line);
  background: var(--panel);
  line-height: 0;
}
.db-map img { display: block; width: 100%; height: auto; }
.db-map-dots {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
/* Gold, and outlined in the page's own dark ground so a dot stays visible over
   both the pale parchment and the dark mountains. */
.db-map-dots circle {
  fill: var(--gold-lit);
  stroke: rgba(20, 16, 12, 0.85);
  stroke-width: 2;
}

/* Why a creature with no spawn is reachable anyway, and the case where it is
   not. The unreferenced note is gold rather than red: it is a finding about the
   content, not an error. */
.db-reach { margin-top: 10px; }
.db-reach li { border-bottom: 0; padding: 3px 0; }
.db-unref {
  margin-top: 10px;
  padding-left: 12px;
  border-left: 2px solid var(--gold-dim);
  color: var(--ink-dim);
}

/* ------------------------------------------- the creature row's columns */

/* RANK AND PRESENCE ARE TWO COLUMNS, NOT ONE CELL WITH TWO THINGS IN IT. They
   used to share the right-hand cell, right-aligned, and the consequence was
   that nothing lined up: measured down the list the rank tag landed at 1632,
   1627, 1546, 1590 and 1541 px, wandering by ninety pixels depending on whether
   the row also carried an "unspawned" mark and on whether the word was "BOSS"
   or "RARE ELITE". The cell had already been widened three times, each attempt
   adding a new rule rather than editing the last, and the final one gave up and
   let it wrap onto two lines.
   They are different facts -- how hard it hits, and whether it is placed in the
   world at all -- so they get a column each, left-aligned, and a row with only
   one of them leaves the other column empty rather than sliding along.
   ONE DEFINITION. The three superseded copies are gone. */
.db-list-npc .db-row {
  grid-template-columns: 44px minmax(0, 1fr) 4.6em minmax(0, 14em) 5.5em;
}
.db-rankcell { min-width: 0; }
.db-list-npc .db-lvl { white-space: nowrap; text-align: left; }
@media (max-width: 900px) {
  .db-list-npc .db-row { grid-template-columns: 44px minmax(0, 1fr) 4.6em; }
}

/* ---------------------------------------------------------------- grid view

   /database/npcs?view=grid. ONLY THIS TAB HAS ONE, and the reason is in the
   pictures: a creature render is ~234px on its long side and the list shows it
   at 44, so a grid is the first thing here that actually uses them. Items have
   56px icons, which a grid would only make emptier, and quests have no picture
   at all. Coverage was measured before any of this was drawn -- 14,367 of
   14,369 indexed creatures resolve to a render -- so this is not a grid of
   placeholder frames.

   FOUR COLUMNS, COUNTED, NOT A minmax FLOOR -- and that is a deliberate
   departure from .gallery's pattern two hundred lines up. An auto-fill floor
   gives a count that falls out of the window width: at 150px it was six across
   a full 1040px .wrap and five at the ~800-950px a part-width window has, which
   is what "they are a little small with 5 per row" was reporting. Asking for
   four per row is asking for a COUNT, and no single floor delivers one --
   tuning it to four at 1040 gives three at 850, and tuning it to four at 850
   gives five at 1040.

   So the cost is a breakpoint, which .gallery does not need and this now has
   exactly one of. Below 560px four columns would be ~130px each, narrower than
   the thing they are complaining about, so it halves there rather than
   shrinking further.

   Cards land at (1040 - 30) / 4 = 252px on a full wrap and ~190px on a
   part-width window -- both comfortably above the ~150px they were. */
.db-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
}

.db-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  background: var(--sheet);
  padding: 10px 10px 8px;
  text-decoration: none;
  color: inherit;
  height: 100%;
}
.db-card:hover { background: var(--panel); border-color: var(--gold-dim); }

/* THE PICTURE BOX IS A FIXED HEIGHT AND THE FIGURE SITS ON ITS FLOOR.
   The renders are trimmed to the figure and their shapes vary enormously --
   66x234 for something tall and thin, 234x128 for something long -- so
   centring them would leave every creature floating at a different height and
   cropping would cut a tall one down to a slice of torso. align-items: flex-end
   puts them all on one ground line, which is what makes a kodo read as bigger
   than a ram instead of both filling their box.

   This is the grid's version of the rule .db-model states for a creature's own
   page: never scale a render up. max-height, not height, on the image. */
.db-cardart {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  height: 160px;
  margin-bottom: 8px;
}
/* max-height, never height: a render is shown at its own size or smaller and
   NEVER scaled up, so the 234px tall ones come down to 160 and the 128px ones
   sit at 128 on the same floor. Raised from 116 with the column count, because
   a 252px card with a 116px picture is a card with a hole in it. */
.db-cardart img {
  max-width: 100%;
  max-height: 160px;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* Two lines and then an ellipsis. A creature name is short far more often than
   not, but "Scarlet Commander Mograine" exists and a card that grows to fit it
   would break the row's alignment for the other seven. */
.db-cardname {
  font-weight: 600;
  line-height: 1.25;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* margin-top: auto pins this to the bottom of the card, so the level badges
   line up across a row whose names are one line and two. */
.db-cardmeta {
  margin-top: auto;
  padding-top: 6px;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

/* THE SORT STRIP, which exists because the grid has no column headings and
   sorting on this page IS the headings. Same links, same arrows; a row of them
   instead of a table head. */
.db-sortbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 14px;
  padding: 8px 11px;
  margin-bottom: 10px;
  border: 1px solid var(--line);
  background: var(--panel);
}
.db-sortbar-label { color: var(--ink-dim); }

/* List | Grid. Deliberately quiet: it is two links about how to look at the
   page, not a filter, and the loudest thing above this list should still be
   what is in it. */
.db-view {
  display: flex;
  gap: 0;
  margin-bottom: 8px;
}
.db-viewbtn {
  border: 1px solid var(--line);
  padding: 4px 12px;
  text-decoration: none;
  color: var(--ink-dim);
  background: var(--sheet);
}
.db-viewbtn + .db-viewbtn { border-left: 0; }
.db-viewbtn:hover { color: var(--ink); background: var(--panel); }
.db-viewbtn.on { color: var(--ink); background: var(--panel); border-color: var(--gold-dim); }

@media (max-width: 560px) {
  .db-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .db-cardart, .db-cardart img { height: 120px; max-height: 120px; }
}

/* "2 forms" on a collapsed twin row, and the link between the halves on their
   pages. Quieter than the "ours" tag: it is a note about the data, not a
   property of the creature. */
.db-forms {
  display: inline-block;
  margin-left: 8px;
  color: var(--ink-faint);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}
.db-head-forms { margin: 6px 0 0; font-size: 0.88rem; color: var(--ink-dim); }
.db-head-forms a { color: var(--gold); }

/* ---------------------------------------------------------- dressing room

   Four regions on a grid: the figure, who you are, what you are wearing, and
   the picker. The picker is the only one that appears and disappears, and it
   takes the fourth cell rather than floating over the figure -- a modal would
   hide the very thing you are choosing against.

   On a narrow screen it all becomes one column, figure first. */

/* The page is one column now: the paper doll, then the picker under it. The
   slots used to be a list in a side column; they are squares flanking the
   figure, the way the game's character panel arranges them. */
.dr { display: block; }

/* THE PAPER DOLL: a column of slot squares, the figure, another column.
   space-between pushes the two columns to the edges and gives everything
   between them to the figure. */
.dr-doll {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
}
.dr-col { display: flex; flex-direction: column; gap: 8px; flex: 0 0 auto; }
/* The stage takes everything the two slot columns leave. */
.dr-stage { flex: 1 1 auto; min-width: 0; }

.dr-figure {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--line);
  /* HEIGHT IS WHAT MAKES THE CHARACTER BIGGER, not width. A character is tall
     and narrow, and the render is contained inside this box, so the fit is
     bounded by the box's height -- widening the frame alone would move the
     slot columns apart and leave the figure exactly the size it was.
     So the frame is given as much HEIGHT as comfortably fits the window, and
     takes whatever width the row has left. The dark space either side of the
     character is the same empty panel the game's character sheet has.
     A fixed height also keeps the controls below from jumping as one look is
     swapped for a taller one, which is what the old square aspect was for. */
  height: min(78vh, 820px);
  display: flex;
  align-items: center;
  justify-content: center;
}
/* THE FIGURE'S OWN IMAGE, and nothing else's. `.dr-figure img` matched every
   picture that has since been put inside the frame -- the slot icons in the
   hands row and every card in the picker -- and at (0,1,1) it outranks the
   .dr-slotpic and .dr-shot rules meant to size them, so a 128px silhouette
   stayed 128px and no amount of editing those rules did anything. A direct
   child is what was meant all along. */
.dr-figure > img { width: 100%; height: 100%; object-fit: contain; display: block; }

.dr-spinner {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(20, 16, 12, 0.45);
}
.dr-spinner::after {
  content: "";
  width: 28px; height: 28px;
  border: 2px solid var(--gold-dim);
  border-top-color: var(--gold-lit);
  border-radius: 50%;
  animation: dr-spin 0.7s linear infinite;
}
@keyframes dr-spin { to { transform: rotate(360deg); } }
/* Respect a reader who has asked for less movement: the spinner still says
   "working" as a steady ring rather than a moving one. */
@media (prefers-reduced-motion: reduce) {
  .dr-spinner::after { animation: none; border-top-color: var(--gold-dim); }
}

/* Over the figure's bottom-right, opposite the appearance fold. Both sit on the
   picture instead of under it, so the preview keeps its height and the two
   overlays balance the frame. */
/* No chip behind them: the two buttons already read as controls, and a panel
   round a pair of buttons is a box drawn for its own sake. */
.dr-turn {
  position: absolute; right: 10px; bottom: 10px; z-index: 2;
  display: flex; align-items: center; gap: 6px;
}
.dr-note { margin: 8px 0 0; font-size: 0.85rem; min-height: 1.2em; text-align: center; }

/* ------------------------------------------------ the appearance overlay

   Laid over the figure's top-left rather than given a column of its own. It is
   set once and then in the way, so it folds; the summary carries the race and
   gender so the fold still says who is being dressed.

   The panel is absolutely positioned and the <summary> is not, so opening and
   closing never moves the figure or reflows the page. */
.dr-who {
  position: absolute; top: 10px; left: 10px; z-index: 2;
  max-width: calc(100% - 20px);
}
.dr-who-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px;
  background: rgba(20, 16, 12, 0.82);
  border: 1px solid var(--line);
  color: var(--ink); font: 0.85rem var(--serif);
  cursor: pointer; list-style: none; user-select: none;
}
.dr-who-btn::-webkit-details-marker { display: none; }
.dr-who-btn::before {
  content: "\25B8"; color: var(--gold); font-size: 0.8em;
  transition: transform 0.12s ease;
}
/* Both folds, not just the appearance one. `.dr-bg` borrows .dr-who-btn for
   its summary but is its own element, so the open rule has to name it too --
   the backdrop arrow sat pointing right whether the panel was open or shut. */
.dr-who[open] > .dr-who-btn::before,
.dr-bg[open] > .dr-who-btn::before { transform: rotate(90deg); }
.dr-who-btn:hover { border-color: var(--gold-dim); }
.dr-who-btn:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: 2px; }
.dr-who-panel {
  position: absolute; top: calc(100% + 6px); left: 0;
  /* Wide enough that "Hair colour" and "Facial hair" stay on one line; at 230
     they wrapped and the rows lost their alignment. */
  width: 264px;
  padding: 10px 12px 12px;
  background: rgba(20, 16, 12, 0.93);
  border: 1px solid var(--line);
}
.dr-who-random { width: 100%; margin-top: 4px; }

/* Only ever visible when a render failed, so it can afford to be loud. */
.dr-note {
  margin: 8px 0 0; text-align: center;
  font-size: 0.85rem; color: var(--bad, #c9724f);
}

/* THE HANDS, in a row under the frame. They were an overlay along the bottom of
   the picture first -- the game's own arrangement -- and that put clickable
   squares over the figure's shins, in among the corner controls they had to
   dodge. Under the frame they belong to the doll rather than to the picture,
   and the row is the one place on this page where a control is allowed to cost
   height: it is three squares, and the frame is 78vh. */
.dr-hands {
  display: flex; align-items: center; justify-content: center;
  gap: 8px; margin-top: 10px;
}
/* Three equal segments in ONE ROW, not the two columns .dr-seg is sized for --
   with two columns the third button wrapped underneath. Two class names deep
   because `.dr-seg` is declared later in this file at the same specificity and
   wins otherwise; this is the third time that has bitten in this block, so the
   rule of thumb is: anything overriding .dr-row or .dr-seg needs an ancestor in
   the selector. `auto` rather than `1fr` so the segments size to their words --
   "Ranged" is wider than "Away" and equal thirds padded the short ones out. */
/* Lit when the glow is on, in the same palette as a chosen segment, so the two
   controls in this row read as one idea. */
.dr-hands .dr-glow { margin-left: 8px; }
.dr-hands .dr-glow.off {
  background: var(--panel); color: var(--ink-dim);
  border-color: var(--line); font-weight: 400;
}
.dr-hands .dr-glow.off:hover { background: var(--panel-2); color: var(--ink); border-color: var(--gold-dim); }

.dr-hands .dr-hold {
  display: inline-grid; grid-auto-flow: column; grid-template-columns: none;
  grid-auto-columns: auto; margin-left: 10px;
}

/* THE BACKDROP FOLD mirrors the appearance one into the opposite corner and
   borrows its summary button wholesale -- two folds on one picture that looked
   like two different controls would be worse than one idea used twice. */
.dr-bg { position: absolute; top: 10px; right: 10px; z-index: 2; }
.dr-bg-btn { flex-direction: row-reverse; }     /* the arrow after the swatch */
.dr-bg-panel { left: auto; right: 0; width: 232px; }
.dr-bg-dot {
  width: 14px; height: 14px; border-radius: 50%;
  border: 1px solid var(--line); background: var(--panel);
}
.dr-bg-presets { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin-bottom: 10px; }
.dr-bg-swatch {
  height: 26px; padding: 0; border: 1px solid var(--line); cursor: pointer;
  background: var(--panel);
}
.dr-bg-swatch:hover { border-color: var(--gold-dim); }
.dr-bg-swatch.on { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); }
.dr-bg-swatch:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: 2px; }
/* Two class names deep on purpose: `.dr-row` is declared LATER in this file and
   at the same specificity, so a plain `.dr-bg-row` loses to it and the override
   silently does nothing.
   A FIXED LABEL COLUMN, not .dr-row's `1fr`. A bare `1fr` is
   `minmax(auto, 1fr)`, so each row's first column fell back to the width of its
   own word -- "Saturation" is wider than "Hue" -- and the three sliders started
   at three different x positions even though all three were 140px wide. 76px
   fits the longest label, and the slider takes what is left of every row. */
.dr-bg .dr-bg-row { grid-template-columns: 76px minmax(0, 1fr); margin-bottom: 6px; }
.dr-bg .dr-bg-row label { font-size: 0.82rem; color: var(--ink-dim); }
.dr-bg .dr-bg-row input[type="range"] { width: 100%; min-width: 0; margin: 0; accent-color: var(--gold); }
@media (prefers-reduced-motion: reduce) {
  .dr-who-btn::before { transition: none; }
}

/* A LABEL COLUMN AND A CONTROL COLUMN, both fixed. As a flex row with
   space-between, each control was as wide as its own content -- the select
   stretched, the gender pair did not, and the steppers were a third width
   again, so nothing lined up down the panel. A grid with one fixed control
   column gives every row the same edges. */
.dr-row {
  display: grid; grid-template-columns: 1fr 140px;
  align-items: center; gap: 10px; margin-bottom: 8px;
}
.dr-row label, .dr-label { font-size: 0.9rem; color: var(--ink-dim); white-space: nowrap; }
.dr-row select { width: 100%; max-width: none; min-width: 0; }

/* Arrows at the ends, the number in the middle -- so the arrows sit exactly
   under the gender buttons and the select's edges. This also retires the old
   `min-width: 6ch` on the number, which was hand-holding the flex row into
   something the grid now does properly. */
.dr-step { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 4px; }
.dr-stepnum { text-align: center; font: 0.82rem var(--mono); color: var(--ink-dim); }

.dr-seg { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
/* THE SELECTED ONE HAS TO LOOK SELECTED. This pair used to rely on `.ghost`
   for the unselected state, and `.ghost` only changes text colour -- on a
   gold-filled .btn that is invisible, so both halves looked identical and the
   toggle never showed which way it was set. */
.dr-seg .btn {
  background: var(--panel); color: var(--ink-dim);
  border-color: var(--line); font-weight: 400;
}
.dr-seg .btn:hover { background: var(--panel-2); color: var(--ink); border-color: var(--gold-dim); }
.dr-seg .btn.on {
  background: var(--gold); color: #221a0c;
  border-color: var(--gold); font-weight: 600;
}
.dr-seg .btn.on:hover { background: var(--gold-lit); border-color: var(--gold-lit); }
/* Right-aligned, and constrained to the doll's own width rather than the page's
   so they land under the right-hand slot column instead of drifting off to the
   far edge. 640 is the doll: two 46px columns, two 14px gaps and a 520 stage. */
/* Over the figure's bottom-left, the mirror of the turn arrows. No chip behind
   them for the same reason: a panel around two buttons is a box drawn for its
   own sake. */
.dr-actions {
  position: absolute; left: 10px; bottom: 10px; z-index: 2;
  display: flex; gap: 6px;
}

.dr-h { font-weight: 400; font-size: 1rem; margin: 0 0 10px; color: var(--ink-dim); }

/* THE SQUARE AND ITS CLEAR BUTTON. The wrapper only exists to hang the corner
   control off; it is `line-height: 0` so it does not add a text baseline's
   worth of height under the square and push the column out of line. */
.dr-slotwrap { position: relative; line-height: 0; }
.dr-slotclear {
  position: absolute; top: -6px; right: -6px; z-index: 1;
  width: 18px; height: 18px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  /* A gold rim and full-strength ink: at 18px on a dark panel the ordinary
     line/ink-dim pair was there and invisible, which for a control that only
     appears on hover means it may as well not. */
  border: 1px solid var(--gold-dim); border-radius: 50%;
  background: var(--panel); color: var(--ink);
  font: 14px/1 var(--sans, sans-serif); cursor: pointer;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
  /* Out of the way until wanted. `visibility` rather than `display` so the
     button keeps its box and the hover does not flicker as it appears. */
  opacity: 0; visibility: hidden;
  transition: opacity 0.1s ease;
}
.dr-slotwrap:hover .dr-slotclear,
.dr-slotclear:focus-visible { opacity: 1; visibility: visible; }
.dr-slotclear:hover { background: var(--gold); color: #221a0c; border-color: var(--gold); }
.dr-slotclear:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: 1px; }

/* A slot square. The empty-slot art is the client's own silhouette, so an
   unfilled square looks exactly like the game's; a filled one shows the item's
   inventory icon, same as a bag square. */
.dr-slot {
  display: block; padding: 0; line-height: 0;
  background: var(--panel);
  border: 1px solid var(--line);
  cursor: pointer;
}
.dr-slot:hover { border-color: var(--gold-dim); }
.dr-slot:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: 2px; }
.dr-slot.on { border-color: var(--gold); box-shadow: inset 0 0 0 1px var(--gold-dim); }
.dr-slotpic { width: 66px; height: 66px; display: block; }
/* The silhouettes are a dim grey in the client and read as a disabled control
   here; dropping their opacity further makes "nothing in this slot" obvious at
   a glance without another colour in the palette. */
.dr-slot.empty .dr-slotpic { opacity: 0.45; }

/* A set's card is a whole figure rather than a body part, so it wants the
   opposite fit: contain, or cover would crop the head and the boots off the
   one picture where the whole outfit is the point. And two rows rather than
   three, so there is room for a figure to be a figure. */
.dr-shot-set { object-fit: contain; }
/* The stand-in when a display names a model this client does not ship. Contain
   and dimmed, so it reads as "nothing to show" rather than as a picture. */
.dr-shot-miss { object-fit: contain; opacity: 0.3; }
.dr-results.sets { grid-template-rows: repeat(2, minmax(0, 1fr)); }

/* What is worn, under the doll -- the squares are icons and an icon cannot say
   "Judgement Breastplate". */
.dr-worn { margin: 10px 0 0; text-align: center; font-size: 0.85rem; min-height: 1.3em; }

/* The name of the look, above the tool. A labelled field in the site's own
   input style rather than a bare line of text: an input with no border reads as
   a heading that has somehow become editable, and the label is what says the
   box is for typing in. The label matches the ones on /icons and /objects. */
/* 640, not 420. The row holds the field and two buttons, so at 420 the field
   itself was 260 -- narrower than the names people give things, under a tool
   1040 wide. */
.dr-name { margin: 0 0 14px; max-width: 640px; }
.dr-namenote { margin: 6px 0 0; text-align: left; font-size: 0.82rem; }
.dr-namelabel {
  display: block; margin: 0 0 6px;
  font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-faint);
}

/* ------------------------------------------------------------- the dialog */

/* One ask-before-you-do dialog, in the layout, used by whoever needs it. Same
   two pieces as the picker -- a veil that swallows the click and a panel over
   it -- at a higher z-index, because this is the one thing on the page that has
   to be answered before anything else can happen. */
.dlg-scrim {
  position: fixed; inset: 0; z-index: 59;
  background: rgba(20, 16, 12, 0.6);
}
.dlg {
  position: fixed; z-index: 60;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(380px, 92vw); padding: 18px 20px 16px;
  background: var(--ground); border: 1px solid var(--gold-dim);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.dlg-text { margin: 0 0 16px; font-size: 0.95rem; }
/* The confirming button first and the way out beside it, both full-height and
   equal width: a dialog with two answers should not make one of them look like
   the afterthought it is not. */
.dlg-row { display: flex; gap: 8px; }
.dlg-row .btn { flex: 1 1 0; }

/* ------------------------------------------------------------- saved looks */

/* Under the doll, and given room to breathe: this is a second thing on the
   page rather than another control on the first one. */
.dr-saved { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--line); }
.dr-savedhead { margin: 0 0 12px; font-size: 1rem; font-weight: 400; color: var(--ink-dim); }

/* Cards as wide as they need to be and no wider: a look is a standing figure,
   so the card is taller than it is broad and the grid fits as many as the row
   has room for. */
.dr-cards {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
  /* STRETCHED, so the plus card is exactly as tall as a saved one. Every saved
     card is the same height as every other -- a square picture and a name on
     one line -- so the only card stretching does anything to is the plus, which
     is the one that was short. It was `align-items: start` for a revision, back
     when the picture was 384px tall and stretching made every card a column of
     empty panel; the picture is square and fluid now and that is no longer a
     risk. */
  align-items: stretch;
}
.dr-card {
  position: relative;
  background: var(--panel); border: 1px solid var(--line);
  /* So the plus card's button can fill the height the grid hands it. */
  display: flex; flex-direction: column;
}
.dr-card:hover { border-color: var(--gold-dim); }
.dr-cardlink { display: block; text-decoration: none; color: inherit; }
.dr-cardpic {
  /* `height: auto` is what makes aspect-ratio apply at all: the element carries
     width and height ATTRIBUTES so the browser can reserve a box before the
     render lands, and while the height attribute is in force the ratio is
     ignored -- which drew every card 384px tall in a 138px column. */
  display: block; width: 100%; height: auto; aspect-ratio: 1 / 1;
  object-fit: contain; background: var(--ground-2);
}
.dr-cardname {
  display: block; padding: 6px 8px; text-align: center;
  font-size: 0.82rem; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The corner controls are the slot square's, reused wholesale -- same shape,
   same hover, same meaning. They only need telling which hover reveals them,
   and it is the card's rather than their own. */
.dr-card:hover .dr-slotclear,
.dr-card .dr-slotclear:focus-visible { opacity: 1; visibility: visible; }

/* OVERWRITE LIVES ON THE CARD IT OVERWRITES, beside the minus: the same 18px
   circle, so the two corner controls read as one family. It borrows
   .dr-slotclear for all of that and only moves along the top edge.

   ON EVERY CARD, ALWAYS. It was on the edited card only, and only once
   something had changed, which made it a control you had to earn before you
   could see it -- and it ruled out the thing somebody will obviously want,
   which is to point at any saved look and put what is on the doll into it. The
   minus still waits for hover because deleting wants a moment's deliberation;
   this does not. */
.dr-cardsave { color: var(--gold); }
.dr-cardsave svg { display: block; }

/* HALF AGAIN THE SIZE OF A SLOT'S CORNER, because these are not the same job. A
   slot square's minus is a correction on a control you are already pointing at;
   these two are the only way to save or delete a look. They inherit everything
   else -- the shape, the hover, the focus ring -- from .dr-slotclear. */
.dr-card .dr-slotclear {
  width: 27px; height: 27px;
  top: -8px; font-size: 15px;
}
.dr-card .dr-cardx { right: -8px; }
.dr-card .dr-cardsave { right: 23px; }
/* The look you are in, lit whether or not you have changed it -- a dozen
   figures in a row and otherwise no way to tell which one is on the doll. */
.dr-card.open { border-color: var(--gold); }
.dr-card.open .dr-cardname { color: var(--gold); }
/* Changed, and so worth finding without hovering every card to look. */
.dr-card.editing { box-shadow: inset 0 0 0 1px var(--gold-dim); }

/* The plus card matches the others in size and says what it does; an icon on
   its own would be a mystery button in a grid of pictures. */
.dr-card-new { border-style: dashed; }
.dr-newbtn {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* min-height is the floor for the only case stretch cannot answer: the plus
     card alone in the grid, with nothing saved yet to be as tall as. */
  gap: 6px; width: 100%; flex: 1 1 auto; min-height: 150px; padding: 10px;
  background: none; border: 0; cursor: pointer; color: var(--ink-dim);
  font: inherit; font-size: 0.82rem;
}
.dr-newbtn:hover { color: var(--ink); background: var(--panel-2); }
.dr-newbtn:disabled { opacity: 0.5; cursor: default; }
.dr-plus { font-size: 1.6rem; line-height: 1; color: var(--gold); }


/* A DIALOG CENTRED IN THE WINDOW, not a panel pinned to the frame.
   position:fixed rather than absolute so it is always fully on screen wherever
   the page happens to be scrolled -- pinned to the figure it could open half
   below the fold on a short window. It closes on Close, on Escape, and on
   choosing something. */
/* One below the dialog, over everything else. Light rather than the usual heavy
   modal dim -- the point is to push the page back a step so the dialog reads as
   being in front of it, not to hide the page. */
.dr-scrim {
  position: fixed; inset: 0; z-index: 39;
  background: rgba(20, 16, 12, 0.55);
}

.dr-picker {
  position: fixed; z-index: 40;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(880px, 92vw);
  height: min(660px, 82vh);
  display: flex; flex-direction: column;
  padding: 10px 12px 12px;
  /* Fully opaque. At 0.97 the appearance summary and the corner buttons showed
     faintly through the panel, which reads as a rendering fault rather than as
     depth. The three overlays are hidden outright as well -- a Clear button
     ghosting under a grid is a thing people try to click. */
  background: var(--ground);
  border: 1px solid var(--gold-dim);
}
/* The dialog no longer sits over the figure, so the frame's own corner controls
   can stay where they are. */
.dr-pager {
  display: flex; align-items: center; gap: 6px;
  flex: 0 0 auto; margin-left: auto;
}
.dr-pager span { min-width: 7ch; text-align: center; font: 0.78rem var(--mono); }
.dr-pager button[disabled] { opacity: 0.35; cursor: default; }

/* One tab per armour type the slot has -- or one per class in the set browser.
   It sits under the search bar and directly above the grid it filters, so the
   margin is below it rather than above. */
.dr-tabs { display: flex; flex-wrap: wrap; gap: 4px; margin: 0 0 8px; }
.dr-tab {
  padding: 4px 10px;
  background: var(--panel);
  border: 1px solid var(--line);
  color: var(--ink-dim); font: 0.82rem var(--serif);
  cursor: pointer;
}
.dr-tab:hover { border-color: var(--gold-dim); color: var(--ink); }
.dr-tab:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: 2px; }
.dr-tab.on { border-color: var(--gold); color: var(--ink); background: var(--panel-2); }
.dr-tab-n { color: var(--ink-faint); font: 0.72rem var(--mono); margin-left: 5px; }

.dr-pickhead { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.dr-pickbar { display: flex; gap: 10px; margin: 8px 0; align-items: center; }
#drcount { flex: 0 0 auto; font-size: 0.8rem; white-space: nowrap; }
/* THE SAME TRAP .obj-bar DOCUMENTS, walked into again. The global input/select
   rule sets width:100%, so `flex: 1 1 auto` makes both items resolve their
   basis from that and the select takes the whole row while the search box
   collapses to a sliver. Both need an explicit basis, and the select needs
   width:auto to undo the global. */
.dr-pickbar input[type="search"] { flex: 1 1 240px; min-width: 0; }


.dr-results {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  /* FOUR COLUMNS AND THREE ROWS, both fixed. The card is a picture and the
     picture is the thing being compared, so a predictable grid beats one that
     reflows -- and `1fr` rows mean the twelve cards divide whatever height the
     frame has rather than overflowing it. That is what replaced the scrollbar:
     a page always fits exactly. */
  /* minmax(0, 1fr), NOT 1fr. A bare `1fr` is `minmax(auto, 1fr)`, and `auto` as
     a minimum means min-content -- so the rows refused to shrink below the size
     the pictures wanted and the third one was clipped off the bottom no matter
     what the container did. The 0 minimum is what lets three rows actually
     divide the height they are given. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-template-rows: repeat(3, minmax(0, 1fr));
  gap: 8px;
  /* flex-basis 0, NOT auto. With `auto` the grid is sized from its content --
     twelve cards each as tall as their picture wants -- so it overflowed the
     dialog and the pager ended up drawn across the second row. With 0 it takes
     exactly the height the header, tabs and pager leave, and the 1fr rows
     divide that. overflow:hidden is the belt to that pair of braces. */
  flex: 1 1 0;
  min-height: 0;
  overflow: hidden;
}
.dr-result {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  width: 100%; height: 100%; min-height: 0; overflow: hidden; text-align: center;
  /* No side padding: the picture goes edge to edge and the caption is inset
     instead. The render is the thing being compared, so it gets the width. */
  padding: 0 0 8px;
  background: var(--panel);
  border: 1px solid var(--line);
  color: inherit; font: inherit; cursor: pointer;
}
/* The picture takes whatever the card's row leaves after the caption. No
   aspect-ratio: the row height is set by the grid, and an aspect box would
   fight it and push the last row out of the frame.
   COVER, not contain. The renders are square and the cards are not quite, so
   `contain` left a band of dead panel above and below every one of them --
   twelve little letterboxes. `cover` fills the card and crops the edges
   instead, which costs nothing here: the figure is centred in its render with
   margin all round, so what gets trimmed is background. */
.dr-shot {
  flex: 1 1 auto; min-height: 0; width: 100%;
  display: block; object-fit: cover;
  background: var(--ground-2);
}
.dr-resulttext { display: flex; flex-direction: column; gap: 1px; width: 100%; padding: 0 8px; }
.dr-result:hover { border-color: var(--gold-dim); background: var(--panel-2); }
.dr-result.on { border-color: var(--gold); background: var(--panel-2); }
.dr-result:focus-visible { outline: 2px solid var(--gold-lit); outline-offset: -2px; }
/* ONE LINE, then an ellipsis. Item names run long and a name that wrapped would
   push its card taller than the three beside it, which breaks the row the grid
   exists to make comparable. The full name is in the title attribute for
   anything that gets cut. */
.dr-resultname {
  font-size: 0.82rem; line-height: 1.4;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%;
}
.dr-resultmeta { font-size: 0.72rem; }

@media (max-width: 860px) {
  .dr-doll { gap: 8px; }
  .dr-slotpic { width: 44px; height: 44px; }
  /* Less window to spend on a phone, and the picker matters more there. */
  .dr-figure { height: min(62vh, 520px); }
  .dr-results { grid-template-columns: repeat(2, 1fr); }
  /* On a phone the overlay would cover most of the figure, so it sits above it
     in the flow instead and keeps the fold. */
  .dr-who { position: static; max-width: none; margin-bottom: 10px; }
  .dr-who-panel { position: static; width: auto; margin-top: 6px; }
}
@media (max-width: 420px) {
  .dr-results { grid-template-columns: 1fr; }
}
