/* ==========================================================================
   App layer — loaded AFTER parse-theme.css
   --------------------------------------------------------------------------
   parse-theme.css owns the design: every colour, font and radius token lives
   there and it styles bare HTML on its own. This file defines NO colours of
   its own. It only:

     1. bridges markup this app already uses onto that design
        (header.app, footer.app, a.btn.primary, …)
     2. styles the components the design layer doesn't know about
        (.doll, .board, .parse, …)

   Keeping it that way means a new design layer drops straight in — nothing
   here needs merging, because nothing here decides how anything looks, only
   which rules apply to which markup. That is not theoretical: this file has
   been through two full re-skins — cold-black-and-red, then warm-black-gold,
   now the WoW logo's blue-black and gold with four game fonts — and not one
   rule below was rewritten for colour on either pass. Only sizes moved, and
   only because the typefaces changed width.

   The one deliberate exception is the two borrowed scales near the bottom,
   documented where they appear.
   ========================================================================== */


/* ---------- page frame ---------------------------------------------------- */

html, body { min-height: 100vh; }
body { display: flex; flex-direction: column; }

main {
  flex: 1;
  width: 100%;
  max-width: 1240px;
  margin: 0 auto;
  padding: 30px 22px 70px;
}

/* header.app is this app's .navbar + .navbar-inner in one element. */
header.app {
  background: var(--bar);
  border-bottom: var(--border) solid var(--chrome-line);
  display: flex;
  align-items: center;
  gap: 22px;
  min-height: 54px;
  padding: 0 22px;
  flex-wrap: wrap;
  position: relative;
  isolation: isolate;
}
/* Chain lattice across both chrome bars — they are the frame around the page,
   so they get the one texture with an actual pattern in it rather than grain.
   It tiles (seams 7.2/5.9) and is very dark to begin with, so it reads as
   worked metal rather than as a picture of chains.

   A single chain running the width of the header was tried here and cut. It
   worked mechanically — one measured period, cross-faded join, field knocked
   out — but the links are twice as wide as they are tall, so a chain short
   enough for a 54px bar ends up as thirty small links across a wide screen and
   reads as busy noise. The lattice has no such scale problem: it is a texture,
   and textures do not care how many times they repeat. */
header.app::before, footer.app::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--art-chain) center / 292px repeat;
  opacity: 0.55;
  pointer-events: none;
}
footer.app { position: relative; isolation: isolate; }

/* The brand sits in a thin frame — the emblem treatment. It stays on the left
   rather than moving to the centre: that would be a layout change, and the nav
   has to live somewhere.

   The name is red, which is a deliberate exception to the rule stated at the
   top of parse-theme.css that crimson belongs to things that *act*. A wordmark
   isn't a control, but it is the one thing on the page allowed to be the
   brand — and on a near-black bar the red is what makes it read as a mark
   rather than as more chrome. Everything else in the header keeps the rule. */
header.app .brand {
  font-family: var(--font-header);
  font-size: 15px;
  letter-spacing: 0.16em;
  color: var(--accent-quiet);
  text-transform: uppercase;
  /* Neutral fill, not the accent one. Play sits inches away with a red plate
     and a red border, and giving the wordmark the same treatment made the
     header look like two buttons. Red text and a red edge on a dark plate says
     "this is the mark"; the filled crimson stays with the thing that acts. */
  background: var(--select-soft);
  border: var(--border) solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 5px 13px 4px;
  transition: border-color 0.12s ease, color 0.12s ease;
}
header.app .brand:hover {
  color: var(--accent-text);
  border-color: var(--accent-line-hover);
}

header.app nav { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }

/* Play is the only thing on this site that starts a run, so it stops looking
   like a link and starts looking like the one you press: a blood plate with a
   few drips running off the bottom edge.
   The drips are a masked SVG rather than baked art, exactly like the slot
   silhouettes — the shape lives here, the colour comes from `--accent` through
   currentColor, so a re-skin recolours them without touching this file. */
header.app .modes a {
  position: relative;
  padding: 7px 15px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 600;
  font-family: var(--font-body);
  color: var(--text-bright);
  background: var(--accent-soft);
  border: var(--border) solid var(--accent);
  transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
  /* Three drips of uneven length, so it reads as running rather than as a
     fringe. The fill is black because this is only ever used as a *mask* —
     nothing but the alpha survives, and the colour arrives from the rule
     below. `preserveAspectRatio='none'` lets it stretch to whatever width the
     word is without the drops going oval. */
  --drip-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2064%2016'%20preserveAspectRatio='none'%3E%3Cpath%20fill='black'%20d='M0%200h64v2H0z'/%3E%3Cpath%20fill='black'%20d='M12%201h2.6v7.4a1.3%201.3%200%201%201-2.6%200z'/%3E%3Cpath%20fill='black'%20d='M30%201h2.6v3.9a1.3%201.3%200%201%201-2.6%200z'/%3E%3Cpath%20fill='black'%20d='M48%201h2.6v10.6a1.3%201.3%200%201%201-2.6%200z'/%3E%3C/svg%3E");
}
header.app .modes a::after {
  content: "";
  position: absolute;
  left: -1px; right: -1px;
  top: 100%;
  height: 15px;
  /* currentColor is the trick: the drips are the same colour as whatever the
     border is doing, so they brighten on hover with everything else and never
     need a second rule to keep them in sync. */
  color: var(--accent);
  background-color: currentColor;
  -webkit-mask: var(--drip-shape) top center / 100% 100% no-repeat;
          mask: var(--drip-shape) top center / 100% 100% no-repeat;
  pointer-events: none;
  transition: color 0.14s ease;
}
header.app .modes a:hover {
  background: var(--accent);
  border-color: var(--accent-hover);
  color: var(--text-bright);
}
header.app .modes a:hover::after { color: var(--accent-hover); }
/* "You are here" is a place, so it takes the logo's plate blue. Crimson stays
   reserved for things that act; gold stays chrome. Three colours, three jobs.
   Unused for now — base.html stopped applying it, because with a single-item
   nav it only ever announced that the header had changed colour. Kept for the
   day there is somewhere else to navigate to. */
header.app .modes a.is-active {
  background: var(--select-soft);
  border-color: var(--select-line);
  color: var(--text-bright);
  font-weight: 600;
}
header.app .spacer { flex: 1; }

/* ---- header groups -------------------------------------------------------
   Layout only. Every colour below is an existing token doing the job it
   already does elsewhere in this file; nothing new was introduced. */

/* The static game/expansion label. Reuses `.who`'s treatment because it is the
   same kind of thing — a quiet statement of context, not a control. */
header.app .game-label {
  font-size: 11px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-family: var(--font-body);
  white-space: nowrap;
}
header.app .game-label i { font-style: normal; opacity: 0.55; margin: 0 2px; }

header.app .modes { gap: 6px; }

/* Not links, so they get none of the plate. A mode that is coming reads as a
   label with a tag rather than as something you can press and be disappointed
   by. */
header.app .is-soon {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px;
  font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase;
  font-weight: 600; font-family: var(--font-body);
  color: var(--text-faint);
  cursor: default;
  white-space: nowrap;
}
header.app .is-soon i {
  font-style: normal;
  font-size: 8.5px; letter-spacing: 0.12em;
  padding: 2px 5px;
  border: var(--border) solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--text-dim);
}

/* Product pages, up from the footer. Quiet on purpose: they sit beside the one
   control that starts a run and must not compete with it. */
header.app .util { gap: 4px; }
header.app .util a {
  padding: 7px 11px;
  font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase;
  font-family: var(--font-body);
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  transition: color 0.12s ease;
}
header.app .util a:hover { color: var(--text-bright); }

/* On a phone the header now has six things in it and wrapped to 180px, which is
   a fifth of the screen spent before you can see the game. The two entries that
   go first are the ones that are context rather than navigation: the static
   game label, and the Soon tags for modes that cannot be opened anyway. Brand,
   Standard, How to play and About survive, which is everything you can press. */
@media (max-width: 760px) {
  header.app .game-label,
  header.app .is-soon { display: none; }
}
/* `--accent-quiet` and not `--accent`: this is small uppercase text on a
   near-black bar, and the full crimson is a *fill* colour that goes muddy at
   11px. The quiet variant exists for exactly this — accent-coloured text. */
header.app .who {
  font-size: 11px;
  color: var(--accent-quiet);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-family: var(--font-body);
}

footer.app {
  border-top: var(--border) solid var(--line);
  background: var(--surface);
  padding: 20px 22px;
  font-size: 12.5px;
  color: var(--text-dim);
  line-height: 1.7;
}


/* ---------- version badge and changelog ---------------------------------- */

/* The footer's one clickable thing anybody has a reason to press twice, so it
   reads a step brighter than the legal text around it without becoming a
   button. `--chrome` is the brand/links role; nothing here picks a colour. */
.version-badge {
  color: var(--chrome-bright);
  font-family: var(--font-numeric);
  letter-spacing: 0.02em;
  position: relative;
  margin-right: 4px;
}
.version-badge:hover { color: var(--text-bright); }

/* The unread dot. Added by changelog-badge.js only when this browser last saw
   an older version — never on a first visit, because a new player is not behind
   on anything and a dot that greets everyone stops carrying information.
   
   Positioned absolutely so it cannot reflow the row when it appears. The
   offsets sit *inside* the link's padding box, hugging the text corner --
   the old -2px/-7px were written for the padding-less footer link, and in
   the padded header they floated the dot 18px into empty space, which on
   the wrapped phone header read as a loose gold speck. */
.version-badge.has-news::after {
  content: "";
  position: absolute;
  top: 3px; right: 3px;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--plate-gold);
  box-shadow: 0 0 5px 1px rgb(248 207 71 / 0.45);
}

/* One release. The rule above each entry does the separating, so the versions
   read as a stack of dated blocks rather than as one long list of sentences. */
.release + .release { border-top: var(--border) solid var(--line-soft); }
.release h2 {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
.release h2 time {
  margin-left: auto;
  font-family: var(--font-numeric);
  font-size: 12.5px;
  color: var(--text-dim);
  /* Never the reason the heading wraps: the date is the least important thing
     on the line and should be the first to fall away, not the thing that
     pushes the title down. */
  flex: none;
}
.release-version {
  font-family: var(--font-numeric);
  color: var(--plate-gold);
  border: var(--border) solid var(--line-strong);
  border-radius: var(--radius);
  padding: 1px 7px;
  font-size: 13px;
  flex: none;
}
/* A release that needs explaining (1.14 Duels): an intro line and a few
   headed sections above the tagged change lines. */
.release-intro { margin: 6px 0 10px; color: var(--text-body); }
.release-section h3 { margin: 14px 0 4px; font-size: 12px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--plate-gold); }
.release-section p { margin: 0; color: var(--text-body); }
.release-section ul { margin: 4px 0 0; padding-left: 20px; color: var(--text-body); }
.release-section li { margin: 3px 0; }
.release-section + .release-changes { margin-top: 12px; }
.release-changes { list-style: none; padding: 0; margin: 0; }
.release-changes li {
  display: flex; gap: 10px;
  padding: 7px 0;
  color: var(--text-body);
}

/* added / changed / fixed. Fixed width so the sentences start on one column and
   the list reads as a table without being one. */
.change-kind {
  flex: none;
  width: 62px;
  text-align: center;
  align-self: flex-start;
  margin-top: 2px;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border-radius: var(--radius);
  padding: 2px 0;
  border: var(--border) solid var(--line);
  color: var(--text-muted);
}
.change-kind.is-added   { color: var(--ok);      border-color: var(--ok-line); }
.change-kind.is-fixed   { color: var(--chrome-bright); border-color: var(--chrome-line); }
.change-kind.is-changed { color: var(--plate-gold); border-color: var(--line-strong); }

@media (max-width: 560px) {
  /* The tag above the text rather than beside it: at this width a 62px column
     leaves the sentence in a gutter. */
  .release-changes li { flex-direction: column; gap: 4px; }
  .change-kind { width: auto; align-self: flex-start; padding: 2px 8px; }
  .release h2 time { margin-left: 0; }
}


/* ---------- type helpers -------------------------------------------------- */

p.lede, .lede {
  color: var(--text-dim);
  font-size: 12.5px;
  margin: 0 0 16px;
}

/* Section headings: centred, gold hairlines running out to both edges, a
   diamond at each inner end. Rule and diamond are two background layers on the
   pseudo-elements — the gradient stops 13px short and the diamond fills the
   gap — so the ornament costs no markup and every heading in every template
   gets it for free. Both the diamond and the rule colour come from design-layer
   tokens; nothing about how this looks is decided here. */
main > h2, .duel-new > h2 {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin: 38px 0 18px;
  text-align: center;
  /* The Character Draft treatment (Sosa, 1.08): the hero's display serif
     in plate gold with the plate shadow, size untouched. The uppercase
     tracking goes with the old face; the serif carries its own case. */
  font-family: var(--font-display);
  text-transform: none;
  letter-spacing: 0.02em;
  color: var(--plate-gold);
  text-shadow: var(--plate-text-shadow);
}
main > h2::before, .duel-new > h2::before,
main > h2::after, .duel-new > h2::after {
  content: "";
  flex: 1;
  height: 9px;
  background-repeat: no-repeat;
  background-size: 7px 7px, calc(100% - 13px) 1px;
}
main > h2::before, .duel-new > h2::before {
  background-image: var(--ornament-diamond),
    linear-gradient(to right, transparent, var(--chrome-line));
  background-position: right center, left center;
}
main > h2::after, .duel-new > h2::after {
  background-image: var(--ornament-diamond),
    linear-gradient(to left, transparent, var(--chrome-line));
  background-position: left center, right center;
}

/* A card's heading is a TITLE, not a section divider — it gets none of the
   above. Gold like the hero since 1.08 (Sosa: every title below Character
   Draft wears its style), at its own smaller size. */
.card h2, .card h1 {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.03em;
  text-transform: none;
  color: var(--plate-gold);
  text-shadow: var(--plate-text-shadow);
  margin: 0 0 6px;
}
/* The one carve-out, per the note above .hero h1: gold lettering on
   "Privacy" would read as a mistake. Legal and prose pages keep quiet
   headings. */
.prose h1, .prose h2 {
  color: var(--text);
  text-shadow: none;
}


/* ---------- anchors styled as buttons ------------------------------------- */

/* `a.btn.primary` used to be styled here, because the design layer covered
   `button.primary` and not the anchor form. That put the crimson in two files
   and let this one silently win, which is exactly the drift this split exists
   to prevent — so the selector moved into parse-theme.css with the rest of the
   plate and this file is back to defining no colour at all. */
a.btn { text-decoration: none; }


/* ---------- stat tiles ---------------------------------------------------- */

.stat {
  background: var(--surface-2);
  border: var(--border) solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
}
/* A score is a number the game would have thrown at you, so it takes the
   combat-text face. Metamorphous is wide and decorative — it runs smaller than
   a plain serif would at the same optical size. */
.stat .value {
  font-family: var(--font-numeric); font-size: 21px;
  color: var(--chrome-bright); line-height: 1.25;
}
.stat .label {
  color: var(--text-dim); font-size: 10.5px;
  text-transform: uppercase; letter-spacing: 0.14em; margin-top: 3px;
}


/* ---------- layout -------------------------------------------------------- */

.grid { display: grid; gap: 16px; }
/* 420, not 320. At 320 four rule cards fit three to a row on a desktop and the
   fourth is left orphaned on its own line; 420 forces the honest 2x2. */
.grid.cols-2 { grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); }
.grid.cols-3 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.grid.cols-4 { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.card { margin-bottom: 16px; }
/* The grid already spaces its children. Leaving the margin on made every row
   gap 32px against a 16px column gap, which reads as a mistake because it is
   one. */
.grid > .card { margin-bottom: 0; }

/* ---- modes and the leaderboard placeholder ------------------------------- */
.mode-grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  margin-bottom: 16px;
}
.mode-grid > .card { margin-bottom: 0; }
.mode-head { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; }
.mode-name {
  font-family: var(--font-display); font-size: 20px; letter-spacing: 0.02em;
  /* Gold with the rest of the titles (Sosa, 1.08): Standard and Daily
     wear the Character Draft treatment too, at their own size. */
  color: var(--plate-gold);
  text-shadow: var(--plate-text-shadow);
}
/* One tag component for every "not yet" on the site, so Daily in the header,
   Daily on the page and the leaderboard panel all say it the same way. */
.mode-tag {
  font-family: var(--font-body);
  font-size: 8.5px; letter-spacing: 0.14em; text-transform: uppercase;
  padding: 3px 7px;
  border: var(--border) solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--text-dim);
}
.mode-tag.is-live { border-color: var(--accent-line); color: var(--accent-quiet); }
/* The Soon card's name used to grey out; since the 1.08 gold pass both
   mode names match (Sosa) and the Soon tag alone carries the state. */
.mode-card p, .soon-panel p { margin: 0; }

/* Fills the space that used to sit empty above the footer. Deliberately says
   what is coming rather than showing invented rows: the leaderboard needs
   display names, there are no accounts, and fabricated usernames on a page
   whose problem is looking auto-generated would be the worst possible filler. */
.soon-panel {
  display: flex; align-items: center; gap: 14px;
  padding: 18px 20px;
}

.notice {
  display: flex; gap: 13px; flex-direction: column;
  padding: 13px 16px; margin-bottom: 18px;
  background: var(--surface-2);
  border: var(--border) solid var(--line);
  border-left: 3px solid var(--line-strong);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font-size: 13.5px;
}
.notice.error { border-left-color: var(--accent-line); color: var(--danger-text); }


/* ==========================================================================
   Two borrowed colour scales
   --------------------------------------------------------------------------
   Everything above this point uses the design layer's tokens and defines no
   colours of its own. These two blocks are the deliberate exception, and it is
   worth being explicit about why: neither scale is ours to pick.

   Item rarity and the Warcraft Logs parse bands are conventions this audience
   has read at a glance for twenty years. Re-tinting them to fit the site's
   palette — even to fit a gold one that would flatter them — would be a
   flourish that destroys the one piece of information people came for. They
   live in their own tokens so they stay obviously separate from the design
   system, and they survived the re-skin untouched.
   ========================================================================== */

:root {
  /* WoW item quality */
  --q-poor:      #9d9d9d;
  --q-common:    #ffffff;
  --q-uncommon:  #1eff00;
  --q-rare:      #0070dd;
  --q-epic:      #a335ee;
  --q-legendary: #ff8000;

  /* Warcraft Logs parse bands */
  --parse-grey:   #808080;
  --parse-green:  #1eff00;
  --parse-blue:   #0070ff;
  --parse-purple: #a335ee;
  --parse-orange: #ff8000;
  --parse-pink:   #e268a8;
  /* 100 only. Heirloom gold, the one colour in the game that means nothing
     ranks above this. Borrowed like the rest of the scale, not chosen. */
  --parse-gold:   #e5cc80;
}

.q-poor      { --rarity: var(--q-poor); }
.q-common    { --rarity: var(--q-common); }
.q-uncommon  { --rarity: var(--q-uncommon); }
.q-rare      { --rarity: var(--q-rare); }
.q-epic      { --rarity: var(--q-epic); }
.q-legendary { --rarity: var(--q-legendary); }


/* ---------- home ---------------------------------------------------------- */

.hero { max-width: 780px; margin: 30px 0 44px; }
/* The pitch in a frame. Roomier than a normal card because it is the first
   thing on the page and the only one carrying a 28px heading — the standard
   17px would have the title touching the border. */
.hero-panel { padding: 26px 30px 28px; margin-bottom: 0; }
.hero-panel h1 { margin-top: 0; }
.hero-panel .hero-line:last-child { margin-bottom: 0; }
/* The one heading that takes the button lettering: same face, same gold, same
   shadow as every plate on the site. It works here and nowhere else — this is
   the only heading large enough to carry a display serif with a hard shadow
   under it, and gold lettering on "Privacy" would read as a mistake.

   The old comment here claimed h1 was already gold in the design layer. It was
   not; it was `--chrome-bright` in Almendra, left over from an earlier skin. */
.hero h1 {
  /* Centred while the paragraph under it stays left. The title is two words
     in a display serif sitting between the two gryphons, so centring it puts
     it on the axis they already imply; the body is a real measure and
     centring that would leave it ragged on both edges. */
  text-align: center;
  font-size: 34px;
  line-height: 1.25;
  margin-bottom: 18px;
  font-family: var(--font-display);
  color: var(--plate-gold);
  text-shadow: var(--plate-text-shadow);
}
.hero-line { font-size: 15px; line-height: 1.65; color: var(--text-muted); }
.hero-line strong { color: var(--text); }
.hero-line em { color: var(--chrome-quiet); font-style: normal; }

/* The picker is the page's whole job, so it gets its own grid rather than
   borrowing `cols-3`. A fourth spec reflows instead of being orphaned on a row
   of its own, which is what a fixed three-column grid would have done the day
   the roster grew.

   `auto-fit`, not the `auto-fill` the spec asked for. They differ only when
   there are fewer items than tracks, which is exactly our case: at 1196px
   `auto-fill` made four columns for three specs, shrinking every card to a
   quarter width and leaving a hole where the fourth would go. `auto-fit`
   collapses the empty track so three cards fill the row. Same reflow, no gap —
   and it matches `.grid.cols-2` and `.cols-3`, which already use it. */
.spec-grid {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(auto-fit, minmax(268px, 1fr));
  margin-bottom: 16px;
}
.spec-grid > .spec-card { margin-bottom: 0; }

.spec-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
  padding: 22px;
  text-align: left;
  background: var(--surface-2);
  border: var(--border) solid var(--line);
  font-weight: 400;
  /* No plate shadows. `.spec-card` is a `<button>`, and parse-theme.css defines
     the button plate on `.btn, button, input[type="submit"]` — a bare element
     selector — so every button on the site inherits its insets, this panel
     included. The offender is `inset 0 1px 0 rgb(255 255 255 / 0.20)`: an inset
     shadow paints at the **padding** edge, which since the frame arrived is
     thirteen pixels inside the metal, so it drew a light rule in the middle of
     the panel that read as a second, inner box. It looked worse on hover only
     because the fill changed underneath it while the rule did not.
     
     This is a panel that happens to be a button. The frame is its edge.
     
     Stated as the outer shadow rather than `none`, which is what it was: `none`
     killed the insets *and* the depth, so this panel alone sat flat on the page
     while every other framed panel had a shadow under it. */
  box-shadow: 0 6px 22px rgb(0 0 0 / 0.55);
}
/* The same ground as every other card — see the panel rule near the top, which
   lists .spec-card alongside them. Carved stone was tried here to make the two
   rows read as different materials and it just looked like a smudge: cropped to
   a card, that texture's soft light and dark patches have no structure to read
   as stone. One material for every card is both simpler and better. */
.spec-card:hover:not(.disabled) {
  background: var(--surface-3);
  border-color: var(--chrome-line);
}
/* Blizzard's own spell art, so it needs no border or tint — just a quiet
   inset to sit it on the card rather than floating. */
.spec-icon {
  flex: none;
  border-radius: 3px;
  border: var(--border) solid var(--line-strong);
  vertical-align: middle;
}
.spec-head { display: flex; align-items: center; gap: 13px; }
.spec-card .spec-name {
  font-family: var(--font-display); font-size: 21px; letter-spacing: 0.02em;
}
.spec-card:hover:not(.disabled) .spec-icon { border-color: var(--chrome-line); }
.spec-card .spec-meta { font-size: 12px; color: var(--text-dim); }
/* This card *is* the action, so its label is crimson, not gold.
   It used to be a bare line of small text, which meant the one control this
   page exists to get pressed looked like a caption. Same colours, given the
   shape of a button. */
.spec-card .spec-go {
  margin-top: auto;
  align-self: stretch;
  text-align: center;
  padding: 9px 14px;
  font-size: 13px; letter-spacing: 0.06em;
  color: var(--plate-gold);
  font-family: var(--font-display);
  text-shadow: var(--plate-text-shadow);
  /* The same crimson plate every other primary button wears. It had a
     near-copy of the old flat treatment, which would now be the one button on
     the site that had not been restamped. */
  background: linear-gradient(180deg, #8e2429 0%, #ad3036 18%, #8b2126 58%, #5c1418 100%);
  border: 2px solid var(--plate-edge);
  border-radius: 3px;
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.20),
    inset 0 -2px 3px rgb(0 0 0 / 0.45),
    inset 0 0 14px rgb(0 0 0 / 0.35);
  transition: background 0.12s ease, color 0.12s ease;
}
.spec-card:hover:not(.disabled) .spec-go {
  background: linear-gradient(180deg, #a32d33 0%, #c53b42 18%, #9e2a30 58%, #6c191e 100%);
  color: var(--plate-gold-lit);
}
/* `margin-top: auto` above needs somewhere to push against, so the cards match
   heights and the buttons line up across the row however long a tagline runs. */
.spec-grid > .spec-card { height: 100%; }
/* Unavailable, not broken. Fading the whole card at 0.38 took its background
   and border with it, so the card itself looked half-rendered against the
   textured page rather than looking like a thing that isn't ready yet. The card
   stays solid; its *contents* are what recede. */
.spec-card.disabled { cursor: default; }
.spec-card.disabled .spec-icon { filter: grayscale(0.9); opacity: 0.45; }
/* The one place a class colour is deliberately dropped. It isn't a re-tint of
   the borrowed scale — an unplayable spec has no class colour to earn yet. */
.spec-card.disabled .spec-name { color: var(--text-dim); }
.spec-card.disabled .spec-meta,
.spec-card.disabled .spec-go { color: var(--text-faint); }


/* ---------- run: the top bar ---------------------------------------------- */

/* The run bar wears the plate treatment, inverted. The buttons on it are raised
   — lit along the top, shadowed below — because you press them. The bar holds
   them, so it is *recessed*: the same rim, the same bevel logic, the light
   coming from the same place, but the facets swapped. That is what makes a
   row of stamped buttons look set into a frame rather than scattered on a
   background. */
.run-bar {
  display: flex; align-items: center; gap: 22px; flex-wrap: wrap;
  padding: 14px 18px; margin-bottom: 16px;
  background: linear-gradient(180deg, #1b1d20 0%, #212428 40%, #191b1e 100%);
  border: 2px solid var(--plate-edge);
  border-radius: 3px;
  box-shadow:
    inset 0 2px 4px rgb(0 0 0 / 0.55),
    inset 0 -1px 0 rgb(255 255 255 / 0.06),
    0 1px 2px rgb(0 0 0 / 0.35);
}
.run-bar .spacer { flex: 1; }

/* In Standard mode "rolls remaining" and "slots empty" are always the same
   number — you roll while a slot is open — so this is one counter rather than
   two that can never disagree. Design doc §15 left that open; live filtering
   settles it. */
.run-counter { display: flex; flex-direction: column; }
.run-counter .value {
  font-family: var(--font-numeric); font-size: 24px; line-height: 1.1;
  color: var(--chrome-bright);
}
.run-counter .label {
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-dim); margin-top: 5px;
}
.run-counter.quiet .value { font-size: 20px; color: var(--text-muted); }

/* The track yields; the controls never do.
   
   `.run-bar` is a wrapping flex row, so anything that grows past the line
   pushes whatever follows it onto the next one — and what follows is Reroll
   Boss and Reroll All. Holding four partial sets at once was enough to do it,
   which moved the two most-clicked buttons on the page out from under the
   cursor mid-run.
   
   `min-width: 0` plus `nowrap` is what lets this shrink instead of shoving:
   a flex item's default `min-width: auto` refuses to go below its content, so
   without that line it cannot yield and the rerolls have to move. The pills are
   already capped in drawTier, so the clip is a backstop rather than the plan. */
.tier-track {
  display: flex; gap: 8px;
  /* Pills wrap onto a second row rather than being cut off.
     
     This is safe *because* of the `flex: 1 1 0` below, and the two have to be
     read together. A zero basis means the track claims no intrinsic width at
     all, so it can never push the rerolls anywhere no matter how many pills it
     holds — the wrapping happens inside the track's own box, and the buttons
     keep the position they had when the track was empty. That is the whole
     difference from the original bug, where the track's natural width shoved
     Reroll Boss and Reroll All onto a line of their own mid-run.
     
     Wrapping beats truncating here because flex collects items into lines from
     their natural widths *before* any shrinking, so a pill that does not fit
     moves down whole instead of being squeezed. The ellipsis on `.tier-name` is
     still the last resort for a single set name wider than the entire track. */
  flex-wrap: wrap;
  row-gap: 6px;
  /* `flex: 1 1 0` and not `min-width: 0` alone. The parent wraps, and wrapping
     is decided from each item's *natural* width before any shrinking happens —
     so a track wide enough to need shrinking has already pushed the rerolls to
     the next line by the time shrinking would help. Measured: six pills took
     the bar from 79px to 147px with `min-width: 0` set. A zero basis means the
     track claims no intrinsic width at all, takes only what is left over, and
     therefore can never push anything. Same six pills, bar stays at 79px.
     
     The grow factor is 5 against `.spacer`'s 1, and that ratio is the whole
     reason wrapping is rare rather than constant. Both have a zero basis, so
     they split the leftover room by grow alone — at 1:1 the spacer was taking
     244px of *nothing* while the pills wrapped onto a third row beside it.
     Weighted 5:1 the same three sets sit on one line. The zero basis is
     untouched, so the rerolls still cannot be pushed. */
  flex: 5 1 0;
  overflow: hidden;
}
.rerolls { flex: none; }
/* Recessed like the bar it sits in. It reports a number, so it should read as
   a window cut into the frame rather than as something else to press. */
/* Never two lines. Once the track shrinks, a pill with room for half its name
   wraps inside itself and grows to 65px, which pushes the whole bar taller —
   the same symptom as the wrap, arriving by a different route. */
.tier-pill {
  white-space: nowrap;
  /* Shrinkable, and allowed to go below its content width — `min-width: auto`
     is the flex default and would refuse, which is what left the track
     overflowing and being sliced mid-word by its own `overflow: hidden`.
     Measured at 1280px with two real sets: 269px of pills in 244px of track. */
  display: inline-flex; align-items: baseline; gap: 5px;
  flex: 0 1 auto; min-width: 0;
  padding: 5px 11px; border-radius: 3px; font-size: 11.5px;
  background: linear-gradient(180deg, #141618 0%, #1a1d20 100%);
  border: var(--border) solid var(--plate-edge);
  color: var(--chrome-quiet);
  box-shadow: inset 0 1px 3px rgb(0 0 0 / 0.5);
}
/* The count is the information; the name only says which set it belongs to.
   So the count never shrinks and the name takes the ellipsis — "2/8 Bloodfang
   Arm…" tells you everything "2/8 Bloodfang Arm" did, and looks intended. */
.tier-pill b { flex: none; font-family: var(--font-numeric); color: var(--chrome-bright); }
.tier-name { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* The overflow count. Quieter than a real pill — it is a note about what is
   not shown, not a set you are tracking. Its `title` lists them. */
.tier-pill.more { color: var(--text-faint); font-family: var(--font-numeric); }

.rerolls { display: flex; gap: 8px; }
.reroll {
  flex-direction: column; align-items: flex-start; gap: 1px;
  padding: 7px 14px; line-height: 1.25;
}
.reroll .reroll-name { font-size: 12.5px; }
.reroll .reroll-note {
  font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase;
  /* Gold held back rather than grey. On a crimson face a neutral grey reads as
     a different material sitting on top of the button; the same hue dimmed
     reads as part of it. */
  color: var(--plate-gold); opacity: 0.55;
  font-weight: 400; font-family: var(--font-body);
  text-shadow: var(--plate-text-shadow);
}
.reroll.spent .reroll-name { text-decoration: line-through; }


/* ---------- run: layout --------------------------------------------------- */

.run-layout {
  display: grid;
  grid-template-columns: minmax(360px, 460px) 1fr;
  gap: 16px;
  align-items: start;
}


/* ---------- run: paper doll ----------------------------------------------- */

/* ---------- ground textures ----------------------------------------------- */
/* Two textures, each used the way its own edges want — which was measured, not
   chosen. The parchment shows a 12.7-level seam left-to-right, so it never
   repeats; it carries a vignette instead, which is exactly what one fixed
   viewport-covering image wants. The stone seams at 8/10, which is clean enough
   to tile inside panels.
   That distinction is the whole lesson from the previous texture pass: art with
   soft or vignetted edges belongs on the viewport, art that tiles belongs on a
   surface, and neither can do the other's job. */

/* `isolation` is doing real work, not tidiness. A negative-z-index child paints
   behind its parent's background unless the parent forms a stacking context —
   and body's background is an opaque `--bg`, so without this the texture is
   painted and then completely covered. Same reason the panels below carry it. */
body { isolation: isolate; }
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  /* Repeated at native size, never scaled. That is the whole reason the stone
     is the one on the page: a `cover` image is resampled to whatever the window
     happens to be, and the parchment was being blown up about 1.5x on a normal
     desktop, which is what made it look low resolution. A tile is never
     resampled at any window size. */
  /* Ember-lit variant, on trial. `--art-page-ground` is the unlit stone and is
     still baked — swapping this one token back is the whole revert. */
  background: var(--art-page-ground-ember) top left repeat;
  /* Fixed, not scrolling: it's the desk the interface sits on, not the paper.

     0.3, chosen by looking at 0.14, 0.3 and 0.45 in turn. The ceiling still
     holds: at 0.5 this stops being grain and becomes a photograph, which lifts
     the page off its near-black and leaves every card looking see-through, and
     0.45 is already close enough to that to feel hot.

     It used to sit at 0.14, which was right for the *unlit* stone. The ember
     tile carries its own light in the mortar, and at 0.14 that light composites
     to within 1-2 steps of 255 of the unlit version — i.e. invisible, which
     made the swap pointless. The texture changed, so the number had to. */
  opacity: 0.3;
  pointer-events: none;
}

/* The parchment covers each panel rather than tiling in it — a panel is smaller
   than the viewport, so `cover` shrinks it instead of stretching it, which is
   the opposite of what the page needs and is why these two are the way round
   they are. Its vignette ends up framing each card.

   Very low. A card is a surface the eye reads dense text off, so this is the
   last trace of grain before it starts competing: at 0.35 the stone showed
   straight through and the cards read as transparent. */
.card, .board, .doll.card, .result-card, .spec-card {
  position: relative; isolation: isolate;
}

/* A carved stone frame around the cards on the front page, 9-sliced out of the
   panel texture. Its ornate band is 10.7% of a 240px bake, so the slice is 26px
   drawn into a 7px border — thin enough to read as a carved edge rather than as
   a picture frame, which is what 15px looked like.

   `round` and not `stretch`: the carving has a repeating rhythm along each
   edge, and stretching it would smear that into a blur on a wide card while
   leaving it cramped on a narrow one. `round` fits whole repeats instead.

   Every large panel on the site wears it, so the front page, the board, the
   character sheet and the results card all read as the same kind of object.
   `.doll` and the rule cards get it through `.card`; the other two are named
   because they are their own components. */
.card, .spec-card, .board, .result-card, .reel {
  /* A soft shadow under every framed panel, and it is doing a specific job.
     
     The panel is `rgb(33,36,39)` on a `rgb(19,20,21)` page — a 14/255 step that
     ends abruptly — and the frame's outer alpha ramp lets a little panel colour
     show past the metal. Neither can be removed: the step *is* the panel, and
     the ramp is how the art antialiases. What the shadow changes is the
     reading. Without it the eye sees a rectangle pasted onto the page; with the
     page darkening toward the panel it sees a raised object, and the edge stops
     being something you notice.
     
     The value is the tooltip's, not a new one. `.tip` wears the same kind of
     9-sliced ornate frame with gaps in it and settled on exactly this pairing —
     `border-box` plus this shadow — for the same reason. Two surfaces with the
     same problem should not have two answers. */
  box-shadow: 0 6px 22px rgb(0 0 0 / 0.55);

  /* The fill runs *under* the border, which is the default and is deliberate.
     
     This art has soft edges — measured, the band's alpha ramps
     34/68/119 → solid → 119/68/34, three pixels of baked antialiasing a side,
     drawn to blend into whatever it frames. Clipping the fill to the padding
     box made those three pixels composite over the page instead of the panel:
     they came out at (9,9,10), darker than the page itself, and then the
     interior jumped to (33,36,39) — a 29/255 step, which is the hard inner
     rectangle you can see once you look. Under the border the same pixels land
     at (29,31,34) against (33,36,39): a 5/255 step, six times smaller and below
     the threshold where an edge reads as an edge.
     
     A `padding-box` clip lived here briefly and fixed a different bug: a slice
     of 16 that pulled in three pixels of *pure transparency* with the band
     crushed into a 7px border. The slice is cut flush at 12/13 now and drawn at
     its native 13px, so the only thing showing through is the antialiasing —
     which is what it is for. */
  /* 13px, which is the art's native size. At 7 the band was scaled to just
     over half and the bevel collapsed into a single hairline — the outer rail,
     the gap and the inner rail all landed on the same pixel. 13 draws it 1:1,
     which is where the corner studs and the double rail actually read. */
  border: 13px solid transparent;
  /* ON TRIAL — the client's bevelled dialog band instead of the carved stone.
     `--art-frame` is still baked, so reverting is this one line.
     
     Slice 12, which is the band's own thickness — see `_dialog_frame`. It was
     16 first, and 16 cut 3px of transparent padding in with the metal; scaled
     into the border that padding became a grey rail of `--surface-2` showing
     through along every edge. Border width stays 7px either way, so the
     padding tuned against it everywhere else does not move. */
  /* Four values, because the bands and the rails are not the same thickness:
     12px top and bottom, 13px left and right. See `_dialog_frame`. */
  border-image: var(--art-frame-dialog) 12 13 12 13 round;

  /* One corner for every panel wearing this frame.
     
     `.spec-card`, `.reel`, `.board` and `.result-card` each pinned this to 0,
     under a comment of mine arguing a rounded fill inside a square frame draws
     a second, smaller box. `.card` never did, so the hero panel has been
     rounded this whole time — and shown the two side by side, Sosa picked the
     rounded one. Looking at it beats the argument.
     
     Declared here rather than deleted from the four, because deleting is not
     the same change: `.spec-card` is a `<button>`, and the button plate gives
     buttons `--radius-sm`, so it would have settled at 1px against the cards'
     2px. That is the same inconsistency wearing a smaller number. The frame and
     its corner are one decision and belong in one place.
     
     `border-image` ignores `border-radius` — the art is drawn square either
     way. What this rounds is the *fill*, at the outer corner where it would
     otherwise show through the art's transparent pixels as a hard point. */
  border-radius: var(--radius);
}
/* Padding minus the border everywhere, so the total inset lands exactly where
   it was before the frame arrived — 17/18 on a card and the board, 26 on the
   results card, 26/30 on the hero panel. The frame is decoration; it should not
   quietly cost the content its room. */
/* Still "the old inset minus the frame", with the frame now 13 rather than 7 —
   so the total distance from the panel's outer edge to its text is unchanged at
   17/18 on a card and 26/30 on the hero. A thicker frame is decoration and must
   not quietly cost the content its room. */
.card, .spec-card { padding: 4px 5px 5px; }
.hero-panel { padding: 13px 17px 15px; }
.card::before, .board::before, .result-card::before, .spec-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--art-panel-ground) center / cover no-repeat;
  opacity: 0.07;
  border-radius: inherit;
  pointer-events: none;
}

/* ---------- item tooltips ------------------------------------------------- */
/* The client's own frame, 9-sliced: `8` takes the 8px ring off a 24px image, so
   the panel grows with its text and the corners never distort — which is the
   entire reason the export ships nine separate pieces and build_ui_assets puts
   them back together.
   
   Deliberately **without** `fill`, and that word is the whole reason this
   tooltip used to look washed out. `fill` tiles the texture's middle cell as
   the background, and that cell is light grey (141,139,141) at alpha 187 —
   over the parchment page it came out pale and bright, nothing like the
   client's. It was never meant to be a background: WoW builds a tooltip from a
   `bgFile` and an `edgeFile`, two separate layers, and this texture is only the
   edge. So the ring comes from the art and the backdrop is painted here, which
   is the same split the client makes. */
.tip {
  position: fixed;
  z-index: 60;
  max-width: 340px;
  padding: 11px 13px;
  border: 8px solid transparent;
  border-image: var(--art-tip-frame) 8 repeat;
  /* Under the border too, not just the padding box: the ornate ring has
     transparent gaps in it, and letting the page glow through them is the
     brightness problem again in miniature. */
  background: var(--tip-bg);
  background-clip: border-box;
  box-shadow: 0 6px 22px rgb(0 0 0 / 0.55);
  font-family: var(--font-body);
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--text);
  pointer-events: none;   /* never let it swallow the hover that spawned it */
}
.tip-line { white-space: normal; }
/* Slot on the left, armour type hard right — the client's layout, and the
   reason it can be read without actually reading it. */
.tip-split { display: flex; justify-content: space-between; gap: 18px; }
.tip-name {
  font-family: var(--font-display);
  font-size: 15px;
  line-height: 1.25;
  color: var(--rarity, var(--text-bright));
  margin-bottom: 1px;
}
.tip-ilvl { color: var(--parse-orange); }
/* Green for anything the item *does* when worn, white for what it simply has.
   Borrowed from the client and deliberately not re-tinted, like the rarity and
   parse scales. */
.tip-equip { color: #1eff00; }
.tip-set { color: var(--text-muted); }
.tip-source { color: var(--text-dim); font-style: italic; }
.tip-rule {
  height: 1px;
  margin: 6px 0 5px;
  background: var(--art-tip-divider) center / 100% 1px repeat-x;
  opacity: 0.5;
}
/* Nothing to hover on a touch screen, and a tooltip that latches on tap is
   worse than none. */
@media (hover: none) {
  .tip { display: none !important; }
}

/* ---------- gryphons ------------------------------------------------------ */
/* One carving and its mirror, flanking a thing worth flanking: the pitch on the
   home page, and the parse on the results page. That's the whole use — they
   bookend the run rather than decorating every surface, which is what keeps
   them reading as ornament instead of as wallpaper. */

/* Neither host has a spare gutter to hang these in. The hero is left-aligned
   against the column edge; the parse is pinned to the head of a flex row with
   the facts filling everything to its right. So the flanked element *makes* the
   room with its own inline padding, and the pair stands inside it — one
   geometry that works for both instead of two sets of offsets that don't. */
.flanked { position: relative; padding-inline: var(--flank); }
.gryphon {
  --flank: 232px;                  /* 212 of gryphon + 20 of breathing room */
  position: absolute;
  top: 50%;
  width: 212px;
  aspect-ratio: 460 / 288;
  transform: translateY(-50%);
  background: var(--art-gryphon) center / contain no-repeat;
  /* The carving is light stone against a dark page, so it needs holding back
     rather than brightening. Opacity, not a filter, so it keeps its own
     modelling. */
  opacity: 0.34;
  pointer-events: none;
}
.flanked { --flank: 232px; }
/* Facing inward, toward the thing they flank. The carving faces *right*, so the
   one on the left is the unmirrored copy and the one on the right is flipped —
   the opposite of what reads as obvious, and it was wrong here until the pair
   was seen side by side facing away from each other like bookends on the wrong
   shelf.
   `left: 0` is the padding box's edge, which is exactly the space made above. */
.gryphon.left  { left: 0; }
.gryphon.right { right: 0; transform: translateY(-50%) scaleX(-1); }

/* box-sizing is border-box globally, so the hero's max-width has to grow by the
   padding or the sentence would lose it. At 212px the pair no longer fits
   alongside a 780px measure — 780 + 2x232 is 1244 against a 1196px column — so
   the hero fills the column instead and the text takes what is left, which is
   732px. A slightly shorter line is a fair price for gryphons that read.
   The body copy stays left-aligned; only the block moves, and the h1
   centres itself on the axis between the gryphons. */
.hero.flanked {
  max-width: 1196px;
  margin-inline: auto;
}
.hero.flanked .gryphon { top: 96px; }

/* The pair now needs the whole 1196px content column, which is 1240px of
   viewport once main's padding is paid. Below that they would overlap the
   pitch, and a gryphon on top of the text is worse than no gryphon. */
@media (max-width: 1239px) {
  .gryphon { display: none; }
  /* The padding only existed to hold them, so it goes with them. */
  .flanked { --flank: 0px; }
  .hero.flanked { max-width: 780px; margin-inline: 0; }
}

/* ---------- the game's own textures --------------------------------------- */
/* Every rule in this section reads a --art-* token from ui-assets.css and never
   a path, so `manage.py build_ui_assets` owns which file each one is. If the
   assets haven't been baked the token is unset, `url()` resolves to nothing, and
   each rule degrades to a plain surface rather than to a broken layout.
   Textures always go on a pseudo-element or an extra background layer, never on
   the element's own `background`, so the palette underneath still shows. */

/* No panel backgrounds here, and that is the finding rather than an omission.
   UI-PaperDoll-SlotBackground and the achievement toast were both tried as
   grounds for the big panels and both looked wrong for the same reason: they
   are fixed-size art with soft transparent edges, so tiling one across a
   responsive panel shows its seams and stretching one leaves a blob floating in
   the middle. The textures below all work because each is used at its native
   size on an element it belongs to. Large flat surfaces stay flat. */

/* UI-Common-MouseHilight, the hover glow the whole game uses. It is a soft
   32px square built to be stretched, so `100% 100%` is correct rather than
   lazy. */
.slot:hover::after, .item:hover::after, .gear-cell:hover::after {
  content: "";
  position: absolute; inset: 0;
  background: var(--art-hover) center / 100% 100% no-repeat;
  opacity: 0.22;
  border-radius: inherit;
  pointer-events: none;
}
.slot, .item, .gear-cell { position: relative; }

/* Shirt and tabard: present for symmetry, deliberately never fillable. Hatched
   rather than merely empty, so they read as "not a slot" instead of "a slot you
   failed to fill" — an empty-looking row in a finished run is a question.
   
   Scoped to `.gear-cell.is-blank` and nothing else. An earlier version of this
   block was spliced into the middle of the selector list above, which handed
   `.slot` and `.item` its `pointer-events: none` — every Loot button on the
   board stopped taking clicks, while a scripted `.click()` still worked because
   that skips hit-testing. */
.gear-cell.is-blank {
  opacity: 0.4;
  border-style: dashed;
  pointer-events: none;
}
.gear-cell.is-blank .gear-icon {
  background: repeating-linear-gradient(45deg,
    rgb(255 255 255 / 0.05) 0 4px, transparent 4px 8px);
  border-color: var(--line);
}
.gear-cell.is-blank .gear-slot { color: var(--text-faint); }

/* The drafting panel's own shirt/tabard blanks, dressed the same way. Same
   scoping caution as above: `.slot` must never inherit pointer-events: none
   from a shared selector, so this block names `.slot.is-blank` alone. */
.slot.is-blank {
  opacity: 0.4;
  border-style: dashed;
  pointer-events: none;
}
.slot.is-blank .slot-icon {
  background: repeating-linear-gradient(45deg,
    rgb(255 255 255 / 0.05) 0 4px, transparent 4px 8px);
  border-color: var(--line);
}
.slot.is-blank .slot-label { color: var(--text-faint); }

/* Carved stone relief was tried behind the slots and the item rows and cut. The
   crop worked — the panel's ornate frame dropped away cleanly and the field
   downscaled sharp — but a busy relief directly behind small text and item art
   fights both. These two surfaces stay plain; the grain on this page lives on
   the panels *around* them, which is far enough from the text to be texture
   rather than interference. */

/* A legendary, marked twice over — once where you choose it and once where you
   wear it.

   The board's mark used to be `legendaryorange64.png` on a pseudo-element, and
   it drew a plain orange square on Thunderfury. That file is not a glow: it is
   64x64, opaque in every pixel, and its 43 colours are a straight top-to-bottom
   ramp. It is a *colour swatch* the client uses as a fill source and never
   draws directly, so as a CSS background it can only ever be a rectangle. The
   box-shadow below was already doing the real work underneath it. */
.item.is-legendary .item-icon {
  position: relative;
  box-shadow: 0 0 14px 3px rgba(255, 128, 0, 0.42);
}
/* The slot's ring is genuine art, but it is drawn on **black** rather than on
   transparency — the client composites it with an additive blend, where black
   contributes nothing. Without a blend mode here it is a black square, which on
   a dark panel hid instead of looking wrong. `screen` is the CSS equivalent:
   black drops out, the ring stays. */
.slot.is-legendary .slot-icon::after {
  content: "";
  position: absolute; inset: -7px;
  background: var(--art-glow-ring) center / contain no-repeat;
  mix-blend-mode: screen;
  pointer-events: none;
}
.slot.is-legendary .slot-icon { position: relative; }

.doll { position: sticky; top: 16px; }
.doll-head {
  display: flex; align-items: center; gap: 10px;
  padding-bottom: 12px; margin-bottom: 14px;
  border-bottom: var(--border) solid var(--line);
}
/* Class colour, not gold — see the borrowed-scales note above. */
.doll-spec { font-family: var(--font-display); font-size: 18px; letter-spacing: 0.02em; }
.doll-mode {
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-dim); font-family: var(--font-body);
}

/* Two columns flanking the character with the weapons beneath is the in-game
   inspector layout. Reproducing it exactly is the point — it reads to this
   audience without a legend. */
.doll-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 14px; }
.doll-col { display: flex; flex-direction: column; gap: 6px; }
.doll-col.bottom {
  grid-column: 1 / -1;
  flex-direction: row;
  margin-top: 8px; padding-top: 12px;
  border-top: var(--border) solid var(--line-soft);
}
.doll-col.bottom .slot { flex: 1; min-width: 0; }

.slot {
  display: flex; align-items: center; gap: 9px;
  padding: 5px 7px; border-radius: var(--radius-sm);
  background: var(--surface);
  border: var(--border) solid var(--line-soft);
  min-width: 0;
}
.slot.mirror { flex-direction: row-reverse; text-align: right; }
.slot.filled { background: var(--surface-3); border-color: var(--line); }

.slot-icon {
  flex: none;
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 3px;
  background: var(--surface-2);
  /* The border carries the rarity. `--rarity` is unset on an empty slot, so
     the fallback keeps it a quiet outline rather than a coloured one. */
  border: var(--border) solid var(--rarity, var(--line-strong));
  font-family: var(--font-mono); font-size: 10px;
  color: var(--rarity, var(--text-faint));
}
.slot-body { display: flex; flex-direction: column; min-width: 0; }
.slot-label {
  font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-faint); font-family: var(--font-body);
}
.slot-item {
  font-size: 12.5px; color: var(--text-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.slot.filled .slot-item { color: var(--rarity, var(--text-body)); }

/* A pity piece is meant to be conspicuous. A complete set with two obvious
   consolation greens is a better share artifact than a set with holes — it is
   legible, slightly funny, and invites the comment you want (v2 §12.2). */
.slot.is-pity { border-style: dashed; border-color: var(--ok-line); }
.slot.is-pity .slot-item { color: var(--ok); }


/* ---------- icons and their fallback -------------------------------------- */
/* `icons.css` is generated by `manage.py build_icons` and carries one
   background-position rule per icon that made it into the sprite. Where art
   exists, `has-icon` hides the abbreviation below; where it doesn't, the item
   keeps its two mono letters in a rarity-bordered square, which reads as
   deliberate rather than broken. That's why a gap in the pack is survivable. */

/* An empty slot shows a silhouette of what belongs in it — the same thing the
   game's paperdoll does, and far more readable than a two-letter code.
   `--slot-shape` is applied as a MASK, not as a background image, which buys
   two things:

     * The SVGs carry no colour. `fill='black'` is inert — a mask reads only the
       alpha channel — so this file still defines no colours of its own and the
       silhouettes recolour with the theme for free. (Contrast --ornament-diamond
       in the design layer, which had to bake its colour in precisely because a
       background-image data URI *can't* do this.)
     * The mask lands on ::after, a child box, so the icon square's own border
       and background are untouched.

   It also composes with icons.css for nothing: that file sets `content: none`
   on every baked icon, so a filled slot shows its item and an empty one shows
   its silhouette, with no state to track anywhere.

   These are RECREATIONS, not Blizzard's art — the real placeholders live in
   Interface/PaperDoll/ inside CASC and no site serves them. `build_slot_icons`
   swaps in the genuine textures if they're ever exported. */

[data-glyph]::after {
  content: "";
  width: 22px;
  height: 22px;
  /* currentColor, so the silhouette follows the icon square's own colour rule
     — `--rarity` when something is equipped, `--text-faint` when empty. One
     token decides how prominent it is; no second opacity to keep in sync. */
  background-color: currentColor;
  -webkit-mask: var(--slot-shape) center / contain no-repeat;
          mask: var(--slot-shape) center / contain no-repeat;
}

[data-glyph="head"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20fill-rule='evenodd'%20d='M12%202C7.6%202%204%205.6%204%2010v8a2%202%200%20002%202h12a2%202%200%20002-2v-8c0-4.4-3.6-8-8-8zm-4%209h8v4H8z'/%3E%3C/svg%3E");
}
[data-glyph="neck"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M5%202a1.5%201.5%200%20013%200c0%203.6%201.8%205.5%204%205.5S16%205.6%2016%202a1.5%201.5%200%20013%200c0%204.5-2.7%207.2-5.5%208.2V12h-3v-1.8C7.7%209.2%205%206.5%205%202z'/%3E%3Ccircle%20fill='black'%20cx='12'%20cy='17'%20r='4.5'/%3E%3C/svg%3E");
}
[data-glyph="shoulder"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M1%2020v-5c0-4.4%203-8%206.5-8H10v13zm22%200v-5c0-4.4-3-8-6.5-8H14v13z'/%3E%3C/svg%3E");
}
[data-glyph="back"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20fill-rule='evenodd'%20d='M9%202h6l.5%202.5C19%206%2022%209.5%2022%2014v8H2v-8c0-4.5%203-8%206.5-9.5zm3%204l-2.5%204h5z'/%3E%3C/svg%3E");
}
[data-glyph="chest"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M9%202l3%202.5L15%202l4.6%202.3A4%204%200%200121.8%209l-1.3%205.5H18V22H6v-7.5H3.5L2.2%209a4%204%200%20012.2-4.7z'/%3E%3C/svg%3E");
}
[data-glyph="wrist"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M4%206h16a1%201%200%20011%201v3H3V7a1%201%200%20011-1zm-1%208h18v3a1%201%200%2001-1%201H4a1%201%200%2001-1-1z'/%3E%3C/svg%3E");
}
[data-glyph="hands"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M7%205a1.5%201.5%200%20013%200v5h.5V3.5a1.5%201.5%200%20013%200V10h.5V6a1.5%201.5%200%20013%200v8.5c0%204.1-2.7%207.5-6.5%207.5S4%2018.6%204%2014.5V10a1.5%201.5%200%20013%200z'/%3E%3C/svg%3E");
}
[data-glyph="waist"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20fill-rule='evenodd'%20d='M2%209h8v6H2zm12%200h8v6h-8zM9%206h6v12H9zm1.5%203.5v5h3v-5z'/%3E%3C/svg%3E");
}
[data-glyph="legs"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M4%202h16v5l-1.5%2015h-4.7L12%2011l-1.8%2011H5.5L4%207z'/%3E%3C/svg%3E");
}
[data-glyph="feet"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M6%202h6v11.5l5.6%203.6A4%204%200%200119.5%2020.5V22H6z'/%3E%3C/svg%3E");
}
[data-glyph="finger1"], [data-glyph="finger2"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20fill-rule='evenodd'%20d='M12%205.5a8%208%200%20100%2016%208%208%200%20000-16zm0%204.5a3.5%203.5%200%20110%207%203.5%203.5%200%20010-7z'/%3E%3Cpath%20fill='black'%20d='M12%201l3.5%204.5h-7z'/%3E%3C/svg%3E");
}
[data-glyph="trinket1"], [data-glyph="trinket2"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M12%201.5l9%205.5-3.4%2011.5h-11L3%207z'/%3E%3C/svg%3E");
}
[data-glyph="main_hand"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M12%201l3%204.5V15H9V5.5zM5.5%2016h13v2.6h-13zM10.5%2019.5h3V23h-3z'/%3E%3C/svg%3E");
}
[data-glyph="off_hand"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M12%201.5l9%203v7.5c0%205.2-3.7%209.6-9%2011.5-5.3-1.9-9-6.3-9-11.5V4.5z'/%3E%3C/svg%3E");
}
[data-glyph="ranged"] {
  --slot-shape: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Cpath%20fill='black'%20d='M22%202L2%209.8l8.4%203.8L14.2%2022z'/%3E%3C/svg%3E");
}


/* ---------- run: the roll reel -------------------------------------------- */
/* The board's header, permanently — not an overlay that swaps with it. It spins
   on a new roll and then stays put naming the raid and boss, which is why the
   board below prints neither. */

.board-column { display: flex; flex-direction: column; min-width: 0; gap: 10px; }

.reel {
  display: flex; flex-direction: column; gap: 9px;
  /* 15/18/14 minus the frame's 7px. */
  padding: 8px 11px 7px;
  background: var(--surface-2);
  /* Framed in its own right now, rather than borrowing the board's top border
     to fake one panel. Two carved edges meeting flush read as a seam; a small
     gap and two frames read as a header above a list, which is what these are.
     `.board-column` supplies the gap. */
  position: relative;
}
/* No boss watermark. UI-UnitFrame-Boss was tried here, desaturated, and the
   problem was not the gold — it is a unit frame, so its health bars and portrait
   socket come with it, and ghosted behind a heading it reads as a broken widget
   rather than as ornament. Art that depicts a specific piece of UI cannot be
   demoted to texture. */

/* Clicking only does something mid-spin, so only offer the affordance then. */
.reel:has(.spinning) { cursor: pointer; }

/* One height for the cell and its own window, because the reel's maths is
   exactly "scroll by one cell" — the day they disagree, the strip lands short
   and the previous name shows above the answer. That is not hypothetical; it is
   the bug this token exists to prevent recurring. The two stages carry
   different sizes now that this is a header, so each keeps its own pair. */
/* The reel is now the height of the art rather than the height of two lines of
   text: the backgrounds are 2560x360, so at column width they want ~170px and
   cropping them would throw away most of what was drawn.

   Both stages became *layers* rather than stacked rows. Each is a full-bleed
   window over the same box, the raid one carrying the background and the boss
   one transparent on top of it. That is what lets the raid's art stay put while
   the boss spins over it, which is the two-stage sequence the roll actually is.

   One height for the cell and the window, still — the strip's maths is exactly
   "scroll by one cell", and the day they disagree it lands short. */
/* The one panel that keeps the thin frame. `.reel` is a fixed-height window
   onto the raid art, so a 13px border on all four sides would eat 26px of the
   170px picture. The strip maths is unaffected either way — the window carries
   its own height — but the photograph is the point of the panel. */
.reel { border-width: 7px; }
.reel {
  --reel-cell-h: 170px;
  height: var(--reel-cell-h);
  padding: 0;
  overflow: hidden;
}
.reel-stage {
  position: absolute;
  inset: 0;
  display: block;
}
/* The RAID / BOSS captions. Absolute so they sit over the art instead of taking
   room in a flow that no longer exists. */
.reel-label {
  position: absolute;
  left: 18px;
  /* The label precedes the window in the DOM, so a positioned sibling with no
     z-index paints over it. This is what lifts the caption above the scrim. */
  z-index: 1;
  font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase;
  /* Same white as the names. `--text-faint` is the dimmest text token there is
     and it was chosen when these captions sat on a flat panel; over art it
     disappeared. At 10px with this much letter-spacing white reads as a caption
     rather than as shouting, so nothing is lost by making it legible. */
  color: var(--text-bright);
  opacity: 0.72;
  pointer-events: none;
}
.reel-stage:first-of-type .reel-label { top: calc(var(--reel-cell-h) * 0.094); }
.reel-stage:last-of-type  .reel-label { top: calc(var(--reel-cell-h) * 0.435); }

/* The scrim lives *inside* the raid cell, and it has to.
   
   The art and the name share one cell so they can never scroll out of step,
   which means no reel-wide layer can get between them — and the first version
   of this tried exactly that. A `.reel::after` at z-index 2 with the stages at
   1 and 3 painted the dark gradient straight over the raid name: `z-index` on
   `.reel-stage` makes it a stacking context, so a `z-index: 3` on a name inside
   it cannot climb past a sibling of its own ancestor. The name computed as pure
   white and rendered grey, which is a confusing way to lose an afternoon.
   
   As a pseudo-element of the cell it sits between that cell's background and
   its text, which is the only place it was ever meant to be. It still covers
   the boss stage's text too, because the boss layer floats above the raid cell
   the scrim belongs to.
   
   A left-to-right gradient rather than a flat darkening keeps the right side,
   where the portrait stands, fully visible. */
.reel-raid-cell.has-art::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    rgb(8 9 11 / 0.92) 0%,
    rgb(8 9 11 / 0.78) 34%,
    rgb(8 9 11 / 0.32) 62%,
    rgb(8 9 11 / 0.12) 100%
  );
  pointer-events: none;
}
/* Above the scrim within the cell. No z-index on the stages, so the two layers
   stack in document order — raid first, boss over it — and nothing creates a
   context that traps the text again. */
/* Absolute, not padding-driven — and this is load-bearing.
   
   The cell's border box must equal `--reel-cell-h` *exactly*, because reel.js
   steps the strip by `offsetHeight` while the window takes its height from the
   token. Vertical padding breaks that the moment the token drops below the
   padding: with `box-sizing: border-box` a border box cannot shrink below its
   own padding, so the cell keeps measuring 90px while its window becomes 24px.
   That is not hypothetical — a stale mobile override did exactly this, the
   strip travelled 8 x 90px against a 24px window, and the reel came to rest
   showing the *decoy one cell before the answer*: a wrong boss from the right
   raid, sitting above the correct boss's loot.
   
   Offsets are fractions of the token, so changing the reel's height moves the
   text with it and the cell's box is never involved. */
.reel-cell-name {
  position: absolute;
  left: 18px;
  z-index: 1;
}
.reel-raid-cell .reel-cell-name { top: calc(var(--reel-cell-h) * 0.188); }
.reel-boss-cell .reel-cell-name { top: calc(var(--reel-cell-h) * 0.529); }

/* The window is one cell tall with the strip clipped to it — that clipping is
   the entire slot-machine effect. It carries no border of its own: with global
   `box-sizing: border-box` a border here comes out of the content height and
   silently desynchronises the window from the cell. The rule lives on the stage
   instead, where it is decoration and can't touch the maths. */
.reel-window {
  height: var(--reel-cell-h);
  overflow: hidden;
  position: absolute;
  inset: 0;
}

.reel-strip { will-change: transform; }
.reel-cell {
  height: var(--reel-cell-h);
  position: relative;
  padding: 0 18px;
  font-family: var(--font-body);
  /* The raid caption. Small and spaced-out because it is the lesser of the two
     answers, but at full text colour rather than muted — over art both names
     need the contrast, and a dim raid above a bright boss read as though the
     raid had not finished resolving. */
  font-size: 13px; letter-spacing: 0.18em; text-transform: uppercase;
  line-height: 1;
  color: var(--text-bright);
  white-space: nowrap;
}
/* The raid's own background, paired with its name in the same cell so the two
   can never scroll out of step. `cover` because the panel is narrower than the
   2560px source at every realistic width. */
/* `background-image` itself is set inline per cell by reel.js, because the URL
   comes from the roster. Only the sizing belongs here. A cell without art never
   gets the class and so stays transparent — the documented fallback. */
.reel-raid-cell.has-art {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* The boss layer is transparent so the raid art shows through it. */
.reel-boss-cell {
  background: none;
  font-family: var(--font-display);
  font-size: 30px; letter-spacing: 0.02em; text-transform: none;
  color: var(--chrome-bright);
}
/* Right of the panel, held clear of the text by the scrim's light end. Sized
   off the reel height so it scales with it, and clipped rather than squashed if
   a portrait is ever not square. */
.reel-portrait {
  position: absolute;
  right: 20px; top: 50%;
  height: calc(var(--reel-cell-h) * 0.88);
  width: calc(var(--reel-cell-h) * 0.88);
  transform: translateY(-50%);
  object-fit: contain;
  pointer-events: none;
  filter: drop-shadow(0 3px 10px rgb(0 0 0 / 0.55));
}
/* Fast frames blur and resolve sharp as the strip slows. Cheap, and it is most
   of what separates a spin from a list scrolling past. */
.reel-window.spinning .reel-cell { filter: blur(1.1px); opacity: 0.86; }
.reel-window:not(.spinning) .reel-cell {
  filter: none; opacity: 1;
  transition: filter 160ms ease-out, opacity 160ms ease-out;
}
/* The raid line is the one that needed brightening. It is 13px over a
   photograph, so it takes the brightest text token; the boss name is 30px and
   already carries itself, and keeps the colour it has always had.
   Contrast here is a function of size, not of importance. */
.reel-window:not(.spinning) .reel-raid-cell { color: var(--text-bright); }
.reel-window:not(.spinning) .reel-boss-cell { color: var(--text); }

/* Only worth saying while there is something to skip. */
.reel-hint {
  position: absolute; bottom: 11px; right: 16px;
  font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-faint);
  opacity: 0; transition: opacity 120ms;
}
.reel:has(.spinning) .reel-hint { opacity: 1; }

/* A toggle that is on reads as a plate held *down*: the lit facet moves to the
   bottom and the face darkens, which is the same thing :active does. A latched
   button that just changes colour looks like a different button rather than the
   same one in a different position. */
.instant.on {
  background: linear-gradient(180deg, #23272b 0%, #2c3136 54%, #383e44 100%);
  color: var(--plate-gold-lit);
  box-shadow:
    inset 0 2px 4px rgb(0 0 0 / 0.55),
    inset 0 -1px 0 rgb(255 255 255 / 0.08);
}

/* Restart is armed by its first click, and only then does it look dangerous.
   Unarmed it stays as quiet as any other secondary control — a button that
   looks alarming all the time stops being read at all. */
.restart.armed {
  background: linear-gradient(180deg, #8e2429 0%, #ad3036 18%, #8b2126 58%, #5c1418 100%);
  border-color: var(--plate-edge);
  /* Gold, like every other crimson plate. `--accent-text` is the near-white
     that belonged to the old flat treatment and would make this the one red
     button on the site lettered differently. */
  color: var(--plate-gold);
}


/* ---------- run: the boss board ------------------------------------------- */

/* ---------- the loot window's header ------------------------------------- */
/* The client's own furniture: a title plate across the top of the loot list
   with a skull medallion hanging off its left end.

   The wrapper exists so the plate has something to overlap. It cannot be the
   board itself — `drawBoard` clears `board.innerHTML` on every roll, which is
   the same reason the reel is a sibling rather than a child. */
.loot-frame { position: relative; padding-top: 22px; }

.loot-plate {
  position: absolute;
  /* Not symmetric, deliberately. The medallion occupies the left end, so an
     equal inset on both sides leaves the bar looking short of the panel on the
     right — it reads as off-centre rather than as inset. The left keeps its
     clearance for the disc; the right runs almost to the frame. */
  top: 0; left: 46px; right: 14px;
  height: 40px;
  z-index: 2;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
  /* Built here rather than from `dialogbox-gold-header.png`, and that was a
     mistake worth recording: the file is 256x64 but its **ink is only 137x38**,
     a small ornate gold cartouche floating in a mostly empty canvas. Stretched
     across a 628px board it became a 4.6x upscale of a bright yellow plate —
     a smear, and far too saturated for a page whose whole palette is near
     black. That art is meant to be drawn at native size or not at all.
     
     The client's loot banner is dark stone with a gold rule and gold letters,
     which is a gradient and two hairlines. It also puts this in the same family
     as the button plates instead of importing a second, brighter gold. */
  /* The site's own surfaces, not a warm brown. The first version was mixed
     from the client's olive-gold banner and read as a different product bolted
     on: every other panel here is a neutral near-black (`--surface-2` #131316
     through `--surface-3` #17171b) and the only warmth in the palette is the
     red of the plates and the gold of their lettering. So the bar is built
     from those — neutral stone, the plates' own near-black rim, and a single
     gold hairline along the top to catch the light. */
  background:
    linear-gradient(180deg, var(--surface-hover) 0%, var(--surface-3) 44%, #101014 100%);
  border: 1px solid var(--plate-edge);
  border-radius: 2px;
  box-shadow:
    inset 0 1px 0 rgb(248 207 71 / 0.16),
    inset 0 -1px 0 rgb(0 0 0 / 0.55),
    0 0 0 1px rgb(255 255 255 / 0.03),
    0 2px 6px rgb(0 0 0 / 0.5);
}
.loot-plate-label {
  font-family: var(--font-display);
  font-size: 15px;
  letter-spacing: 0.14em;
  color: var(--plate-gold);
  text-shadow: var(--plate-text-shadow);
}

/* The skull, in a disc drawn here rather than shipped. The export has no
   medallion ring — `misc/goldring64.png` is a flat 64x64 with no alpha at all,
   an opaque tile rather than a ring — so a radial fill and a gold hairline do
   the job honestly and scale with the layout. */
.loot-medallion {
  position: absolute;
  left: -38px; top: -12px;
  width: 62px; height: 62px;
  border-radius: 50%;
  background:
    var(--art-dice) center / 34px 34px no-repeat,
    radial-gradient(circle at 38% 32%, #2c2c33 0%, #0f0f12 76%);
  /* A struck rim rather than a flat ring: one border colour reads as a grey
     disc with a sticker on it, which is what the first attempt looked like.
     The two inset shadows give it a lit top edge and a shadowed underside, so
     it sits in the light the rest of the chrome is lit by. */
  border: 2px solid var(--plate-edge);
  box-shadow:
    0 3px 9px rgb(0 0 0 / 0.75),
    inset 0 2px 1px rgb(233 205 138 / 0.35),
    inset 0 -3px 4px rgb(0 0 0 / 0.65),
    0 0 0 1px rgb(0 0 0 / 0.55);
}


.board {
  background: var(--surface-2);
  /* No `border` here — the carved frame near the top of this file supplies it,
     and a border declared later would silently win over it. Padding is the
     old 17/18 minus the frame, which is now its native 13 rather than 7, so
     the content keeps its room. */
  padding: 4px 5px 5px;
  min-height: 320px;
}
.board.working { opacity: 0.5; pointer-events: none; }

/* On a roll this is one line — the reel above is the header, so all that's left
   here is the instruction. The pity board still uses all three spans. */
.board-head {
  display: flex; flex-direction: column; gap: 3px;
  padding-bottom: 11px; margin-bottom: 12px;
  border-bottom: var(--border) solid var(--line);
}
.board-raid {
  font-size: 10.5px; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--chrome-quiet); font-family: var(--font-body);
}
.board-boss {
  font-family: var(--font-display); font-size: 23px; line-height: 1.2;
  letter-spacing: 0.02em; color: var(--text);
}
.board-note { font-size: 12px; color: var(--text-dim); }

.item-list { display: flex; flex-direction: column; gap: 6px; }

.item {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 11px; border-radius: var(--radius-sm);
  background: var(--surface);
  border: var(--border) solid var(--line-soft);
  border-left: 3px solid var(--rarity, var(--line-strong));
  text-align: left; width: 100%;
}
.item:hover {
  background: var(--surface-hover);
  border-color: var(--line);
  border-left-color: var(--rarity, var(--line-strong));
}
.item-icon {
  flex: none; width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 3px;
  background: var(--surface-2); border: var(--border) solid var(--rarity, var(--line-strong));
  font-family: var(--font-mono); font-size: 10px;
  color: var(--rarity, var(--text-faint));
}
.item-main { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
/* Deliberately NOT the display face. A boss board runs to twenty-five rows,
   and rarity colour has to stay the thing the eye catches — a wide small-caps
   serif at this size costs legibility and flattens the rarity signal. */
.item-name {
  font-family: var(--font-body);
  font-size: 14px; color: var(--rarity, var(--text)); font-weight: 500;
}
.item-meta {
  font-size: 12.5px;
  color: var(--item-meta);
  letter-spacing: 0.015em;
}
/* A board shows the boss's own drops and the wider raid pool together. The tag
   is the only thing separating them, so it has to be legible without being loud
   — this is context, not a warning. */
.item-tag {
  margin-left: 8px;
  padding: 1px 6px;
  border-radius: 2px;
  border: var(--border) solid var(--line-strong);
  background: var(--surface-2);
  color: var(--text-dim);
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  vertical-align: middle;
}
/* Tier reads as a find rather than as context, so this one is allowed to carry
   the chrome colour where the raid-drop tag stays muted. */
.item-tag.is-token {
  border-color: var(--chrome-line);
  color: var(--chrome);
  letter-spacing: 0.06em;
}
.item.is-raid-drop { border-left-style: dashed; }
/* The row's stat digest. Pitched at the tooltip rather than at the metadata
   above it, because it is the line the pick is actually made on: same family
   and near enough the same size, at full text colour instead of muted.

   Still one line, and that is the constraint the rest of the styling answers
   to. The tooltip can afford a stat per row because only one is ever open; a
   board shows thirteen items at once, and stacking four stats each would make
   it three times taller and push the last items off the screen.

   Deliberately not borrowing the tooltip's white-and-green split. Vertically
   that separates "what this has" from "what this does" and earns its keep;
   compressed onto one line it reads as two colours of noise. The server
   already sorts these by what they are worth (scoring.stat_order), so the
   ordering carries the meaning the colour would have. */
.item-stats {
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--text);
  letter-spacing: 0.01em;
}
/* One stat, coloured by what it is worth to your spec. The tier comes from the
   server, which is the only side that knows the weights — see
   scoring.stat_tiers. Spaced with a wider gap than a normal word space so the
   colour bands read as separate values rather than as one tinted sentence. */
.item-stats .sv { margin-right: 11px; }
.item-stats .sv:last-child { margin-right: 0; }
.item-stats .v0 { color: var(--stat-v0); }
.item-stats .v1 { color: var(--stat-v1); }
.item-stats .v2 { color: var(--stat-v2); }
.item-stats .v3 { color: var(--stat-v3); }
.item-stats .v4 { color: var(--stat-v4); font-weight: 600; }
/* The loot bag on Take, UI-GroupLoot-Pass on the rerolls: the game's own icons
   for the actions they're on, not decoration borrowed for the look.

   The bag rather than the dice, which is what used to be here. The dice is the
   client's *Need* icon, and Need is a roll against other players — in a solo
   draft there is nobody to roll against, so the gesture is looting. The pass
   hand is untouched, because rerolling a boss really is passing on it.

   One consequence: the bag is an item icon and has no `-highlight` sibling the
   way the loot-roll set does, so hover brightens it instead of swapping frames.
   Tuned to land close to where the dice's highlight sat. */
.item-take .btn::before,
.reroll::before {
  content: "";
  flex: none;
  width: 15px; height: 15px;
  /* No margin — .btn is a flex/grid container with its own gap, and adding one
     here would double the spacing on the Take buttons only. */
  background: var(--art-icon) center / contain no-repeat;
}
.item-take .btn { --art-icon: var(--art-loot-bag); }
/* The bag carries a baked 1px outline, and it was disappearing. The source is
   56px, the button draws it at 15px, so that ring scales down to 0.27px and
   vanishes — the border was there the whole time, just sub-pixel.
   
   `drop-shadow` fixes it where a re-bake cannot: filters apply *after* layout
   and scaling, so 1px here is one real pixel on screen whatever size the icon
   is drawn at. Four of them, one per direction, because a single centred
   drop-shadow is a smudge under the subject rather than a line around it.
   
   The hover brightness has to be part of the same `filter` value rather than a
   separate declaration — one property, so setting it on hover would otherwise
   drop the outline exactly when the lighter face makes it most needed. */
.item-take .btn::before {
  filter:
    drop-shadow( 1px  0    0 rgb(0 0 0 / 0.9))
    drop-shadow(-1px  0    0 rgb(0 0 0 / 0.9))
    drop-shadow( 0    1px  0 rgb(0 0 0 / 0.9))
    drop-shadow( 0   -1px  0 rgb(0 0 0 / 0.9));
  transition: filter 90ms ease-out;
}
.item-take .btn:hover::before {
  filter:
    drop-shadow( 1px  0    0 rgb(0 0 0 / 0.9))
    drop-shadow(-1px  0    0 rgb(0 0 0 / 0.9))
    drop-shadow( 0    1px  0 rgb(0 0 0 / 0.9))
    drop-shadow( 0   -1px  0 rgb(0 0 0 / 0.9))
    brightness(1.28) saturate(1.1);
}

/* The rerolls stack a name over a note, so the icon has to span both lines
   rather than sit above them the way a flex-column ::before would. */
.reroll {
  --art-icon: var(--art-pass);
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: 9px;
  justify-items: start;
}
.reroll::before { grid-row: 1 / 3; grid-column: 1; align-self: center; }
.reroll .reroll-name, .reroll .reroll-note { grid-column: 2; }
.reroll:hover:not(:disabled) { --art-icon: var(--art-pass-lit); }
/* Spent or unavailable: the icon greys out with the rest of the control rather
   than staying bright above a struck-through label. */
.reroll:disabled::before, .reroll.spent::before { opacity: 0.35; }

.item-take { display: flex; gap: 6px; flex: none; }
.item-take small { font-weight: 400; font-size: 10px; opacity: 0.8; margin-left: 5px; }
/* On a phone, two side-by-side hand buttons squeezed the item text into a
   one-word-per-line ribbon (Jeff's report). Stacked, the description keeps
   its width and a single Loot button looks exactly as it did. */
@media (max-width: 640px) {
  .item-take { flex-direction: column; align-items: stretch; }
}

.pity-group { margin-bottom: 18px; }
.pity-group h4 { margin-bottom: 8px; }
.pity-option { cursor: pointer; margin-bottom: 5px; }
.pity-option.chosen { border-color: var(--select-line); background: var(--select-soft); }


/* ---------- results ------------------------------------------------------- */

.result-card {
  background: var(--surface-2);
  /* Frame comes from the shared rule; see .board for why it isn't declared
     here. 26 - 13 keeps the inset where it was, the frame having gone from 7
     to its native 13. */
  padding: 13px;
  margin-bottom: 18px;
}
.result-headline {
  display: flex; align-items: center; gap: 32px; flex-wrap: wrap;
  padding-bottom: 24px; margin-bottom: 22px;
  border-bottom: var(--border) solid var(--line);
}
.result-facts { flex: 1; min-width: 300px; }
.result-mode {
  display: flex; align-items: center; gap: 9px;
  /* Wrap + centre: on a phone the centre column gets too narrow for one
     line, and without these the spec name ran off the card edge. */
  flex-wrap: wrap; justify-content: center;
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--text-dim); margin-bottom: 12px; font-family: var(--font-body);
}

/* The parse number, huge and colour-coded, is the whole share artifact
   (§12.2). The colour is the flex, so it comes from the borrowed scale rather
   than from the palette. */
.parse { display: flex; flex-direction: column; align-items: center; }

/* No achievement shield behind the number any more — the gryphons took that
   job. Three pieces of art around one number is a crowd, and a carved pair
   flanking it says "trophy" better than a badge behind it does. */
.parse { position: relative; }
.parse-value, .parse-label { position: relative; z-index: 1; }

/* And the bling, only above 95. It keeps its colour — an orange or pink parse
   is the one moment on this page that has earned it. */
.parse.is-rare::after {
  content: "";
  position: absolute;
  /* `inset: 0` plus `margin: auto` is what centres it. With a width and height
     but no offsets it lands at its auto flow position — under the number and
     off to one side — which is not a subtle wrongness once you see it. Centring
     this way rather than with a translate leaves `transform` free for the
     animation below, which would otherwise overwrite it on the first frame. */
  inset: 0;
  margin: auto;
  /* Scales with the viewport, and it has to. Fixed at 300px this frame is wider
     than a phone's card: measured at 375px it centred on a `.parse` box only
     82px wide and hung **83px off the left edge**, so what a player actually
     saw was a gold flourish sliced by the card border. The number drops to 62px
     on mobile and this stayed at 300, which also made the halo 4.8x the number
     against 3.6x on desktop — oversized exactly where there is least room.
     
     `min()` keeps the desktop size and lets narrow screens take the smaller of
     the two, with no breakpoint to keep in step with the font size. */
  width: min(300px, 58vw); height: min(300px, 58vw);
  background: var(--art-badge-bling) center / contain no-repeat;
  opacity: 0.42;
  pointer-events: none;
  z-index: 0;
  animation: bling 2.4s ease-out 0.2s both;
}
@keyframes bling {
  0%   { opacity: 0;    transform: scale(0.55) rotate(-25deg); }
  40%  { opacity: 0.62; }
  100% { opacity: 0.28; transform: scale(1) rotate(0deg); }
}
/* A burst that spins in is celebration for most people and a problem for some.
   The parse still gets its shield and its colour. */
@media (prefers-reduced-motion: reduce) {
  .parse.is-rare::after { animation: none; opacity: 0.32; }
}

/* The achievement toast was tried as a ground for this strip and cut for the
   same reason as the others: it is a 512x128 panel with its own shaped edges, so
   stretched to an arbitrary width it stops being a texture and becomes a smear.
   The shield above works because it is symmetric, sized in px, and sits behind
   one number. */
/* Set like a Warcraft Logs parse, not like the rest of this site: heavy
   grotesque, tight, no serif. The number is borrowed from their report exactly
   as the colour bands are, and it stops reading as a parse the moment it is
   set in the page's own face. */
.parse-value {
  font-family: var(--font-parse);
  font-size: 84px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  /* Tabular figures so the number doesn't jump width between a 9 and a 91.4 —
     it sits between two gryphons, and a centred thing that shifts is worse than
     one that is slightly off-centre. */
  font-variant-numeric: tabular-nums;
  color: var(--parse);
}
.parse-label {
  font-size: 10.5px; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--text-dim); margin-top: 10px; font-family: var(--font-body);
}
.parse-grey   { --parse: var(--parse-grey); }
.parse-green  { --parse: var(--parse-green); }
.parse-blue   { --parse: var(--parse-blue); }
.parse-purple { --parse: var(--parse-purple); }
.parse-orange { --parse: var(--parse-orange); }
.parse-pink   { --parse: var(--parse-pink); }
.parse-gold   { --parse: var(--parse-gold); }
.parse-none   { --parse: var(--text-faint); }

/* A perfect run gets the only animated number on the site.
   
   It is the one score that cannot be beaten, so it is the one place a glow is
   not decoration — every other band says "how you did", gold says "nobody did
   better". Reserved that hard on purpose: an effect that fires often stops
   reading as an achievement, and a 100 is rare enough that most players will
   never see this.
   
   Two shadows breathing rather than a filter: `text-shadow` composites behind
   the glyphs and leaves the number itself crisp, where `filter: drop-shadow`
   would soften the digits it is meant to celebrate. */
.parse-gold .parse-value {
  animation: parse-perfect 2.6s ease-in-out infinite;
}
@keyframes parse-perfect {
  0%, 100% {
    text-shadow:
      0 0 14px rgb(229 204 128 / 0.45),
      0 0 34px rgb(229 204 128 / 0.20);
  }
  50% {
    text-shadow:
      0 0 22px rgb(229 204 128 / 0.75),
      0 0 54px rgb(229 204 128 / 0.38);
  }
}
/* Same rule as everywhere else on this page: motion is a garnish, and a
   permanent pulse is exactly the kind of thing that makes a page unusable for
   someone who asked for less of it. The gold stays; only the breathing goes. */
@media (prefers-reduced-motion: reduce) {
  .parse-gold .parse-value {
    animation: none;
    text-shadow: 0 0 18px rgb(229 204 128 / 0.55);
  }
}

/* The character sheet totals, between the headline and the gear list. Sits in
   the `surface-logs` scope with everything else on this card, so it reads as
   another row of the report rather than as a panel that wandered in. */
.sheet {
  grid-area: sheet;
  display: flex; flex-wrap: wrap; gap: 10px 26px;
  /* Centred, because it describes the character above it rather than
     introducing a list below it. */
  justify-content: center;
  padding: 16px 2px 4px; margin: 16px 0 0;
  border-top: var(--border) solid var(--line);
}
.sheet-stat { display: flex; flex-direction: column; gap: 2px; min-width: 74px; }

/* The run screen's copy, under the paper doll. Tighter than the results one —
   it lives beside the gear rather than in a report — and it carries the cap
   note, which is the reason it exists at all. */
.doll-sheet {
  gap: 8px 18px;
  padding: 12px 2px 2px;
  margin-bottom: 0;
  border-bottom: 0;
}
.doll-sheet .sheet-value { font-size: 16px; }
.sheet-cap {
  font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--accent-quiet); font-family: var(--font-body);
}
/* The cap target sits in the label so the row reads left to right as three
   plain numbers: what the cap is, what you have, what's left. */
.doll-sheet .sheet-label b {
  font-weight: 600;
  color: var(--chrome-quiet);
}
/* Capped is the good state, so it reads as done rather than as a warning. */
.sheet-stat.is-capped .sheet-value { color: var(--ok); }
.sheet-cap.is-capped { color: var(--ok); }
.sheet-value {
  font-family: var(--font-numeric); font-size: 19px; line-height: 1;
  color: var(--text); font-variant-numeric: tabular-nums;
}
.sheet-label {
  font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-dim); font-family: var(--font-body);
}

/* The character panel. Three columns — gear, the parse, gear — with the
   weapons on a row of their own beneath, spanning all three.

   `minmax(0, …)` on every track, not `1fr` alone: a track's default minimum is
   its content, so a long item name would push its column wider and shove the
   parse off centre. The centre gets the larger share because it carries the
   number, and the columns only need room for an icon and a name.

   The old flat grid lived here and is gone; nothing it showed is missing, only
   rearranged. */
/* The player's name, over the character the way the client shows it. Reserved
   now even though accounts do not exist: added later it would push the parse
   down and every screenshot taken before then would stop matching. */
.doll-name {
  margin: 0;
  font-family: var(--font-display);
  font-size: 26px; line-height: 1.2;
  color: var(--text-bright);
  text-align: center;
  word-break: break-word;
}
/* Until there is a name to show. Quiet enough to read as absent rather than as
   somebody actually called this. */
.doll-name.is-unclaimed {
  color: var(--text-faint);
  font-size: 22px;
  font-style: italic;
}

.gear-doll {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr) minmax(0, 1fr);
  /* The totals are the last row, under the weapons. They sum the whole
     character — the weapons included — so sitting between the gear and the
     weapons put a total above half of what it was totalling. */
  grid-template-areas:
    "left   centre right"
    "bottom bottom bottom"
    "sheet  sheet  sheet";
  gap: 6px 18px;
  align-items: start;
}
.gear-col { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.gear-col.left   { grid-area: left; }
.gear-col.right  { grid-area: right; }
/* The weapon row: centred under the panel and never wider than it needs, so it
   reads as the three slots it is rather than as a third column of gear. */
.gear-col.bottom {
  grid-area: bottom;
  flex-direction: row; justify-content: center; flex-wrap: wrap;
  margin-top: 14px;
}
.gear-col.bottom .gear-cell { flex: 0 1 236px; }

/* The centre stack: mode line, the parse with its gryphons, then the three
   tiles. Everything that used to sit in `.result-headline` beside the number,
   now underneath it. */
.gear-centre {
  grid-area: centre;
  display: flex; flex-direction: column; align-items: center;
  gap: 14px; min-width: 0;
  padding: 4px 0 10px;
}
/* Stacked, not three across. The centre column is narrow between two columns of
   gear, and three tiles side by side there squeeze their numbers to nothing —
   the number is the point of a tile. */
/* Smaller here than on the home page, and held inside the centre track.
   
   `--flank` has to be set on `.flanked` and not on the gryphon: the flank is
   made by `padding-inline: var(--flank)` on the *parent*, so overriding the
   variable on the child changed nothing at all. Left at its 232px default the
   parse block came out 555px wide inside a 405px column, which is what put a
   gryphon 68px into the gear on each side.
   
   Zero flank plus full width instead: the block is exactly the centre column,
   and the gryphons sit on its edges rather than beyond them. No padding means
   the number's width no longer moves them, so a three-digit parse cannot shove
   them outward either. */
.gear-centre .parse.flanked {
  --flank: 0px;
  width: 100%;
}
.gear-centre .gryphon { width: 124px; }

.gear-centre .grid.cols-3,
.gear-centre .grid.cols-4 {
  width: 100%;
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
}
.gear-centre .lede { text-align: center; margin: 0; }
.gear-cell {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 11px; border-radius: var(--radius-sm);
  background: var(--surface);
  border: var(--border) solid var(--line-soft);
  border-left: 3px solid var(--rarity, var(--line-strong));
}
.gear-icon {
  flex: none; width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 3px;
  background: var(--surface-2); border: var(--border) solid var(--rarity, var(--line-strong));
  font-family: var(--font-mono); font-size: 10px;
  color: var(--rarity, var(--text-faint));
}
.gear-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.gear-slot {
  font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-faint); font-family: var(--font-body);
}
.gear-item {
  font-size: 12.5px; color: var(--rarity, var(--text-dim));
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gear-cell.is-pity { border-left-color: var(--ok-line); border-style: dashed; }
.gear-cell.is-pity .gear-item { color: var(--ok); }

/* Names what the two buttons under it are for. Its rule is the divider between
   replaying and sharing — the panel already has one above Draft Again, and
   these two lines are what turn a column of three controls into two decisions. */
.showoff-label {
  margin: 26px 0 0;
  padding-top: 20px;
  border-top: var(--border) solid var(--chrome-line);
  text-align: center;
  font-family: var(--font-body);
  /* Louder than it was. At 12px in `--text-dim` it was quieter than the two
     buttons it introduces, which made it read as a caption on them rather than
     as the heading of a section. Same face, same caps, same tracking — it is
     the same kind of label, just audible. */
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-bright);
}

/* The two share buttons, sized rather than coloured.
   
   Gold faces were tried here and taken off: they caught the eye at the cost of
   sitting quietly with everything else, and the stone plate every other
   secondary button wears reads cleaner. Their hover overrides went with them —
   those existed only because `.btn:hover` turns text light gold, which on a
   gold face was gold on gold. With the plate back, the base hover is correct.
   
   So hierarchy is said with size instead. Copy link is the action worth taking
   and is the larger of the two; both are bigger than the 37px they started at.
   
   Height is pinned and shared; only width and type size differ. Two buttons side
   by side at 51px and 46px do not read as hierarchy, they read as one of them
   having gone wrong — measured, that is exactly what differing padding gave.
   `min-height` also settles a difference that has bitten here before: Copy link
   is a `<button>` and takes the UA's `line-height: normal` while Save image is
   an `<a>` that inherits the body's, so identical padding rendered 46px against
   50px. The `line-height` pin stays as the belt to that brace.
   
   52px against the 37px they started at, and under Draft Again's 60px, which is
   the order these three should read in. */
.showoff-bar .btn { line-height: 1.4; min-height: 52px; }
#grab-link { padding: 12px 34px; font-size: 15px; }
#save-card { padding: 12px 26px; font-size: 14px; }

/* The share bar, centred beneath Draft Again rather than ranged left above it.
   Sharing is the thing we most want to happen, but drafting again is the thing
   every player does, so it takes the big plate and these two sit under it as
   the quieter pair. */
.showoff-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  justify-content: center;
  margin: 12px 0 0;
}

/* The replay button, and the largest thing on the page after the parse. */
.result-actions {
  display: flex; justify-content: center; gap: 10px;
  margin: 26px 0 0;
  padding-top: 22px;
  border-top: var(--border) solid var(--chrome-line);
}
.btn.draft-again {
  font-size: 17px;
  padding: 15px 46px;
  min-width: 260px;
  justify-content: center;
  letter-spacing: 0.04em;
}
.showoff-said {
  font-family: var(--font-numeric);
  font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--quality-uncommon, #1eff00);
  opacity: 0; transition: opacity 180ms ease-out;
}
.showoff-said.on { opacity: 1; }



/* ---------- small screens ------------------------------------------------- */

@media (max-width: 1000px) {
  .run-layout { grid-template-columns: 1fr; }
  .doll { position: static; }
}
@media (max-width: 640px) {
  header.app { gap: 10px; padding: 8px 14px; }
  main { padding: 18px 14px 40px; }
  .hero h1 { font-size: 24px; }
  /* Cinzel is wide. Without this the section rules squeeze to nothing and the
     diamonds collide with the text. */
  main > h2 { gap: 10px; }
}
@media (max-width: 560px) {
  .doll-grid { grid-template-columns: 1fr; }
  .slot.mirror { flex-direction: row; text-align: left; }
  .parse-value { font-size: 62px; }
  /* Centred once the headline wraps. `.result-headline` is a flex row of the
     number and the facts; below this width they stop fitting side by side and
     the number ends up alone on its line, hard against the left edge — which
     is both odd on its own and the reason the achievement frame behind it had
     nowhere to go. Centring gives the frame its room back. */
  .result-headline { gap: 18px; justify-content: center; }
  .board-boss { font-size: 20px; }
  /* The reel window clips rather than wraps, so a name that doesn't fit is a
     name you can't read. "Golemagg the Incinerator" needs this. */
  .reel-stage:last-of-type .reel-cell { font-size: 18px; }
  /* Shorter on a phone — but set on `.reel`, so both stages shrink together and
     every cell still measures exactly one window. Setting it per-stage is the
     bug described at `.reel-cell-name`; setting padding on `.reel` is the other
     half of it, because `height` here is a border box and 25px of padding left
     both 170px windows inside a 145px frame. Neither belongs in this block. */
  .reel { --reel-cell-h: 140px; }
  /* Give the four run controls their own line, and let them wrap inside it.
     
     `.rerolls` carries `flex: none` on desktop so the tier pills cannot shove
     it onto a second line — measured, that took the bar from 79px to 147px.
     The cost on a phone was that the group could neither shrink nor break: it
     rendered 434px wide inside a 347px bar and simply ran off the right edge,
     taking Restart with it.
     
     `flex-wrap` alone does not fix that, which is worth stating because it
     looks like it should. `flex: none` is `flex-basis: auto`, so the group is
     sized to max-content — it wraps against its own natural width, finds
     everything already fits, and stays 434px. A `100%` basis is what actually
     holds it to the bar; only then does the wrap have an edge to break at. */
  .rerolls { flex: 1 1 100%; flex-wrap: wrap; justify-content: center; }
  /* The tier pills get their own row too, for the same reason and with the same
     fix. Sharing a line with the two counters left the track 79px wide, so all
     three set names truncated to an ellipsis apiece — the information was there
     and unreadable. Full width, and they fit.
     `.spacer` has no job once everything is on its own row; leaving it growing
     would just re-take the width this is trying to give back. */
  .tier-track { flex: 1 1 100%; }
  .run-bar .spacer { display: none; }
}

/* ---------- breaking news ------------------------------------------------- */
/* The announcement box from WoW's login screen: a dark panel with a thin
   grey double-line border, gold header, small grey-white body. It floats in
   the empty stone to the left of the content column, which only exists on
   wide screens -- below 1500px it flows to the top of the column instead,
   because overlapping the spec cards would be worse than being ordinary. */
.breaking-news {
  /* Anchors the dismiss button; the >=1800px float swaps this for `fixed`,
     which anchors it just the same. */
  position: relative;
  background: rgba(8, 8, 12, .92);
  border: 1px solid #6a6a72;
  outline: 1px solid #26262c;
  outline-offset: -4px;
  border-radius: 3px;
  /* Roomier than launch (12px/360px): the 1.6 announcement is a real
     paragraph or three, not one maintenance line. */
  padding: 16px 22px 18px;
  max-width: 420px;
  box-shadow: 0 0 18px rgba(0, 0, 0, .55);
}
/* The quiet x. It sits over the box's own corner chrome, so it stays a
   glyph, not a plate -- a button frame up there would fight the title. */
.breaking-news-close {
  position: absolute; top: 5px; right: 7px;
  padding: 2px 6px;
  background: none; border: none; cursor: pointer;
  font-size: 18px; line-height: 1;
  color: #8a857a;
}
.breaking-news-close:hover { color: var(--text-bright); }
.breaking-news-title {
  margin: 0 0 6px;
  font-size: 1.05em;
  font-weight: 600;
  color: #ffd100;              /* WoW's headline gold, not the site parse gold */
  letter-spacing: .02em;
}
/* Roomier than the .82em it launched at -- Sosa: "a bit cramped". */
.breaking-news-body { font-size: .92em; line-height: 1.65; color: #cfcabe; }
.breaking-news-body p { margin: 0 0 .7em; }
.breaking-news-body p:last-child { margin-bottom: 0; }

/* The gap left of the 1240px content column is (100vw - 1240px) / 2 wide, so
   its midpoint sits (100vw - 1240px) / 4 from the viewport edge -- the box is
   pinned there and pulled back by half its own width, which is what "centred
   in the empty space" means at any monitor width. Width follows the gap up to
   a cap, so a modest desktop gets a slimmer box rather than an overlap, and
   the float only engages once the gap can actually hold one. */
@media (min-width: 1800px) {
  .breaking-news {
    position: fixed;
    left: calc((100vw - 1240px) / 4);
    transform: translateX(-50%);
    top: 140px;
    width: min(360px, calc((100vw - 1240px) / 2 - 56px));
    z-index: 5;
  }
}
@media (max-width: 1799.98px) {
  /* `main` can lay its children out as a flex column, where horizontal auto
     margins on a block are ignored -- align-self is the flex-world spelling
     of the same intent, and both are stated so the box is centred whichever
     model the container uses. */
  .breaking-news {
    margin: 18px auto 0;
    align-self: center;
    width: 100%;
    max-width: 560px;
  }
}

/* The front page feedback box (Sosa, 3 Sep) mirrors the announcement on
   the right: pinned in the empty stone right of the column, under the
   account pills, once a monitor is wide enough to hold it; below that it
   sits at the bottom of the page where the markup puts it. */
.home-ask { margin: 26px auto 0; }
@media (min-width: 1800px) {
  .home-ask {
    position: fixed;
    right: calc((100vw - 1240px) / 4);
    transform: translateX(50%);
    top: 140px;
    margin: 0;
    width: min(360px, calc((100vw - 1240px) / 2 - 56px));
    z-index: 5;
  }
}

/* ---------- header auth ---------------------------------------------------- */
/* Sign in is the one *action* in a nav of links, so it gets the chip frame
   the mode tags use: quiet border, gold text, no fill until hover. Sized and
   cased to sit beside How to play / About without shouting over them. */
/* The one true action in the header wears the same crimson plate as every
   other primary button on the site, sized down to nav scale. It should read
   as pressable from across the room -- Sosa's note, and he was right, the
   bordered ghost read as another link. */
header.app .auth-signin {
  padding: 7px 16px;
  font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--plate-gold);
  font-family: var(--font-display);
  text-shadow: var(--plate-text-shadow);
  background: linear-gradient(180deg, #8e2429 0%, #ad3036 18%, #8b2126 58%, #5c1418 100%);
  border: 2px solid var(--plate-edge);
  border-radius: 3px;
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 0.20),
    inset 0 -2px 3px rgb(0 0 0 / 0.45);
  transition: background 0.12s ease, color 0.12s ease;
}
header.app .auth-signin:hover {
  background: linear-gradient(180deg, #a32d33 0%, #c53b42 18%, #9e2a30 58%, #6c191e 100%);
  color: var(--plate-gold-lit);
}

/* Signed in: just the name, dressed exactly like the util links beside it
   but in the chrome-bright gold so the state reads at a glance. It links to
   the account page, which is where sign out lives. */
/* The two personal chips, kept together so they read as a pair and wrap
   as one. */
header.app .auth-pair { display: inline-flex; align-items: stretch; gap: 6px; }

header.app .auth-name {
  display: inline-flex; align-items: center;
  padding: 6px 11px;
  font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase;
  font-family: var(--font-body);
  color: var(--chrome-bright);
  border: 1px solid var(--chrome-line);
  border-radius: var(--radius-sm);
  background: rgb(0 0 0 / 0.22);
  white-space: nowrap;
  transition: color 0.12s ease;
}
header.app .auth-name:hover {
  color: var(--text-bright); border-color: var(--chrome-bright);
}
/* The name carries the width limit, NOT the chip -- see base.html. */
.auth-name-text {
  max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The collection, beside the profile. Same plate, and on a narrow header it
   keeps the mark and drops the word rather than shrinking both. */
header.app .auth-badges {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 11px;
  font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase;
  font-family: var(--font-body);
  color: var(--chrome-bright);
  border: 1px solid var(--chrome-line);
  border-radius: var(--radius-sm);
  background: rgb(0 0 0 / 0.22);
  white-space: nowrap;
}
header.app .auth-badges:hover {
  color: var(--text-bright); border-color: var(--chrome-bright);
}
.auth-badges-glyph { font-size: 13px; line-height: 1; color: #e5cc80; }
@media (max-width: 900px) {
  .auth-badges-text { display: none; }
  header.app .auth-badges { padding: 6px 9px; }
  .auth-name-text { max-width: 110px; }
}
/* Sign out on the account page, quiet on purpose. */
.auth-out {
  background: none; border: none; padding: 0; cursor: pointer;
  font-size: 12px; letter-spacing: 0.06em;
  color: var(--text-muted); text-decoration: underline;
}
.auth-out:hover { color: var(--text-bright); }


/* ---------- login page ----------------------------------------------------- */
/* The Discord button in Discord's own clothes: blurple, pill-cornered, mark
   plus words. Everything else on this site dresses in the site's theme; this
   one deliberately does not, because a sign-in button people already know
   converts better than one they must read. */
.discord-btn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 11px 22px;
  background: #5865F2;
  color: #fff;
  font-size: 15px; font-weight: 600;
  font-family: var(--font-body);
  border-radius: 6px;
  border: none;
  transition: background 0.12s ease;
}
.discord-btn:hover { background: #4752c4; color: #fff; }
/* It became a <button> when the invite code moved beside it, so the two
   things a button brings and an anchor does not have to be undone. */
button.discord-btn { cursor: pointer; line-height: normal; }

/* Button and code on one line, wrapping to two when there is no room
   rather than squeezing the code field down to nothing. */
.discord-row {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 14px; margin: 0 0 4px;
}
.discord-ref { display: flex; flex-direction: column; gap: 3px; }
.discord-ref label {
  color: var(--text-muted); font-size: 12px; letter-spacing: 0.04em;
}
.discord-ref input {
  width: 130px; padding: 8px 10px;
  background: #14120e; color: var(--text-bright);
  border: 1px solid var(--chrome-line); border-radius: 3px;
  font-family: var(--font-numeric); letter-spacing: 0.06em;
}
.discord-ref input:focus { outline: none; border-color: var(--chrome-bright); }
.discord-ref-hint { color: var(--text-faint); font-size: 11px; }
.discord-btn svg { width: 22px; height: 17px; flex: none; }

.auth-form { max-width: 340px; }
.auth-form label { display: block; margin: 12px 0 4px; color: var(--text-muted); font-size: 13px; }
.auth-form input {
  width: 100%; padding: 9px 10px;
  background: #14120e; color: var(--text-bright);
  border: 1px solid var(--chrome-line); border-radius: 3px;
}
.auth-form input:focus { outline: none; border-color: var(--chrome-bright); }
.auth-form .auth-error { color: #d9534f; margin-top: 10px; font-size: 13px; }
/* The red plate. Shared rather than copied: sign out wears the same button
   as every other submit on the site (Sosa, 30 Aug), and one gradient in one
   place is what keeps them from drifting apart. */
.auth-form button[type="submit"],
.profile-signout .auth-out {
  padding: 9px 22px;
  color: var(--plate-gold);
  font-family: var(--font-display); letter-spacing: 0.06em;
  text-shadow: var(--plate-text-shadow);
  background: linear-gradient(180deg, #8e2429 0%, #ad3036 18%, #8b2126 58%, #5c1418 100%);
  border: 2px solid var(--plate-edge); border-radius: 3px;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.20), inset 0 -2px 3px rgb(0 0 0 / 0.45);
  cursor: pointer;
}
.auth-form button[type="submit"] { margin-top: 16px; }
.auth-form button[type="submit"]:hover,
.profile-signout .auth-out:hover {
  background: linear-gradient(180deg, #a32d33 0%, #c53b42 18%, #9e2a30 58%, #6c191e 100%);
}
.auth-divider { display: flex; align-items: center; gap: 12px; margin: 22px 0; color: var(--text-faint); max-width: 340px; }
.auth-divider::before, .auth-divider::after { content: ""; flex: 1; border-top: 1px solid var(--chrome-line); }


/* ---------- profile (account page) ----------------------------------------- */
/* The stats read like the results page's sheet: same spec icons, same class
   colours, same parse palette. Settings sit under a divider so the page leads
   with what the player did, not with forms. */
.profile { position: relative; }
.profile-signout { position: absolute; top: 18px; right: 22px; margin: 0; }
/* .auth-out is a quiet underlined link everywhere else -- Discord disconnect,
   "No, skip this" on the invite page -- and those stay quiet. Only the
   profile's sign out is promoted, so the overrides are scoped to it. */
.profile-signout .auth-out {
  text-decoration: none; font-size: 12px;
  text-transform: lowercase;
}
.profile-name {
  margin: 0 0 4px;
  /* The sign out button is absolutely positioned over this corner, and a
     20-character username is wide enough to run underneath it. The name
     clips with an ellipsis before it gets there. */
  padding-right: 120px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.profile-meta {
  margin: 0 0 18px; padding-right: 120px;
  color: var(--text-muted); font-size: 13px;
}
.profile-specs {
  width: 100%; max-width: 560px;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.profile-specs th {
  text-align: right; padding: 6px 10px;
  font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: normal; color: var(--text-dim);
  border-bottom: 1px solid var(--chrome-line);
}
.profile-specs td {
  text-align: right; padding: 8px 10px;
  border-bottom: 1px solid rgb(255 255 255 / 0.05);
  color: var(--text-bright);
}
.profile-specs .spec-col { text-align: left; padding-left: 0; }
.profile-specs td.spec-col { white-space: nowrap; }
.profile-specs .spec-icon { vertical-align: -4px; margin-right: 7px; }
.profile-main-tag {
  margin-left: 8px; padding: 2px 7px;
  font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--chrome-bright);
  border: 1px solid var(--chrome-line); border-radius: 999px;
}
/* parse-<colour> supplies --parse; here the number itself wears it. */
.profile-parse { color: var(--parse); font-weight: 600; }


/* ---------- leaderboard ---------------------------------------------------- */
/* Same sheet idiom as the profile table; the board just has more rows and a
   filter rail. `is-mine` marks the viewer's rows; `is-pinned` is their best
   run shown below the fold when it missed the visible top. */
.board-sub { margin: 0 0 14px; color: var(--text-muted); font-size: 13px; max-width: 560px; }
/* The refresh note under the window line (Sosa, 9 Sep): a fresh run can
   take one refresh window to appear, and players were reading that as
   "my run didn't count". */
.board-refresh { margin: -6px 0 14px; color: var(--text-dim); font-size: 12px; max-width: 560px; }
.board-note { color: var(--chrome-bright); font-size: 13px; }
.board-filters {
  display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0 18px;
}
/* The window tabs sit directly above the spec chips; pulling their shared
   margins together makes the two rows read as one control cluster. */
.board-windows { margin-bottom: 0; }
.board-windows + .board-filters { margin-top: 8px; }
/* The mode row (Sosa, 9 Sep): Standard and Daily are two boards, so their
   switch is bigger than a window tab and sits above the windows. The plate
   is the chip plate from parse-theme.css; only the size differs. */
.board-modes { margin: 16px 0 0; gap: 10px; }
.board-modes a {
  justify-content: center; min-width: 132px; padding: 9px 22px;
  font-family: var(--font-display); font-size: 14px; letter-spacing: 0.08em;
  text-transform: uppercase;
}
.board-modes + .board-filters { margin-top: 10px; }
.board-modes + .board-day-nav { margin-top: 14px; }
.board-month-nav .board-day-current { font-size: 15px; }
/* The front page's day picker (1.17): the leaderboard's day nav, sat
   between the class boxes and the spec panels. */
.home-day-nav { margin: 18px 0 2px; }
/* Play this day (1.17): the supporter row under a closed day's nav. */
.board-play-past { margin: 10px 0 4px; }
.board-play-past .board-note { margin-bottom: 6px; }
/* Layout only: the chip's plate lives in parse-theme.css with the other
   plates, per the two-layer contract (Sosa, 1 Sep: "make all of these
   buttons match the button layouts of the rest of the website"). */
.board-filters a {
  display: inline-flex; align-items: center; gap: 6px;
}
.board-filters .spec-icon { vertical-align: -3px; }
/* ---- guide pages (1.18) ------------------------------------------------- */
/* Spec guides, raid loot pages, the glossary. The tables are the board's
   table with the text columns left-aligned; an item reuses the results
   page's icon and rarity classes so a name is painted the same everywhere
   on the site. No numbers on these pages beyond a set total, by design. */
.guide-crumbs { font-size: 12px; margin: 0 0 6px; }
.guide-crumbs a { color: var(--text-dim); }
.guide-title { display: flex; align-items: center; gap: 10px; }
.guide-sub { color: var(--text-dim); margin-top: -2px; }
/* On both classes: the board's own `th`/`td` rules sit later in this
   file and outgun a bare `.guide-table`, which is how the raid tables
   first came out right-aligned. */
.board-table.guide-table th, .board-table.guide-table td { text-align: left; }
.board-table.guide-table th.num, .board-table.guide-table td.num { text-align: right; }
.board-table.guide-table td { font-size: 13px; vertical-align: top; }
.guide-item {
  display: inline-flex; align-items: center; gap: 8px;
  color: var(--rarity, var(--text-bright));
}
/* Painted here because icons.css paints only the run and results page
   cells (`.gear-cell.filled .gear-icon`); the sprite is 32px cells, so
   the guide icon keeps the results page's size rather than scaling it. */
.guide-item .gear-icon {
  background-image: var(--icon-sheet); background-size: 768px auto;
  background-position: var(--icon-pos, -192px -640px); background-repeat: no-repeat;
}
.guide-source { color: var(--text-dim); font-size: 12px; }
.guide-tag, .guide-bucket {
  font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-dim); margin-left: 4px;
}
.guide-tag { color: var(--plate-gold); }
.guide-bucket.is-core { color: var(--plate-gold); }
.guide-bucket.is-useful { color: var(--text); }
.guide-list, .guide-stats { padding-left: 22px; margin: 4px 0 14px; }
.guide-list li, .guide-stats li { margin: 4px 0; }
.guide-chips { display: inline-flex; flex-wrap: wrap; gap: 4px 10px; }
.guide-chip {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11.5px; color: var(--text-dim); white-space: nowrap;
}
.guide-chip.is-best { color: var(--plate-gold); }
.guide-chip .spec-icon { width: 14px; height: 14px; }
.guide-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 10px; margin: 6px 0 18px;
}
.guide-card {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  background: var(--surface-2); border: var(--border) solid var(--line);
  border-radius: var(--radius); text-decoration: none; color: var(--text-bright);
}
.guide-card:hover { border-color: var(--chrome-line); }
.guide-card b { display: block; font-weight: 600; }
.guide-card small { display: block; color: var(--text-dim); }
.guide-boss-list {
  display: flex; flex-wrap: wrap; gap: 4px 14px;
  padding: 0; margin: 4px 0 8px; list-style: none; font-size: 13px;
}
.guide-legendary { color: var(--q-legendary); font-size: 13px; }
.guide-links a { white-space: nowrap; }
/* The BIS set as the results page's paper doll (Sosa, 17 Sep: the
   table "could look a bit better"): the same cells, the two columns and
   the weapon row, no centre, and where each piece drops as a third
   line. Names may wrap here; a guide is read, not scanned. */
.guide-doll {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-areas: "left right" "bottom bottom";
  margin: 10px 0 18px;
}
.guide-doll .gear-item { white-space: normal; }
.guide-doll .gear-source { font-size: 10.5px; color: var(--text-dim); }
.guide-doll .gear-cell.is-twohand { opacity: 0.6; }
.guide-doll .gear-cell.is-twohand .gear-item { color: var(--text-faint); font-style: italic; }
@media (max-width: 560px) {
  .guide-doll { grid-template-columns: minmax(0, 1fr); grid-template-areas: "left" "right" "bottom"; }
}
/* The other-spec links (Jeff, 17 Sep): icon and class colour, the
   colour on a span inside the link because the prose link colour is
   set on the anchor itself. */
.guide-spec-links { display: flex; flex-wrap: wrap; gap: 6px 18px; }
.guide-spec-link { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; }
.guide-spec-link .spec-icon { width: 18px; height: 18px; }
.guide-spec-link:hover span { text-decoration: underline; }
.glossary dt { font-weight: 600; color: var(--text-bright); margin-top: 12px; }
.glossary dd { margin: 2px 0 0; }

.board-table {
  width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums;
}
.board-table th {
  text-align: right; padding: 6px 10px;
  font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: normal; color: var(--text-dim);
  border-bottom: 1px solid var(--chrome-line);
}
.board-table td {
  text-align: right; padding: 7px 10px;
  border-bottom: 1px solid rgb(255 255 255 / 0.05);
  color: var(--text-bright);
}
.board-table .spec-col { text-align: left; }
.board-table .spec-icon { vertical-align: -3px; margin-right: 6px; }
.board-table .board-dim { color: var(--text-dim); }
/* Rows breathe: faint zebra, brighter under the pointer, actions quiet
   until the row is the one being read. Ranks 1-3 wear the podium. */
.board-table tbody tr:nth-child(even) td { background: rgb(255 255 255 / 0.015); }
.board-table tbody tr:hover td { background: rgb(255 255 255 / 0.045); }
/* Body face, not the display face: the display font's numerals are
   old-style (varying height, x-height zero), which made 13/15/17 read as
   smudges next to 10/20 -- Jeff caught it on the first screenshot. Lining
   tabular digits, same as every other number column. */
.board-table .rank-cell {
  font-size: 14px; font-weight: 600;
  font-variant-numeric: lining-nums tabular-nums;
}
.board-table .rank-gold   { color: #e5cc80; font-size: 16px; }
.board-table .rank-silver { color: #c9c9d2; font-size: 15px; }
.board-table .rank-bronze { color: #c47f45; }
.board-table .board-row-actions { opacity: 0.5; transition: opacity 0.12s ease; }
.board-table tbody tr:hover .board-row-actions { opacity: 1; }
/* After zebra and hover, so the viewer's own rows keep their gold wash. */
.board-table tr.is-mine td { background: rgb(229 204 128 / 0.05); }
.board-table tr.is-pinned td { border-top: 1px dashed var(--chrome-line); }
/* Player icons (1.09): the mark before a name, on the board and in the
   header. A glyph rather than an image -- it inherits size and colour from
   whatever row it sits in, and costs nothing on a hundred-row board. */
/* A loadout: up to three marks beside a name (1.10). The box is fixed so
   art and glyph entries line up in the same row, and the art carries the
   squared-off border the game's own icons have rather than floating on
   the background. Sized to sit with the name at a glance -- close to what
   an icon looks like in the game UI, not so large it owns the row. */
.player-icons {
  display: inline-flex; align-items: center; gap: 3px;
  margin-right: 6px; vertical-align: -3px;
}
.player-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; flex: 0 0 16px;
  font-size: 11px; line-height: 1; color: #e5cc80;
  text-shadow: 0 0 6px rgb(229 204 128 / 0.45);
}
.player-icon-art {
  width: 16px; height: 16px; object-fit: cover;
  border: 1px solid rgb(0 0 0 / 0.75);
  border-radius: 2px; box-shadow: 0 0 0 1px rgb(229 204 128 / 0.25);
}
/* The header sits on a larger line than a board row does. */
.auth-name .player-icons { gap: 4px; }
.auth-name .player-icon,
.auth-name .player-icon-art { width: 18px; height: 18px; flex-basis: 18px; }
/* The board's name column has to hold three marks plus the name without
   the spec line beneath it reflowing, so it gets the room explicitly. */
.row-player { display: flex; align-items: center; min-width: 0; }
.row-player .player-icons { flex: 0 0 auto; }
.icon-worn-line {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  font-size: 13px; color: var(--chrome-bright); margin: 0 0 10px;
}
/* The loadout, in the order it will actually appear, with the arrows that
   set it. Shown rather than described, because the order is the thing the
   old copy got wrong. */
.icon-loadout { display: inline-flex; align-items: center; gap: 10px; }
.loadout-slot {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 5px; border: 1px solid var(--chrome-line);
  border-radius: var(--radius-sm); background: rgb(0 0 0 / 0.22);
}
.loadout-move {
  border: 0; background: none; cursor: pointer; padding: 0 2px;
  font-size: 10px; line-height: 1; color: var(--chrome-bright);
}
.loadout-move:disabled { opacity: 0.25; cursor: default; }
.loadout-move:focus-visible { outline: 1px solid var(--chrome-bright); }
.loadout-empty { color: var(--text-muted); font-style: italic; }
.icon-hint { font-size: 12px; color: var(--chrome-dim); margin: 8px 0 10px; }
.icon-choice.is-full:not(.is-chosen) { opacity: 0.45; }
.patron-note { color: var(--chrome-bright); font-size: 13px; }

/* The achievement collection (1.10). Locked rows stay legible rather than
   hidden -- knowing what is left is the point -- but the earned ones carry
   the gold so a full row reads as progress at a glance. */
.ach-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 8px; margin-bottom: 6px;
}
.ach {
  display: flex; align-items: flex-start; gap: 9px; padding: 9px 11px;
  border: 1px solid var(--chrome-line); border-radius: var(--radius-sm);
  opacity: 0.55;
}
.ach.is-earned {
  opacity: 1; border-color: #e5cc80; background: rgb(229 204 128 / 0.06);
}
.ach-glyph { font-size: 16px; line-height: 1.2; color: var(--text-muted); }
/* A badge shipped before its art (the duel ladder, 5 Sep): the glyph in a
   dashed ring, and a line under the requirement saying the icon is coming. */
.ach-glyph-soon { display: inline-flex; align-items: center; justify-content: center;
  width: 21px; height: 21px; flex: none; font-size: 12px; border-radius: 50%;
  border: 1px dashed var(--line-strong); color: var(--plate-gold); }
.ach-soon { display: block; font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-faint); }
/* Real art, drawn at its native 21px. Locked badges are desaturated and
   dimmed rather than hidden -- seeing what is left is the point -- and
   earning one turns the colour on, which is the whole reward. */
.ach-art {
  width: 21px; height: 21px; flex: none; image-rendering: auto;
  filter: grayscale(1) opacity(0.5);
}
.ach.is-earned .ach-art { filter: none; }
.ach.is-earned .ach-glyph {
  color: #e5cc80; text-shadow: 0 0 8px rgb(229 204 128 / 0.4);
}
.ach-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ach-label { font-size: 13px; color: var(--text-bright); }
.ach-req { font-size: 11.5px; color: var(--text-muted); }

/* The picker on the account page. */
/* A uniform grid of small tiles (Sosa, 6 Sep): every tile the same size
   whatever its label, six or so to a row, the checkbox hidden behind the
   tile and the chosen ones ringed in gold. It used to be a flex row of
   width-by-label pills two to a line, half a page tall.
   Prefixed with .icon-form on purpose: the picker lives inside an
   .auth-form, whose generic `label { display: block }` and
   `input:disabled { opacity: 0.55 }` otherwise outrank these and put the
   label under the icon and a faded checkbox in every tile once three are
   chosen (Sosa, 7 Sep). */
.icon-form .icon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 6px; margin-bottom: 10px; }
.icon-form .icon-choice {
  position: relative; display: flex; align-items: center; gap: 8px;
  height: 36px; margin: 0; padding: 0 10px; cursor: pointer; min-width: 0;
  border: 1px solid var(--chrome-line); border-radius: 4px;
  background: var(--select-soft); font-size: 12px; color: var(--chrome-bright);
}
.icon-form .icon-choice input,
.icon-form .icon-choice input:disabled {
  position: absolute; left: 0; top: 0; width: 1px; height: 1px;
  opacity: 0; pointer-events: none; margin: 0; padding: 0; border: 0;
}
.icon-form .icon-choice.is-chosen { border-color: #e5cc80; box-shadow: 0 0 0 1px #e5cc80 inset; }
.icon-form .icon-choice.is-chosen::after { content: ""; position: absolute; right: 7px; top: 50%;
  width: 6px; height: 6px; margin-top: -3px; border-radius: 50%; background: #e5cc80; }
.icon-form .icon-choice .icon-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding-right: 10px; }
.icon-choice .player-icon,
.icon-choice .player-icon-art {
  margin-right: 0; width: 20px; height: 20px; flex-basis: 20px;
  font-size: 14px;
}
.icon-label { font-size: 12px; color: var(--chrome-bright); }
.board-you {
  margin-left: 6px; padding: 1px 6px; font-size: 10px;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--chrome-bright); border: 1px solid var(--chrome-line);
  border-radius: 999px;
}
.board-row-actions { white-space: nowrap; }
.board-row-actions form { display: inline; }
.board-flag-form { display: inline; }
.board-mod-btn {
  background: none; border: none; padding: 0; margin-left: 8px;
  cursor: pointer; font-size: 11px; color: #8f3b34;
  text-decoration: underline;
}
.board-mod-btn:hover { color: #d0524a; }
.board-flag {
  background: none; border: none; padding: 2px 3px; margin-left: 7px;
  cursor: pointer; color: #8f3b34; vertical-align: -1px;
}
.board-flag:hover { color: #d0524a; }
.board-flag svg { display: inline-block; fill: currentColor; }
.board-count { margin-top: 12px; color: var(--text-dim); font-size: 12px; max-width: 560px; }
/* The post-run sign-in pitch: the one moment a signed-out player has a
   number and nowhere to put it. A real panel, not a whisper. */
.signin-pitch {
  /* Above Share results and a size up, per Sosa -- this is the page's
     first ask of a signed-out player, not a footnote. */
  max-width: 560px; margin: 24px auto 0; padding: 18px 24px;
  border: 1px solid var(--chrome-line); border-radius: var(--radius-sm);
  text-align: center;
}
.signin-pitch-line { margin: 0 0 14px; color: #cfcabe; font-size: 15.5px; }
.signin-pitch-line strong { color: var(--chrome-bright); }
.signin-pitch-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
/* The compact top-runs panel on home: the board's own row idiom in a card,
   centred under its centred heading (Sosa's note -- it hung left). */
.home-top-runs { max-width: 640px; margin-left: auto; margin-right: auto; }
.home-board-link {
  display: inline-block; margin-top: 10px;
  font-size: 12.5px; letter-spacing: 0.05em;
  color: var(--chrome-bright); text-decoration: underline;
}
.home-board-link:hover { color: var(--text-bright); }
.board-aside { margin-top: 10px; color: var(--text-dim); font-size: 12.5px; max-width: 480px; }

/* Moderation panel */
.mod-report { border-bottom: 1px solid rgb(255 255 255 / 0.06); padding: 10px 0; }
.mod-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.mod-actions button {
  padding: 5px 12px; font-size: 12px; cursor: pointer;
  background: #14120e; color: var(--text-bright);
  border: 1px solid var(--chrome-line); border-radius: 3px;
}
.mod-actions button:hover { border-color: var(--chrome-bright); }
.mod-log { margin: 4px 0; font-size: 12.5px; color: var(--text-muted); }

/* Links inside the breaking-news box (the Patreon one, mostly). */
.breaking-news-body a { color: var(--chrome-bright); text-decoration: underline; }
.breaking-news-body a:hover { color: var(--text-bright); }
/* The blue clickable "Updates" word (Sosa's spec, literally). Mage blue --
   it's the palette's one true blue and it reads as a link at a glance. */
.breaking-news-body a.announce-updates {
  color: #3fc7eb; text-decoration: underline;
}
.breaking-news-body a.announce-updates:hover { color: #7fdcf6; }
/* The very clickable button under the announcement: the gold-faced plate
   from the early share buttons, rebuilt -- gold face, dark lettering, the
   same stamped-metal treatment as the crimson plates. */
.patreon-btn {
  display: block; margin-top: 14px; padding: 11px 14px;
  text-align: center;
  font-family: var(--font-display); font-weight: 600;
  font-size: 15px; letter-spacing: 0.04em;
  color: #241a08;
  background: linear-gradient(180deg, #d8ae3c 0%, #f2d06a 18%, #cfa32e 58%, #96731c 100%);
  border: 2px solid var(--plate-edge); border-radius: 3px;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.35), inset 0 -2px 3px rgb(0 0 0 / 0.35);
  text-shadow: 0 1px 0 rgb(255 235 170 / 0.5);
}
.patreon-btn:hover {
  background: linear-gradient(180deg, #e7bd49 0%, #ffdf80 18%, #ddb13a 58%, #a68024 100%);
  color: #241a08;
}
/* The header's compact twin, second draft (Sosa: the gold face made the
   zeroes read fat) -- the crimson plate and gold lettering every primary
   button wears, but rimmed in GOLD instead of the dark plate edge, so it
   still reads as the special door. */
/* Written as `.util a.patreon-mini` for a reason: the generic header-link
   rule above is `header.app .util a`, which outguns a bare
   `header.app .patreon-mini` on specificity and was quietly repainting
   the gold lettering grey. */
header.app .util a.patreon-mini {
  padding: 5px 12px; margin-right: 4px;
  font-size: 11.5px; font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--plate-gold);
  text-shadow: var(--plate-text-shadow);
  background: linear-gradient(180deg, #8e2429 0%, #ad3036 18%, #8b2126 58%, #5c1418 100%);
  border: 1px solid #c9a227; border-radius: 3px;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.2), inset 0 -1px 2px rgb(0 0 0 / 0.4);
}
header.app .util a.patreon-mini:hover {
  background: linear-gradient(180deg, #a32d33 0%, #c53b42 18%, #9e2a30 58%, #6c191e 100%);
  color: var(--plate-gold-lit);
}
/* The Discord door beside it (Sosa, 17 Sep: "more clickable, at the top
   with patreon"): the Patreon plate's shape and size in Discord's own
   blurple, the colour the login button already wears, with the mark.
   Same specificity trap as above, hence the long selector. */
header.app .util a.discord-mini {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px; margin-right: 4px;
  font-size: 11.5px; font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #fff; text-shadow: 0 1px 1px rgb(0 0 0 / 0.5);
  background: linear-gradient(180deg, #6b77f6 0%, #5865f2 40%, #4752c4 100%);
  border: 1px solid #2f3a9e; border-radius: 3px;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.2), inset 0 -1px 2px rgb(0 0 0 / 0.4);
}
header.app .util a.discord-mini:hover {
  background: linear-gradient(180deg, #7d88f8 0%, #6b77f6 40%, #5865f2 100%);
  color: #fff;
}
.discord-mark { width: 16px; height: 12px; flex: none; }

/* Glory (1.08). The chase number, holy gold (Sosa: "when i think glory i
   think holy"). Same font and size as the player name beside it -- no
   display face, the cells inherit the table's own type and only the
   colour changes. Written as `td.glory-cell` under each table class
   because the tables' own `td` colour rule outguns a bare class -- the
   same specificity trap the Patreon button fell into. */
.board-table td.glory-cell,
.profile-specs td.glory-cell { color: var(--plate-gold); }

/* The board as row-cards (Sosa's mockup layout, 26 Aug): class-striped
   cards, identity left, the PARSE / DPS / GS trio right, the quiet action
   cluster at the far edge. Same plate surfaces as every other card; the
   old .board-table rules stay for the home page's mini-table. */
.board-rows { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 14px; }
.board-row {
  display: flex; align-items: center; gap: 14px;
  padding: 10px 14px 10px 0;
  /* The smooth opaque grey (Sosa's pick from the screenshots): a solid
     panel that sits OVER the page's stone grain instead of letting it
     bleed through. Same surface on every row; your own rows show through
     the brighter border and the "you" chip. */
  background: var(--select-soft);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.board-row.is-mine { border-color: var(--select-line); }
.board-row.is-pinned { margin-top: 10px; }
/* The stripe borrows the class colour from its own class_css text colour:
   one token, no second palette to keep in sync. */
.row-stripe { width: 3px; align-self: stretch; background: currentColor; flex: none; }
.row-identity { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1; }
.row-rank {
  min-width: 26px; text-align: right; flex: none;
  font-family: var(--font-numeric); font-size: 14px;
  color: var(--text-dim);
}
.row-rank.rank-gold, .row-rank.rank-silver, .row-rank.rank-bronze { font-weight: 600; }
.row-names { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.row-player {
  color: var(--text-bright); font-size: 14.5px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.row-spec { display: flex; align-items: center; gap: 6px; font-size: 12px; }
.row-overall { color: var(--text-faint); font-size: 11px; margin-left: 6px; }
.row-stats { display: flex; gap: 22px; flex: none; }
.stat-block { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; min-width: 52px; }
.stat-block .stat-value { font-size: 15px; font-variant-numeric: tabular-nums; }
.stat-block .stat-dps { color: var(--text-bright); }
.stat-block .stat-label {
  font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-faint);
}
.row-actions {
  display: flex; align-items: center; gap: 8px; flex: none;
  padding-right: 2px;
}
.row-actions > a { font-size: 12px; }
/* Phones: identity on its own line, the trio beneath it, actions tucked
   after -- the card grows tall instead of scrolling sideways. */
@media (max-width: 620px) {
  .board-row { flex-wrap: wrap; row-gap: 8px; padding-right: 10px; }
  .row-identity { flex: 1 1 100%; }
  .row-stats { margin-left: 40px; gap: 18px; }
  .row-actions { margin-left: auto; }
}
/* Results on a phone (Sosa's screenshots, 26 Aug): the centre column is
   narrow, so the name at 26px broke around its own hyphen into giant
   stacked lines, and the minmax stat grid collapsed to one huge tile per
   row. Smaller name, name kept whole, and the three stats stay a row of
   three compact tiles. */
@media (max-width: 700px) {
  .doll-name {
    font-size: 18px; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; max-width: 100%;
  }
  .doll-name.is-unclaimed { font-size: 16px; }
  /* The doll keeps its three columns on phones and the centre track is
     too narrow for three tiles, so the row takes a real width and lets
     gear-centre's own align-items:center do the centring -- flexbox
     centres an overflowing item on the track midline, and the track is
     symmetric in the doll grid so that midline IS the viewport centre.
     (A margin-calc version fought that same flex centring and lost.)
     The base rule's width:100% must be beaten or the tracks crush. */
  .gear-centre .grid.cols-3 {
    width: min(330px, calc(100vw - 28px));
    grid-template-columns: repeat(3, 1fr);
    gap: 6px;
  }
  /* Daily's fourth tile (board can reach): two-by-two reads better on a
     phone than four crushed columns. */
  .gear-centre .grid.cols-4 {
    width: min(330px, calc(100vw - 28px));
    grid-template-columns: repeat(2, 1fr);
    gap: 6px;
  }
  .gear-centre .stat .value { font-size: 17px; }
  .gear-centre .stat .label { font-size: 9px; }
  .result-actions { flex-wrap: wrap; justify-content: center; row-gap: 8px; }
}

/* The one-question feedback box on selected results pages (loud since
   28 Aug -- the quiet version collected one answer in 101,578 runs). Gold
   edge, warm ground, and a title that says the player was chosen, because
   being picked is the reason to answer. Still not a modal: it asks from
   the page, it never blocks the way out. */
.feedback-ask {
  max-width: 520px; margin: 22px auto 4px; padding: 18px 20px;
  border: 1px solid #e5cc80; border-radius: var(--radius-sm);
  background: rgb(229 204 128 / 0.06);
  box-shadow: 0 0 18px rgb(229 204 128 / 0.10);
  text-align: left;
}
.feedback-title {
  margin: 0 0 4px; color: #e5cc80; font-size: 12px;
  letter-spacing: 0.14em; text-transform: uppercase;
}
.feedback-note {
  margin: 8px 0 0; color: var(--text-muted); font-size: 12px;
}
.feedback-q {
  margin: 0 0 10px; color: var(--text-bright); font-size: 15px;
  line-height: 1.45;
}
.feedback-ask textarea {
  width: 100%; padding: 8px 10px; resize: vertical;
  background: #14120e; color: var(--text-bright);
  border: 1px solid var(--chrome-line); border-radius: 3px;
  font-family: var(--font-body); font-size: 13px;
}
.feedback-ask textarea:focus { outline: none; border-color: var(--chrome-bright); }
.feedback-ask .btn { margin-top: 8px; }

/* The way out of a failed request, sitting inside the error notice itself.
   Dressed like a small plain button, not the crimson primary — this is
   recovery, not the next exciting thing to do. */
.error-retry {
  margin-left: 10px; padding: 3px 12px; font-size: 12px; cursor: pointer;
  background: #14120e; color: var(--text-bright);
  border: 1px solid var(--chrome-line); border-radius: 3px;
}
.error-retry:hover { border-color: var(--chrome-bright); }
.profile-empty { color: var(--text-muted); max-width: 480px; }
.profile-cooldown { margin: 8px 0 0; color: var(--text-muted); font-size: 13px; }
.auth-form input:disabled { opacity: 0.55; }

/* Staff-only badge granting on the account page (1.10). Deliberately plain:
   it is a tool, not a feature, and it should not read like one. */
.grant-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.grant-row input[type="text"] { flex: 1 1 190px; min-width: 0; }
.grant-row select { flex: 0 0 auto; }

/* Recruit a Friend. The link is the paste target so it gets the width; the
   code underneath is for anyone retyping it. Wraps to two lines on a phone
   rather than shrinking the field, because a truncated invite link that
   still LOOKS whole is worse than an obviously wrapped one. */
.recruit-blurb { margin: 0 0 10px; font-size: 13px; color: #b9ad97; }
.recruit-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.recruit-row input[type="text"] {
  flex: 1 1 260px; min-width: 0;
  font-family: var(--font-numeric); font-size: 13px;
}
.recruit-row .btn { flex: 0 0 auto; }
.recruit-code { margin: 10px 0 0; font-size: 13px; color: #b9ad97; }
.recruit-code strong {
  font-family: var(--font-numeric); letter-spacing: 0.08em; color: #e5cc80;
}
.recruit-count { margin: 4px 0 0; font-size: 12px; color: #8a8171; }
/* Pending reads brighter than the settled count: it is the line that tells
   someone their code DID work and what is left to do. */
.recruit-waiting { color: #e5cc80; }

/* Daily's spoiler veil. The tile keeps its quality colour and loses its
   identity, which is the whole artifact: how the run went, without what was
   in it. The name is absent from the HTML, not hidden by these rules -- this
   only stops an empty icon slot collapsing the row. */
/* Stepping between daily boards. The date leads and the theme sits under it
   in the raid's own voice, because "3 September, Blackwing Lair" is what
   makes one board memorable as distinct from another. */
/* A spec already played, or one there is no allowance left for. Both are
   dimmed; only the played one is a link. */
/* ---------- the mode selector (Jeff, 31 Aug) ------------------------------ */
/* Layout only, like the rest of this file. The first version set its own
   background, border and radius and opened with `all: unset`, which threw
   away the button plate these inherit for free -- so they read as two flat
   rectangles in a page where everything else is stamped metal (Sosa, 31 Aug).
   Colour comes from `.btn, button` in parse-theme.css, and the selected one
   takes `.primary`, which is the crimson the site already uses for the live
   thing. */
.mode-picker {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
  margin: 0 0 18px;
}
.mode-pick {
  /* The plate is an inline-flex row; these need two stacked lines, so only
     the axis and the alignment are overridden. Everything else -- the
     gradient, the rim, the four-layer bevel, the display face -- is
     inherited. */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: 11px 15px;
  text-align: left;
  width: 100%;
}
.mode-pick .mode-head { display: flex; align-items: center; gap: 8px; }
.mode-pick .mode-blurb {
  /* No colour of its own: it dims the inherited plate gold so the blurb sits
     under the name without introducing a fourth grey. */
  font-family: var(--font-body);
  font-size: 12px;
  letter-spacing: 0;
  line-height: 1.5;
  opacity: 0.68;
}
.daily-context {
  display: flex; align-items: center; justify-content: space-between;
  margin: -6px 2px 16px; font-size: 12px; color: var(--text-muted);
}

/* A daily card's state, inline with the class name so the eye reads name
   then state in one pass. */
.spec-badge {
  font-size: 11px; padding: 2px 7px; border-radius: 3px;
  border: 1px solid var(--chrome-line); color: var(--text-muted);
}
.spec-badge.is-score { border-color: #6b5a2c; color: #e5cc80; }
.spec-badge.is-progress { border-color: #4a4257; color: #a89ec4; }
.spec-badge.is-open { border-color: #3d5a41; color: #8fc496; }
.spec-foot {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 10px; font-size: 12px;
}
.spec-ceiling { color: var(--text-faint); }
.spec-go-quiet { color: var(--plate-gold); font-size: 12px; }

@media (max-width: 560px) {
  /* Jeff's open question: on a phone the five cards already stack into a
     long column, and two description cards on top push the specs below the
     fold. The blurbs go and the picker becomes a compact strip. */
  .mode-picker { gap: 8px; }
  .mode-pick { padding: 9px 10px; }
  .mode-pick .mode-blurb { display: none; }
  .daily-context { flex-direction: column; align-items: flex-start; gap: 2px; }
}

.spec-card.is-done { opacity: 0.75; }
.spec-card.is-spent { opacity: 0.4; cursor: default; }

.board-day-nav, .board-month-nav {
  display: flex; align-items: center; justify-content: center;
  gap: 18px; margin: 10px 0 4px;
}
.board-day-step.is-off { opacity: 0.25; font-size: 13px;
                         color: var(--text-muted); padding: 4px 10px; }
.board-day-current {
  display: flex; flex-direction: column; align-items: center;
  font-family: var(--font-display); letter-spacing: 0.05em;
  color: var(--text-bright);
}
.board-theme {
  font-style: normal; font-size: 11px; letter-spacing: 0.1em;
  text-transform: uppercase; color: #e5cc80;
}

.veil-note {
  margin: 0 0 14px; padding: 9px 12px;
  font-size: 13px; color: #e5cc80;
  border-left: 2px solid #e5cc80; background: rgb(0 0 0 / 0.22);
}
.gear-doll.is-veiled .gear-icon {
  background-image: none;
  opacity: 0.5;
}
.gear-doll.is-veiled .gear-item::after {
  content: "hidden";
  color: var(--text-faint); font-size: 11px; letter-spacing: 0.06em;
}
.auth-notice {
  color: var(--chrome-bright); font-size: 13px; margin: 0 0 10px;
  padding: 8px 12px; border-left: 2px solid var(--chrome-bright);
  background: rgb(0 0 0 / 0.22);
}

/* The earned date sits under the requirement rather than replacing it, so a
   badge still says what it was for after you have it (Jeff, 29 Aug). */
.ach-earned {
  display: block; margin-top: 2px;
  font-size: 11px; letter-spacing: 0.06em; color: #e5cc80;
}

/* A link inside a requirement has to look like one (Jeff, 29 Aug): it sits
   in muted body text, so without the colour and underline it reads as more
   of the sentence. */
.ach-req a {
  color: #e5cc80; text-decoration: underline;
  text-underline-offset: 2px; text-decoration-thickness: 1px;
}
.ach-req a:hover { color: var(--text-bright); }

/* California's opt-out icon sits with the link text, at the 29x14 aspect the
   state publishes it at. Legal requirement, so it is not scaled to taste. */
.ccpa-icon { height: 14px; width: auto; vertical-align: -3px; margin-right: 5px; }

/* The streak on the Daily tab: a flame and a count, sized to ride inside
   the nav link without moving its baseline. Layout only -- the number
   inherits the nav link's own colour, per the theme.css contract. */
.nav-streak {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  margin-left: 6px;
  /* Same size as the tab label itself -- the first cut was 0.78em and
     Sosa's screenshot said what it looked like: too small to be the
     thing the tab exists to show off. */
  font-size: 1em;
  font-weight: 600;
  vertical-align: 0;
}
.nav-streak img { display: block; image-rendering: pixelated; }

/* The daily card's finished-run parse: a bare number where the boxed
   "89 parse" pill sat. Colour and weight come from .profile-parse and
   the parse-* tier classes; this only sizes it to be the card's loudest
   element after the spec name. */
/* Every parse number wears the Warcraft Logs face (Sosa, 7 Sep): the
   same heavy condensed grotesque as the results page's .parse-value, on
   the spec tiles, the class plates, the daily card and the profile. */
/* Copy my day (1.16): the day's lines under the Daily panel's summary, a
   Wordle-shaped block with one square per spec, and the two share buttons.
   The square takes its colour from the same --parse variable the parse
   numbers use, so the block and the numbers can never disagree. */
.daily-share { margin: 6px 0 16px; }
.daily-lines {
  list-style: none; margin: 0 0 12px; padding: 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 20px;
}
.daily-line { display: flex; align-items: center; gap: 9px; min-height: 26px; }
.daily-line.is-unplayed { opacity: 0.55; }
.daily-line .spec-parse { font-size: 1.05em; }
.daily-line-dash { margin-left: auto; color: var(--text-faint); }
.daily-square {
  width: 14px; height: 14px; flex: none; border-radius: 2px;
  background: var(--parse, var(--text-faint));
}
.daily-square.parse-none { background: transparent; border: 1px solid var(--text-faint); }
.daily-share-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.daily-text { width: 100%; min-height: 9em; margin-top: 10px; font-family: var(--font-mono, monospace); font-size: 13px; }
@media (max-width: 520px) { .daily-lines { grid-template-columns: 1fr; } }

.spec-parse {
  font-family: var(--font-parse);
  font-weight: 700;
  font-size: 1.6em;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  margin-left: auto;
}

/* The results page's daily cell: a lead-in line and a small leaderboard
   button where the average tile sat. Sizing only; the button's plate
   comes from .btn. */
.stat-board {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 7px;
}
.stat-board .stat-lead { font-size: 0.8em; letter-spacing: 0.06em;
                         text-transform: uppercase; opacity: 0.75; }
.stat-board .board-btn { font-size: 0.85em; padding: 6px 14px; }

/* The check rides the corner of whatever symbol the streak is wearing. */
.streak-glyph { position: relative; display: inline-flex; }
.streak-glyph .streak-check {
  position: absolute;
  right: -5px;
  bottom: -3px;
  image-rendering: pixelated;
}

/* Jeff's bug 4 (1 Sep): the Available badge overlapped Marksmanship
   Hunter's name -- the longest label in the grid. The head is a flex row;
   the badge must never shrink onto the text, and the name wraps instead
   of colliding. */
.spec-head { flex-wrap: wrap; row-gap: 4px; }
.spec-head .spec-badge { flex-shrink: 0; margin-left: auto; }
/* flex-basis 0 so a long name (Marksmanship Hunter, Destruction
   Warlock) shrinks and wraps ITS TEXT beside the icon instead of
   dropping the whole element under it (Sosa, 2 Sep). The container
   keeps flex-wrap for the Available badge, which may still wrap. */
.spec-head .spec-name { min-width: 0; flex: 1 1 0; }

/* Jeff's bug 2 (1 Sep): the consolation-piece screen borrows board-head,
   whose spans are laid out expecting the boss reel behind them; with the
   reel hidden the status line sat under the items bar, clipped. Stacked
   plainly here instead. */
.board-head.pity-head {
  position: static;
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 18px 0 10px;
}
.board-head.pity-head span {
  position: static;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  max-width: none;
}

/* ── The draft picker (Jeff's mock v3, then Sosa's revision, 2 Sep) ───────────
   Mode, then class, then spec, then play, stacking downward. The pickers
   are the old spec-selector cards, minus the Start button. */
.roster-legend { font-size: 0.82em; color: #8a8578; }
/* The Discord door (1.17): the header's plain util link, and one line
   under the feedback box. No brand colour; the site's own chrome. */
.feedback-discord { margin: 8px 0 0; font-size: 12px; color: var(--text-dim); }
.stage-panel { margin-top: 22px; }
/* The shared "Pick a spec" heading sits where each panel's own used to
   (1.16): it takes the panel gap, and the panel under it takes less. */
.spec-headrow { margin-top: 22px; }
.spec-headrow ~ .spec-panel { margin-top: 10px; }
.stage-headrow { display: flex; align-items: baseline; gap: 14px;
                 justify-content: space-between; flex-wrap: wrap; }
.stage-head { margin-top: 0; }
.mode-meta { display: block; margin-top: 8px; font-size: 0.8em; color: #caa74f; }
/* A picked card: the gold ring the play bar also wears. */
.spec-card.is-selected { border-color: #f4c430;
                         box-shadow: 0 0 0 1px #f4c430 inset; }
.spec-card.is-locked, .spec-card.is-locked:hover {
  cursor: default; opacity: 0.5; transform: none; box-shadow: none;
  background: repeating-linear-gradient(135deg,
    rgba(20, 16, 12, 0.55) 0 6px, rgba(28, 23, 17, 0.55) 6px 12px);
}
.spec-card.is-locked .spec-name { color: #8a8578; }
.spec-card.is-locked .spec-meta { color: #6d675c; }
.spec-card .spec-parse { margin-left: auto; }

.play-bar { margin-top: 16px; display: flex; align-items: center;
            justify-content: space-between; gap: 18px; flex-wrap: wrap;
            background: rgba(34, 28, 20, 0.85);
            border: 1px solid #f4c430; border-radius: 3px; padding: 14px 18px; }
.play-what { margin: 0; font-size: 1.25em; font-weight: 700; }
.play-facts { margin: 3px 0 0; font-size: 0.85em; color: #b0a890; }
.play-actions { display: flex; gap: 10px; align-items: center; }
.play-btn { font-size: 1.05em; padding: 10px 30px; }

/* ── Mode buttons wear their art (Sosa, 2 Sep: "just to see it out") ──────
   Standard is the dungeon gate, Daily the tavern. A dark gradient keeps the
   copy readable; the unchosen one sits dimmed, the chosen one lights up
   behind the gold ring. The old grey/crimson plates are overridden here. */
/* The art is a LAYER (::after), never the button's own background. The
   selected tile wears `.primary`, and the plate's `button.primary:hover`
   sets the `background` shorthand at a specificity that beat the tile's
   cover/center, so hovering the chosen tile re-cropped the picture with a
   120ms slide (Sosa, 3 Sep: "the weird camera change"). On its own layer
   nothing a hover rule does to the button can move it. The same
   construction dresses the class cards below. */
.mode-picker .mode-pick { position: relative; overflow: hidden; isolation: isolate;
  background: #14110d !important;
  border: 1px solid rgba(90, 80, 60, 0.6); color: #e8ddc9; }
.mode-picker .mode-pick > * { position: relative; z-index: 2; }
.mode-picker .mode-pick::after { content: ""; position: absolute; inset: 0; z-index: 0;
  background-size: cover; background-position: center;
  filter: saturate(0.55) brightness(0.8); transition: filter 0.15s ease; }
.mode-picker .mode-pick::before { content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(90deg, rgba(10, 8, 6, 0.82) 0%,
                              rgba(10, 8, 6, 0.55) 55%, rgba(10, 8, 6, 0.35) 100%);
  transition: background 0.15s; }
.mode-picker #pick-standard::after { background-image: url("modes/standard.48fdf7b982eb.jpg"); }
.mode-picker #pick-daily::after { background-image: url("modes/daily.8bc53d906f3a.jpg"); }
.mode-picker #pick-duel::after { background-image: url("modes/duel.94f2a292729a.jpg"); }
.mode-picker .mode-pick:hover::after { filter: saturate(0.85) brightness(0.95); }
.mode-picker .mode-pick:hover, .mode-picker .mode-pick:active { transform: none; }
.mode-picker .mode-pick.is-active, .mode-picker .mode-pick.primary {
  border-color: #f4c430; box-shadow: 0 0 0 1px #f4c430 inset; }
.mode-picker .mode-pick.is-active::after, .mode-picker .mode-pick.primary::after { filter: none; }
.mode-picker .mode-pick.is-active::before {
  background: linear-gradient(90deg, rgba(10, 8, 6, 0.72) 0%,
                              rgba(10, 8, 6, 0.4) 55%, rgba(10, 8, 6, 0.15) 100%); }
.mode-picker .mode-pick .mode-blurb, .mode-picker .mode-pick .mode-meta {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9); }
/* The Duel tile is a link, so it gets the plate the two buttons inherit. */
.mode-picker a.mode-pick { display: flex; flex-direction: column; align-items: flex-start;
  gap: 4px; padding: 11px 15px; border-radius: 3px;
  font-family: var(--font-display); font-size: 13.5px; letter-spacing: 0.06em;
  text-shadow: var(--plate-text-shadow); cursor: pointer;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.20), inset 0 -2px 3px rgb(0 0 0 / 0.45),
              inset 0 0 14px rgb(0 0 0 / 0.35), 0 1px 3px rgb(0 0 0 / 0.55); }
.mode-picker .mode-pick .mode-name { font-family: var(--font-display); font-size: 1.35em;
  letter-spacing: 0.02em; color: var(--plate-gold); text-shadow: var(--plate-text-shadow); }

/* Class tiles (Sosa, 3 Sep, round 4): the plate-button construction, the
   crimson Play plate's gradient shape tinted with each class's colour,
   the classic class crest on the right, the name in the plate's gold
   lettering with its black shadow. No art layer: the plate IS the
   background, so nothing a hover rule does can move a picture. */
.class-grid { grid-template-columns: repeat(3, 1fr); gap: 12px; }
@media (max-width: 720px) { .class-grid { grid-template-columns: 1fr; } }
.class-grid .class-card { position: relative; overflow: hidden; isolation: isolate;
  min-height: 78px; display: flex; flex-direction: row; align-items: center;
  justify-content: space-between; gap: 12px; padding: 8px 14px 8px 16px; text-align: left;
  /* The mode tiles' edge, not the crimson plate rim (Sosa): the same muted
     gold line Standard, Daily and Duel wear. */
  color: #e8ddc9; border: 1px solid rgba(90, 80, 60, 0.6); border-radius: 3px;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.14), inset 0 -6px 10px rgb(0 0 0 / 0.6),
              inset 0 5px 10px rgb(0 0 0 / 0.35), inset 0 0 46px rgb(0 0 0 / 0.65),
              0 1px 3px rgb(0 0 0 / 0.6);
  transition: background 0.12s ease, border-color 0.12s ease; }
/* Aged the way the rest of the site is aged: the same worn panel texture
   every card sits on (7% there), multiplied over the plate a good deal
   stronger here so the colour wears unevenly like old metal. No brushing,
   no grain, no sheen (Sosa, 3 Sep). */
.class-grid .class-card::before { content: ""; position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
  background: var(--art-panel-ground) center / cover no-repeat;
  mix-blend-mode: multiply; opacity: 0.4; }
.class-grid .class-card::after { content: none; }
.class-grid .class-card > * { position: relative; z-index: 2; }
.class-grid .class-card:hover { transform: none; }
.class-grid .class-card:active { transform: none; }
.class-grid .class-card.is-selected { border-color: #f4c430;
  box-shadow: 0 0 0 1px #f4c430 inset, inset 0 -6px 10px rgb(0 0 0 / 0.6),
              inset 0 5px 10px rgb(0 0 0 / 0.35), inset 0 0 46px rgb(0 0 0 / 0.65),
              0 1px 3px rgb(0 0 0 / 0.6); }
.class-grid .class-name { font-family: var(--font-display); font-size: 1.3em;
  letter-spacing: 0.02em; color: var(--plate-gold); text-shadow: var(--plate-text-shadow); }
.class-grid .class-card:not(.is-locked):hover .class-name { color: var(--plate-gold-lit); }
.class-grid .class-icon { width: 42px; height: 42px; flex: none; border-radius: 4px;
  border: 1px solid rgba(0, 0, 0, 0.75); box-shadow: 0 0 10px rgba(0, 0, 0, 0.85); }
/* Locked: the stone plate, dimmed, grey name, no crest, nothing to press. */
.class-grid .class-card.is-locked, .class-grid .class-card.is-locked:hover {
  cursor: default; opacity: 1; transform: none;
  background: linear-gradient(180deg, #202326 0%, #2e3236 16%, #212427 58%, #121415 100%) !important; }
.class-grid .class-card.is-locked .class-name { color: #8a8578;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9); }
.class-grid [data-class="warrior"] { background: linear-gradient(180deg, #614425 0%, #916637 16%, #6b4b29 58%, #382715 100%); }
.class-grid .class-card[data-class="warrior"]:not(.is-locked):hover, .class-grid .class-card[data-class="warrior"].is-selected { background: linear-gradient(180deg, #75522d 0%, #a87640 16%, #805930 58%, #47321b 100%); }
.class-grid [data-class="paladin"] { background: linear-gradient(180deg, #61253f 0%, #91375f 16%, #6b2946 58%, #381525 100%); }
.class-grid .class-card[data-class="paladin"]:not(.is-locked):hover, .class-grid .class-card[data-class="paladin"].is-selected { background: linear-gradient(180deg, #752d4c 0%, #a8406e 16%, #803053 58%, #471b2f 100%); }
.class-grid [data-class="hunter"] { background: linear-gradient(180deg, #486125 0%, #6b9137 16%, #4f6b29 58%, #293815 100%); }
.class-grid .class-card[data-class="hunter"]:not(.is-locked):hover, .class-grid .class-card[data-class="hunter"].is-selected { background: linear-gradient(180deg, #57752d 0%, #7ca840 16%, #5e8030 58%, #35471b 100%); }
.class-grid [data-class="rogue"] { background: linear-gradient(180deg, #615d25 0%, #918b37 16%, #6b6729 58%, #383615 100%); }
.class-grid .class-card[data-class="rogue"]:not(.is-locked):hover, .class-grid .class-card[data-class="rogue"].is-selected { background: linear-gradient(180deg, #75702d 0%, #a8a140 16%, #807a30 58%, #47441b 100%); }
.class-grid [data-class="priest"] { background: linear-gradient(180deg, #616161 0%, #919191 16%, #6b6b6b 58%, #383838 100%); }
.class-grid .class-card[data-class="priest"]:not(.is-locked):hover, .class-grid .class-card[data-class="priest"].is-selected { background: linear-gradient(180deg, #757575 0%, #a8a8a8 16%, #808080 58%, #474747 100%); }
.class-grid [data-class="shaman"] { background: linear-gradient(180deg, #003161 0%, #004991 16%, #00366b 58%, #001c38 100%); }
.class-grid .class-card[data-class="shaman"]:not(.is-locked):hover, .class-grid .class-card[data-class="shaman"].is-selected { background: linear-gradient(180deg, #003b75 0%, #0055a8 16%, #004080 58%, #002447 100%); }
.class-grid [data-class="mage"] { background: linear-gradient(180deg, #255161 0%, #377991 16%, #29596b 58%, #152f38 100%); }
.class-grid .class-card[data-class="mage"]:not(.is-locked):hover, .class-grid .class-card[data-class="mage"].is-selected { background: linear-gradient(180deg, #2d6275 0%, #408ca8 16%, #306a80 58%, #1b3c47 100%); }
.class-grid [data-class="warlock"] { background: linear-gradient(180deg, #342561 0%, #4e3791 16%, #3a296b 58%, #1e1538 100%); }
.class-grid .class-card[data-class="warlock"]:not(.is-locked):hover, .class-grid .class-card[data-class="warlock"].is-selected { background: linear-gradient(180deg, #3f2d75 0%, #5a40a8 16%, #443080 58%, #261b47 100%); }
.class-grid [data-class="druid"] { background: linear-gradient(180deg, #613004 0%, #914706 16%, #6b3504 58%, #381c02 100%); }
.class-grid .class-card[data-class="druid"]:not(.is-locked):hover, .class-grid .class-card[data-class="druid"].is-selected { background: linear-gradient(180deg, #753a05 0%, #a85207 16%, #803f05 58%, #472303 100%); }

/* The daily info panel (the old daily page's copy) sits between the mode
   and class pickers as a card. */
.daily-info { padding: 14px 18px 12px; }
.daily-info .daily-rules { margin: 6px 0 0 18px; color: #b0a890; font-size: 0.92em; }
.daily-info .daily-rules li { margin: 3px 0; }
.daily-info .daily-countdown { color: #caa74f; margin: 6px 0; }
.daily-info .dev-redraw { margin: 10px 0; }

/* No scroll anchoring inside the picker: a section appearing below must
   never make the browser re-anchor the viewport (Sosa, 3 Sep). */
#panel-mode, #panel-class, .spec-panel { overflow-anchor: none; }

/* ── Duel mode (1.14) ──────────────────────────────────────────────────── */
.mode-picker { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.mode-picker a.mode-pick { text-decoration: none; display: block; }
.duel-head h1 { margin-bottom: 4px; }
.duel-vs { font-size: 0.6em; color: #8a8578; margin: 0 10px; }
.duel-score { font-size: 1.6em; margin: 6px 0 0; color: #f4c430; }
.duel-legs { width: 100%; border-collapse: collapse; margin-top: 8px; }
.duel-legs th, .duel-legs td { text-align: left; padding: 8px 10px;
  border-bottom: 1px solid rgba(90, 80, 60, 0.35); }
.duel-legs th { font-size: 0.8em; letter-spacing: 0.06em; text-transform: uppercase; color: #8a8578; }
.leg-wait { color: #6d675c; } .leg-done { color: #b0a890; }
.leg-link { margin-left: 8px; font-size: 0.85em; }
.leg-by { color: #8a8578; font-size: 0.85em; }
.duel-list { width: 100%; } .duel-list td { padding: 6px 10px; }
.duel-spec { cursor: pointer; }
.duel-spec input { position: absolute; opacity: 0; pointer-events: none; }
.duel-spec:has(input:checked) { border-color: #f4c430; box-shadow: 0 0 0 1px #f4c430 inset; }
/* Segmented plate radios (Sosa, 3 Sep): the stone plate, crimson when
   chosen, the same metal as every button on the site. */
.duel-format, .duel-rule { display: flex; gap: 10px; flex-wrap: wrap; margin: 6px 0 8px; }
.duel-choice { position: relative; display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 22px; border-radius: 3px; cursor: pointer;
  background: linear-gradient(180deg, #43484e 0%, #33383d 46%, #23272b 100%);
  border: 2px solid var(--plate-edge); color: var(--plate-gold);
  font-family: var(--font-display); font-size: 13.5px; letter-spacing: 0.06em;
  text-shadow: var(--plate-text-shadow);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.20), inset 0 -2px 3px rgb(0 0 0 / 0.45),
              inset 0 0 14px rgb(0 0 0 / 0.35), 0 1px 3px rgb(0 0 0 / 0.55);
  transition: background 0.12s ease, color 0.12s ease; }
.duel-choice input { position: absolute; opacity: 0; pointer-events: none; }
.duel-choice:hover { background: linear-gradient(180deg, #545a61 0%, #40464c 46%, #2c3136 100%);
  color: var(--plate-gold-lit); }
.duel-choice:has(input:checked) {
  background: linear-gradient(180deg, #8e2429 0%, #ad3036 18%, #8b2126 58%, #5c1418 100%); }
.duel-choice:has(input:checked):hover {
  background: linear-gradient(180deg, #a32d33 0%, #c53b42 18%, #9e2a30 58%, #6c191e 100%); }
.duel-choice:has(input:disabled) { opacity: 0.45; cursor: default; }
.duel-choice:has(input:focus-visible) { outline: 2px solid #f4c430; outline-offset: 2px; }
.duel-rule-label { margin: 14px 0 0; font-size: 12px; letter-spacing: 0.14em;
  text-transform: uppercase; color: #caa74f; }
.duel-rule-note { margin: 0 0 6px; font-size: 12px; color: #8a8578; max-width: 620px; }
.duel-bar { margin-top: 18px; display: flex; align-items: center; justify-content: space-between;
  gap: 18px; flex-wrap: wrap; background: rgba(34, 28, 20, 0.85);
  border: 1px solid #f4c430; border-radius: 3px; padding: 14px 18px; text-align: left; }
.duel-rules { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0 0; }
.duel-rule-chip { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 3px 8px; border: 1px solid rgba(244, 196, 48, 0.5); border-radius: 2px; color: #caa74f; }
.duel-name { display: block; margin: 14px 0; }
.duel-name input { display: block; margin-top: 6px; width: min(100%, 340px); }
.duel-action, .duel-result { text-align: center; }

/* ── Duel results (Jeff's mock, 4 Sep) ─────────────────────────────────── */
.duel-board { padding: 0; overflow: hidden; }
.duel-verdict { padding: 20px 20px 18px; border-bottom: 1px solid rgba(90, 80, 60, 0.45); text-align: center; }
.duel-format { margin: 0 0 10px; font-size: 12px; letter-spacing: 0.08em; color: #8a7d6d; }
.duel-tally { display: flex; align-items: center; justify-content: center; gap: 14px; margin-bottom: 12px; }
.duel-tally-name { font-size: 17px; color: #8a7d6d; max-width: 190px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.duel-tally-name.is-winner, .duel-tally-score b.is-winner, .duel-takes .is-winner { color: #f0c674; }
.duel-tally-name.is-loser, .duel-tally-score b.is-loser { color: #8a7d6d; }
.duel-tally-score { font-size: 26px; color: #e8dcc4; font-weight: 500; letter-spacing: 0.02em; }
.duel-tally-score b { font-weight: 500; }
.duel-tally-dash { color: #5a5048; margin: 0 6px; }
.duel-takes { margin: 0; font-size: 14px; color: #c9bba6; }
.duel-story { margin: 6px 0 0; font-size: 12px; color: #8a7d6d; }
.duel-pips { display: flex; justify-content: center; gap: 6px; padding: 14px 20px;
  border-bottom: 1px solid rgba(90, 80, 60, 0.45); }
.duel-pip { width: 34px; height: 6px; border-radius: 3px; background: #4a4038; }
.duel-pip.is-won, .duel-pip.is-played { background: #7a6a52; }
.duel-pip.is-deciding { background: #f0c674; }
.duel-pip.is-open { background: transparent; border: 1px solid #4a4038; }
.duel-grid { padding: 6px 12px 12px; }
.duel-grid-head, .duel-grid-row { display: grid; grid-template-columns: 34px 1fr 1fr 74px;
  align-items: center; padding: 9px 8px; }
.duel-grid-head { padding: 10px 8px 8px; font-size: 11px; letter-spacing: 0.08em; color: #6f6456;
  text-transform: uppercase; }
.duel-grid-head span, .duel-grid-row > span { text-align: right; }
.duel-grid-head span:first-child, .duel-grid-row > span:first-child { text-align: left; }
.duel-grid-head span:nth-child(2), .duel-grid-head span:nth-child(3),
.duel-leg-score { padding-right: 12px; }
.duel-grid-head span:nth-child(2), .duel-grid-head span:nth-child(3) { overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.duel-grid-row { border-top: 1px solid rgba(90, 80, 60, 0.3); font-size: 15px; }
.duel-grid-row.is-deciding { background: rgba(35, 29, 22, 0.9); }
.duel-leg-no { color: #6f6456; font-size: 13px; }
.duel-leg-no .leg-by { display: block; font-size: 11px; white-space: nowrap; }
.duel-leg-score { color: #b0a890; white-space: nowrap; }
.duel-leg-score.is-won { color: #f0c674; }
.duel-leg-score.is-lost { color: #7d7264; }
.duel-leg-score a { color: inherit; text-decoration: none; border-bottom: 1px dotted rgba(240, 198, 116, 0.35); }
.duel-leg-score a:hover { color: #ffe3a3; border-bottom-color: #ffe3a3; }
.duel-check { width: 13px; height: 13px; vertical-align: -1px; fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.duel-leg-series { font-size: 12px; color: #6f6456; white-space: nowrap; }
.duel-grid-row.is-deciding .duel-leg-series { color: #f0c674; }
.duel-note { margin: 0; padding: 10px 20px 2px; font-size: 12px; color: #6f6456; }
.duel-actions { display: flex; gap: 8px; align-items: center; padding: 14px 16px; margin-top: 12px;
  border-top: 1px solid rgba(90, 80, 60, 0.45); flex-wrap: wrap; }
.duel-actions form { display: contents; }
.duel-act { flex: 1 1 140px; justify-content: center; font-size: 13px; }
.duel-act.is-gold { border-color: #6b5a3e; }
.duel-act-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round; }
.duel-actions .showoff-said { flex: 1 0 100%; text-align: center; }
@media (max-width: 560px) {
  .duel-tally { gap: 8px; }
  .duel-tally-name { max-width: 110px; font-size: 14px; }
  .duel-grid-head, .duel-grid-row { grid-template-columns: 28px 1fr 1fr 58px; font-size: 14px; }
}

/* The class-then-spec picker inside a form (Sosa, 4 Sep): the main menu's
   plates and tiles, with the spec panel appearing under the chosen class. */
.spec-picker { margin: 4px 0 8px; }
.spec-picker .class-grid { margin-bottom: 12px; }
.spec-picker-panel { margin-top: 4px; }

/* The spec tile as the start button (Sosa, 4 Sep): the stone plate from
   parse-theme.css, icon left, gold plate lettering, no frame art. */
.spec-grid .spec-tile {
  flex-direction: row; align-items: center; gap: 14px; padding: 10px 18px; min-height: 66px;
  border-radius: 3px; border: 2px solid var(--plate-edge); border-image: none;
  background: linear-gradient(180deg, #43484e 0%, #33383d 46%, #23272b 100%);
  color: var(--plate-gold); cursor: pointer;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.20), inset 0 -2px 3px rgb(0 0 0 / 0.45),
              inset 0 0 14px rgb(0 0 0 / 0.35), 0 1px 3px rgb(0 0 0 / 0.55);
  transition: background 0.12s ease, color 0.12s ease; }
.spec-grid .spec-tile::before { display: none; }
.spec-grid .spec-tile:hover:not(.disabled) {
  background: linear-gradient(180deg, #545a61 0%, #40464c 46%, #2c3136 100%);
  border-color: var(--plate-edge); color: var(--plate-gold-lit); }
.spec-grid .spec-tile:active { transform: translateY(1px);
  box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.55), inset 0 -1px 0 rgb(255 255 255 / 0.08); }
.spec-grid .spec-tile:disabled { opacity: 0.6; cursor: wait; }
.spec-tile .spec-icon { border-color: rgba(0, 0, 0, 0.7); box-shadow: 0 0 8px rgba(0, 0, 0, 0.7); }
.spec-tile .spec-name { font-size: 19px; letter-spacing: 0.05em; color: inherit;
  text-shadow: var(--plate-text-shadow); }
.spec-tile .spec-meta { margin-left: auto; text-align: right; color: #b0a890; }
.spec-tile .spec-meta .spec-parse { margin: 0 2px; }
.spec-replay-row { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 12px; }

/* A locked class plate opens its dimmed spec list (Sosa, 4 Sep). */
.class-grid .class-card.is-locked { cursor: pointer; }

/* Daily: a class done for today wears a gold check beside its name, and
   only when every playable spec of it has been played (Sosa, 5 Sep). */
.class-done { display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; margin-left: 10px; vertical-align: -3px; border-radius: 50%;
  background: rgba(0, 0, 0, 0.55); border: 1px solid var(--plate-gold); color: var(--plate-gold);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7); text-shadow: none; }
.class-done svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round; }

/* Under the share bar (1.15): the recruit code rides on the link. */
.recruit-note { margin: 8px 0 0; font-size: 12px; color: var(--text-muted); max-width: 640px; }

/* The duel record on the profile (1.15, Ledger's block of 7 Sep):
   W/L/D figures, the last five as dots, rivalry rows with a Rematch. */
.duel-rec { max-width: 520px; }
.duel-rec-figures { display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin: 6px 0 14px; font-size: 14px; color: var(--text-body); }
.duel-rec-figures b { font-size: 24px; font-weight: 400; color: #7d7264; }
.duel-rec-figures .duel-rec-w b { color: #f0c674; }
.duel-rec-figures small { font-size: 14px; color: #8a7d6d; margin: 0 3px 0 2px; }
.duel-rec-dots { display: inline-flex; gap: 5px; margin-left: 6px; }
.duel-rec-dots .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; background: #4a4038; }
.duel-rec-dots .dot-win { background: #f0c674; }
.duel-rec-dots .dot-draw { background: #7a6a52; }
.duel-rec-streak { font-size: 12px; color: #8a7d6d; }
.duel-rec-forfeit { font-size: 12px; color: #6f6456; margin-left: auto; }
.duel-rivals { border-collapse: collapse; width: 100%; font-size: 14px; margin: 0 0 6px; }
.duel-rivals th { font-size: 11px; font-weight: 400; letter-spacing: 0.08em; text-transform: uppercase;
  color: #6f6456; text-align: left; padding: 0 0 8px; border-bottom: 1px solid #2b251f; }
.duel-rivals th.col-centre { text-align: center; }
.duel-rivals td { padding: 11px 0; border-bottom: 1px solid #2b251f; vertical-align: middle; }
.duel-rivals tr:last-child td { border-bottom: none; }
.duel-rivals .rival-name { font-size: 15px; color: #c9bba6; }
.duel-rivals .rival-guest { font-size: 11px; color: #6f6456; border: 1px solid #3a322a;
  border-radius: 3px; padding: 1px 4px; margin-left: 4px; vertical-align: middle; }
.duel-rivals .rival-forfeit { display: block; font-size: 11px; color: #6f6456; }
.duel-rivals .rival-record { text-align: center; width: 90px; }
.duel-rivals .rec-w { color: #f0c674; }
.duel-rivals .rec-l, .duel-rivals .rec-d { color: #7d7264; }
.duel-rivals .rec-dash { color: #4a4038; }
.duel-rivals .rival-last { text-align: center; font-size: 12px; color: #7d7264; width: 70px; }
.duel-rivals .rival-last.is-win { color: #f0c674; }
.duel-rivals .rival-act { text-align: right; width: 86px; }
.duel-rivals .rival-act form { display: inline; margin: 0; }
.duel-rivals .rival-rematch { background: none; border: 0; padding: 0; margin: 0; font: inherit;
  font-size: 13px; color: #8a7d6d; cursor: pointer; }
.duel-rivals .rival-rematch:hover { color: #f0c674; text-decoration: underline; }
.duel-rivals .rival-none { color: #4a4038; }
.duel-rec-empty { margin: 6px 0 8px; font-size: 14px; color: var(--text-body); }

/* Daily: today's parses on the class plate, one line per played spec,
   right-aligned before the crest (purplem, 5 Sep). */
.class-grid .class-parses { display: flex; flex-direction: column; align-items: flex-end;
  gap: 2px; margin-left: auto; margin-right: 6px; font-size: 12px; line-height: 1.2;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9); }
.class-grid .class-parse { display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; }
.class-grid .class-parse-spec { color: #d8ccb0; font-family: var(--font-display); letter-spacing: 0.03em; }
.class-grid .class-parse-note { color: #b0a890; font-size: 11px; }
.class-grid .class-parse .spec-parse { margin: 0; }
@media (max-width: 720px) { .class-grid .class-parse-spec { display: none; } }
.daily-info .daily-today { margin: 0 0 8px; color: #e8dcc4; }
