/* ============================================================
   Feedback Hub - shared styles (both halves)

   Loaded after site.css on every hub page. Everything here is built
   from the site's own tokens - the neutral night surfaces, the 3-step
   line ramp, Sora and Inter, the 8/12/18 radii - so the hub reads as
   part of the site rather than a product bolted onto it. The accent
   stays an accent: the primary action, the voted state, live numbers,
   "Implemented" / "Fixed", the Developer badge, focus and links.

   Every class defined here starts with hub-. The tone modifiers
   (t-good, t-dead ...) only ever appear qualified by a hub- class, and
   the few rules that touch a site component are scoped inside a hub
   element. Every animation has a reduced-motion override at the end.
   ============================================================ */

:root {
  /* The WoW Forever bronze is the patch notes' own marker for Forever
     entries, so a Forever chip reads the same here as there. The blue
     and the amber are the site's existing "being looked at" and
     "work in progress" colours. */
  --hub-forever: #dca77f;
  --hub-forever-line: rgba(220, 167, 127, 0.35);
  --hub-forever-soft: rgba(220, 167, 127, 0.10);
  --hub-info: #5cb8ff;
  --hub-info-line: rgba(92, 184, 255, 0.30);
  --hub-info-soft: rgba(92, 184, 255, 0.10);
  --hub-wip-line: rgba(255, 207, 92, 0.30);
  --hub-wip-soft: rgba(255, 207, 92, 0.10);
  --hub-ring: rgba(12, 210, 157, 0.14);
}

/* ---------------- utilities ---------------- */

/* .hub-sr, the words only a screen reader reads, is at the end of
   css/site.css with the supporter banner: the preset pages carry the
   banner too, and they load that file alone. */

.hub-ic { display: inline-block; flex: none; vertical-align: middle; }

/* The hidden attribute loses to any class that sets display (.btn is
   inline-flex, .notice is flex), so inside a hub page's <main> it is
   made to win - never on the rest of the site. */
.hub-page [hidden] { display: none !important; }

/* A button that reads as a quiet text link ("How voting works",
   "Show all"). The negative margin widens the hit area to a finger's
   width without moving the text. */
.hub-linkbtn {
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 0.8rem; font-weight: 600;
  color: var(--muted);
  padding: 8px 2px; margin: -8px -2px;
  text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 3px;
  transition: color 0.18s ease, text-decoration-color 0.18s ease;
}
.hub-linkbtn:hover { color: var(--ink-dim); text-decoration-color: var(--line-strong); }
.hub-linkbtn.is-accent { color: var(--accent); }
.hub-linkbtn.is-accent:hover { color: var(--accent-bright); text-decoration-color: var(--line-accent); }
.hub-linkbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* The back link that opens every detail and work page. */
.hub-back {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 40px; margin-bottom: 14px;
  font-weight: 600; font-size: 0.92rem;
}
.hub-back svg { width: 15px; height: 15px; transition: transform 0.2s ease; }
.hub-back:hover svg { transform: translateX(-3px); }

/* Busy spinner, shown in place of an icon or label while an action
   is in flight (vote tiles, dialog buttons, Try again). */
.hub-spin {
  display: none;
  width: 14px; height: 14px; flex: none;
  border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  opacity: 0.75;
  animation: hub-spin 0.7s linear infinite;
}
@keyframes hub-spin { to { transform: rotate(360deg); } }
.btn.is-busy .hub-spin { display: inline-block; }
.btn.is-busy svg { display: none; }

/* External-link mark after a label that opens a new tab. */
.btn .hub-ext, .hub-ext { width: 13px; height: 13px; opacity: 0.6; }

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

/* The .status-pill shape, with the line height pinned so every chip in
   a row is the same 26 px as the site's .chip - the one-line clip on
   rows depends on it. */
.hub-status {
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--font-body);
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  line-height: 18px;
  padding: 3px 10px; border-radius: 999px;
  border: 1px solid var(--line-soft);
  white-space: nowrap;
}
.hub-status svg { width: 12px; height: 12px; }
.hub-status.t-neutral { color: var(--muted); background: #212124; border-color: var(--line-soft); }
.hub-status.t-info    { color: var(--hub-info); background: var(--hub-info-soft); border-color: var(--hub-info-line); }
/* Planned: the accent itself for its words and its outline, so a request
   a contributor took on stands out in a list. */
.hub-status.t-accent  { color: var(--accent); background: transparent; border-color: var(--accent); }
.hub-status.t-strong  { color: var(--ink); background: transparent; border-color: var(--line-strong); }
.hub-status.t-wip     { color: var(--warn); background: var(--hub-wip-soft); border-color: var(--hub-wip-line); }
.hub-status.t-good    { color: var(--accent); background: var(--accent-soft); border-color: var(--line-accent); }
/* Ended without a fix. --muted, never --muted-2: the status is
   essential information, so it keeps readable contrast. */
.hub-status.t-dead    { color: var(--muted); background: transparent; border-color: var(--line); border-style: dashed; }

/* ---------------- game mode, module, version ---------------- */

.hub-mode {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-body); font-size: 0.74rem; font-weight: 600;
  line-height: 18px;
  padding: 3px 10px; border-radius: 999px;
  background: #212124; border: 1px solid var(--line-soft);
  color: var(--ink-dim);
  white-space: nowrap;
}
.hub-mode.is-forever {
  color: var(--hub-forever);
  border-color: var(--hub-forever-line);
  background: var(--hub-forever-soft);
}
/* "Retail + Forever": a half-and-half dot, the two modes at a glance.
   The team's screens only - members are only ever shown Retail or
   Forever (HubUI.memberModeChip). */
.hub-mode-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: linear-gradient(90deg, var(--ink-dim) 50%, var(--hub-forever) 50%);
}

/* The module chip borrows the site's .chip.tag-chip; long module names
   ellipsize instead of widening the row. */
.hub-cat {
  display: inline-block;
  max-width: 220px;
  overflow: hidden; text-overflow: ellipsis;
  vertical-align: top;
}
/* A detail page's title block wraps, so it has the room, down to a 320 px
   phone: its chip shows the whole label, a bug page's "Category" key
   included ("Bags / Minimap / Quests"). A module's examples in brackets
   ("Name (examples)", HubUI.categoryChip): lists show the name alone, and
   the title block the whole label. */
.hub-cat-more { display: none; }
.hub-detail-chips .hub-cat { max-width: 100%; }
.hub-detail-chips .hub-cat-more { display: inline; }

.hub-ver {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 0.78rem; line-height: 18px;
  padding: 3px 11px 3px 9px; border-radius: 999px;
  background: #212124; border: 1px solid var(--line-soft);
  white-space: nowrap;
}
.hub-ver-label {
  font-size: 0.63rem; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--muted);
}
.hub-ver-value { color: var(--ink-dim); }

/* A form's module or Section chips come in groups (HubUI.groupBreaks):
   each group starts on a line of its own under a hairline, which also
   doubles the row gap, so a group never reads as a wrapped line of the
   one before it. */
.hub-group-break { flex-basis: 100%; height: 1px; background: var(--line); }

/* ---------------- badges and people ---------------- */

.hub-badge {
  display: inline-flex; align-items: center;
  font-family: var(--font-body);
  font-size: 0.62rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  line-height: 1.5;
  padding: 1px 7px; border-radius: 999px;
  white-space: nowrap;
}
.hub-badge.is-dev { color: var(--accent); border: 1px solid var(--line-accent); }
.hub-badge.is-mod { color: var(--ink-dim); border: 1px solid var(--line-strong); }

.hub-person {
  display: inline-flex; align-items: center; gap: 7px;
  min-width: 0; max-width: 100%;
  color: var(--ink-dim); font-weight: 600;
}
.hub-person-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-person.is-ghost .hub-person-name { color: var(--muted); font-weight: 500; }

.hub-avatar { border-radius: 50%; flex: none; object-fit: cover; background: #212124; }
img.hub-avatar { border: 1px solid var(--line-soft); }
.hub-avatar.is-ghost {
  display: inline-grid; place-items: center;
  border: 1px solid var(--line-soft);
  color: var(--muted);
}
.hub-avatar.is-ghost svg { width: 58%; height: 58%; }

/* ---------------- contributors ---------------- */

/* A rank or achievement emblem (HubUI.emblem): a hexagon in the rank's own
   color (--rk, set on the svg), its mark drawn inside in the same color. */
.hub-emblem { display: inline-block; flex: none; vertical-align: middle; overflow: visible; }
.hub-emblem .hub-emblem-hex { fill: var(--rk); fill-opacity: 0.16; stroke: var(--rk); stroke-width: 1.5; stroke-linejoin: round; }
.hub-emblem .hub-emblem-mark {
  fill: none; stroke: var(--rk); stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round;
}

/* The Architect's mark (HubUI architectMark): the EllesmereUI logo, filled
   in the accent. */
.hub-emblem-eui path { fill: var(--rk); }

/* The small rank mark beside a contributor's name (or the Architect's). */
.hub-rank { display: inline-flex; align-items: center; flex: none; line-height: 0; }

/* A contributor's name linking their profile reads as a name, not a
   link, until it is pointed at. It sits above a row's stretched title
   link where one is used inside a row. */
.hub-person-link { color: inherit; position: relative; z-index: 1; }
.hub-person-link:hover { color: #fff; text-decoration: underline; text-decoration-color: var(--line-strong);
  text-underline-offset: 3px; }
.hub-person-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* Role colors (owner, 2026-10-26; HubUI roleClass): the Architect's name
   in the accent, Staff's and Valkyries' in green, ahead of a rank's. */
.hub-name-architect { --name: #0cd29d; }
.hub-name-staff { --name: #3fcf6e; }
.hub-person-name.hub-name-architect, .hub-person-name.hub-name-staff,
.hub-comment-name.hub-name-architect, .hub-comment-name.hub-name-staff { color: var(--name); }

/* A contributor's name (HubUI rankedName): in their rank's color (or
   their role's) with the rank's mark beside it, both one link to their
   profile. */
.hub-contrib { display: inline-flex; align-items: center; gap: 5px; min-width: 0; }
.hub-contrib, .hub-contrib.hub-person-link, .hub-contrib.hub-comment-name { color: var(--name, var(--rk)); }
.hub-contrib .hub-person-name { min-width: 0; color: inherit; }
.hub-contrib.hub-person-link:hover {
  color: color-mix(in srgb, var(--name, var(--rk)) 75%, #fff);
  text-decoration-color: color-mix(in srgb, var(--name, var(--rk)) 55%, transparent);
}
.hub-comment-head .hub-contrib { gap: 7px; }
/* On a step of a request's or a bug's status history, beside its chip. */
.hub-tl-who { font-size: 0.84rem; font-weight: 600; }

/* The rank mark's tooltip (HubUI showRankTip), in the look of the bell's:
   one element for the page, placed over the mark (under it near the top of
   the window), its caret on the mark. */
.hub-tip {
  position: fixed; top: 0; left: 0; z-index: 10000;
  display: none; flex-direction: column; gap: 1px;
  padding: 7px 11px;
  background: var(--panel-3);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-1);
  font-size: 0.76rem; font-weight: 500; line-height: 1.4; color: var(--muted);
  white-space: nowrap; pointer-events: none;
}
.hub-tip.is-on { display: flex; }
.hub-tip-t { font-size: 0.82rem; font-weight: 700; color: var(--rk, var(--ink)); }
.hub-tip::after {
  content: ""; position: absolute; left: var(--tip-x, 50%); bottom: -5px;
  width: 8px; height: 8px; margin-left: -4px;
  background: var(--panel-3);
  border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
  transform: rotate(45deg);
}
.hub-tip.is-below::after {
  top: -5px; bottom: auto;
  border: 0; border-left: 1px solid var(--line); border-top: 1px solid var(--line);
}

/* "Fixed by" / "Implemented by" under a detail page's title
   (HubUI.madeBy). */
.hub-madeby {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
  margin-top: 14px; padding: 12px 14px;
  border: 1px solid var(--line-soft); border-radius: var(--radius);
  background: var(--panel);
  font-size: 0.9rem; color: var(--muted);
}
.hub-madeby-k {
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted);
}
.hub-madeby-who { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; min-width: 0; }
.hub-madeby-who .hub-person { color: var(--ink); }
.hub-madeby-gh { color: var(--ink); font-weight: 600; }
.hub-madeby-gh:hover { color: #fff; }
.hub-madeby-facts { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; margin-left: auto; }
.hub-madeby-facts > span:not(.hub-status) { white-space: nowrap; }
@media (max-width: 720px) {
  .hub-madeby-facts { margin-left: 0; flex-basis: 100%; }
}

/* ---------------- the vote control ---------------- */

/* The vote sits above the row's stretched title link, as its sibling. */
.hub-vote { position: relative; z-index: 1; flex: none; }

/* Row and compact tiles (.hub-vote-tile is the button, or the plain
   box of a closed item). The card and the quick bar use the site's
   .btn instead, so nothing here reaches them. */
.hub-vote-tile {
  width: 56px; height: 64px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  background: #232326;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  color: var(--ink);
  padding: 0;
  font: inherit;
}
.hub-vote.is-compact .hub-vote-tile { width: 44px; height: 52px; }
button.hub-vote-tile { cursor: pointer; transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease; }
button.hub-vote-tile:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--hub-ring); }

.hub-vote-chev { display: grid; place-items: center; color: var(--muted); transition: transform 0.2s ease, color 0.2s ease; }
.hub-vote-chev svg { width: 18px; height: 18px; }
.hub-vote.is-compact .hub-vote-chev svg { width: 15px; height: 15px; }

/* The number rolls: the old value slides out while the new one slides
   in. The box clips both so the motion stays inside the tile. */
.hub-vote-num {
  position: relative; display: inline-block; overflow: hidden;
  height: 1.2em; line-height: 1.2;
  font-family: var(--font-display); font-weight: 800; font-size: 1.05rem;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}
.hub-vote-num > span { display: block; }
.hub-vote-num.is-long { font-size: 0.86rem; }
.hub-vote.is-compact .hub-vote-num { font-size: 0.92rem; }
.hub-vote.is-compact .hub-vote-num.is-long { font-size: 0.74rem; }
.hub-vote-num > span.is-out { position: absolute; left: 0; right: 0; top: 0; animation: hub-roll-out 0.18s ease forwards; }
.hub-vote-num > span.is-in { animation: hub-roll-in 0.18s ease; }
.hub-vote-num > span.is-out.is-down { animation-name: hub-roll-out-down; }
.hub-vote-num > span.is-in.is-down { animation-name: hub-roll-in-down; }
@keyframes hub-roll-out { to { transform: translateY(-100%); opacity: 0; } }
@keyframes hub-roll-in { from { transform: translateY(100%); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes hub-roll-out-down { to { transform: translateY(100%); opacity: 0; } }
@keyframes hub-roll-in-down { from { transform: translateY(-100%); opacity: 0; } to { transform: none; opacity: 1; } }

.hub-vote-cap {
  font-size: 0.58rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted); line-height: 1.2;
  white-space: nowrap;
}

@media (hover: hover) {
  button.hub-vote-tile:hover { border-color: var(--line-strong); }
  .hub-vote:not(.is-blocked) button.hub-vote-tile:hover .hub-vote-chev { transform: translateY(-2px); color: var(--ink-dim); }
}

/* Voted: the accent moment of the whole hub. */
.hub-vote.is-voted .hub-vote-tile { background: #172b26; border-color: var(--line-accent); }
.hub-vote.is-voted .hub-vote-chev { color: var(--accent); }
.hub-vote.is-voted .hub-vote-chev svg { fill: currentColor; }
.hub-vote.is-voted .hub-vote-tile .hub-vote-num { color: var(--accent); }
@media (hover: hover) {
  .hub-vote.is-voted button.hub-vote-tile:hover { border-color: var(--accent); }
  .hub-vote.is-voted button.hub-vote-tile:hover .hub-vote-chev { color: var(--accent); }
}

/* Blocked (out of votes, account too new, paused): full opacity on
   purpose - only the chevron goes quiet - so the tile still reads as
   something to press for an explanation. */
.hub-vote.is-blocked .hub-vote-chev { color: var(--muted-2); }

/* Closed: a plain number with the status glyph in its chip tone. */
.hub-vote.is-closed .hub-vote-tile { background: transparent; border-color: var(--line-soft); }
.hub-vote-glyph { display: grid; place-items: center; color: var(--muted); }
.hub-vote-glyph svg { width: 16px; height: 16px; }
.hub-vote-glyph.t-good { color: var(--accent); }
.hub-vote-glyph.t-accent { color: var(--accent); }
.hub-vote-glyph.t-info { color: var(--hub-info); }
.hub-vote-glyph.t-wip { color: var(--warn); }
.hub-vote-glyph.t-strong { color: var(--ink); }
.hub-vote.is-compact .hub-vote-glyph svg { width: 14px; height: 14px; }

/* Busy: the spinner takes the icon's place without moving anything. */
.hub-vote.is-busy .hub-vote-chev, .hub-vote.is-busy .hub-vote-ico { display: none; }
.hub-vote.is-busy .hub-vote-tile .hub-spin { display: block; margin: 2px 0; }
.hub-vote.is-busy .btn.hub-vote-btn .hub-spin { display: inline-block; }
.hub-vote.is-busy .hub-vote-btn { cursor: progress; }

/* The pop when a vote lands. */
.hub-vote.is-pop .hub-vote-chev svg,
.hub-vote.is-pop .hub-vote-ico svg { animation: hub-pop 0.22s ease; }
@keyframes hub-pop { 50% { transform: scale(1.25); } }

/* Card variant: the detail page's vote card. */
.hub-vote.is-card { display: grid; gap: 4px; }
.hub-vote-big { display: flex; align-items: baseline; gap: 9px; }
.hub-vote.is-card .hub-vote-num { font-size: 2.4rem; height: 1.15em; line-height: 1.15; letter-spacing: -0.03em; }
.hub-vote-big-cap {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted);
}
.hub-vote-week {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 0.82rem; color: var(--ink-dim);
}
.hub-vote-week[hidden] { display: none; }
.hub-vote-week svg { width: 13px; height: 13px; }
.hub-vote-act { display: grid; gap: 8px; margin-top: 14px; }
.hub-vote-act .btn.hub-vote-btn { width: 100%; }
.hub-vote-ico { display: inline-grid; place-items: center; }
.hub-vote-ico svg { width: 17px; height: 17px; }
.hub-vote-act .btn-dark .hub-vote-ico, .hub-vote.is-quick .btn-dark .hub-vote-ico { color: var(--muted-2); }
.hub-vote-lbl-alt { display: none; }
/* On a mouse, "Voted" turns into "Remove vote" while hovered or
   focused, and a click removes it. On a touch screen that would be one
   stray tap away, so "Voted" stays put and the separate quiet "Remove
   vote" below does the job. */
@media (hover: hover) {
  .hub-vote.is-card.is-voted:not(.is-touch) .hub-vote-btn:hover .hub-vote-lbl,
  .hub-vote.is-card.is-voted:not(.is-touch) .hub-vote-btn:focus-visible .hub-vote-lbl { display: none; }
  .hub-vote.is-card.is-voted:not(.is-touch) .hub-vote-btn:hover .hub-vote-lbl-alt,
  .hub-vote.is-card.is-voted:not(.is-touch) .hub-vote-btn:focus-visible .hub-vote-lbl-alt { display: inline; }
}
.hub-vote-remove {
  justify-self: center;
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 0.82rem; font-weight: 600; color: var(--muted);
  padding: 10px 12px; min-height: 40px;
}
.hub-vote-remove:hover { color: var(--ink-dim); }
.hub-vote-remove[hidden] { display: none; }
.hub-vote-why { justify-self: center; font-size: 0.82rem; font-weight: 600; padding: 6px 0; }
.hub-vote-why[hidden] { display: none; }
.hub-vote-closed {
  margin: 12px 0 0; padding: 12px 14px;
  font-size: 0.86rem; line-height: 1.55; color: var(--muted);
  background: var(--bg-2); border: 1px solid var(--line-soft); border-radius: var(--radius-sm);
}
/* At this width the detail page's quick bar owns Vote, so the card in
   the aside shows its numbers only. */
@media (max-width: 920px) {
  .hub-vote.is-card .hub-vote-act { display: none; }
}

/* Quick variant: the phone quick bar. */
.hub-vote.is-quick { display: inline-flex; align-items: center; gap: 12px; }
.hub-vote-q { display: inline-flex; align-items: baseline; gap: 6px; }
.hub-vote.is-quick .hub-vote-num { font-size: 1.25rem; }
.hub-vote-q-cap { font-size: 0.66rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.hub-vote.is-quick .btn.hub-vote-btn { min-height: 44px; padding: 10px 18px; }

/* ---------------- the weekly budget (feature votes) ---------------- */

.hub-pips { display: inline-flex; align-items: center; gap: 4px; flex: none; }
.hub-pips i {
  display: block; width: 10px; height: 4px; border-radius: 2px;
  background: rgba(255, 255, 255, 0.14);
  transition: background-color 0.2s ease;
}
.hub-pips i.on { background: var(--accent); }

.hub-meter { display: grid; gap: 6px; justify-items: start; }
.hub-meter[hidden] { display: none; }
.hub-meter-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.hub-meter-text { font-size: 0.9rem; color: var(--ink-dim); }
.hub-meter-text b { font-family: var(--font-display); font-weight: 700; color: var(--accent); }
.hub-meter-text.is-new { color: var(--ink); }
.hub-meter-reset { font-size: 0.76rem; color: var(--muted); }
.hub-meter-help { margin-top: 2px; }

/* When the readout wraps on a phone, "How voting works" sits right under
   it: its own 40 px tap area already gives the room a 14 px row gap would
   add again. The two texts share one baseline (their boxes differ in
   padding, so centring them would set one a few pixels lower), and the
   pips sit centred on the count's line. The link is a 40 px target too:
   9 px of padding above and below, 3 px of it handed back by the margin,
   so the readout keeps the height its 6 px padding used to give it. */
.hub-meter.is-compact { display: inline-flex; align-items: baseline; gap: 2px 14px; flex-wrap: wrap; }
.hub-meter-link {
  display: inline-flex; align-items: baseline; gap: 10px;
  color: var(--ink-dim);
  padding: 9px 0; margin: -3px 0;
}
.hub-meter-link .hub-pips { align-self: center; }
.hub-meter-link:hover { color: var(--ink); }
.hub-meter.is-compact .hub-meter-text { font-size: 0.84rem; color: inherit; }
.hub-meter.is-compact .hub-meter-text b { font-family: var(--font-body); font-weight: 700; color: var(--ink); }
.hub-meter.is-compact .hub-meter-help { margin-top: 0; }

/* ---------------- rendered markdown ---------------- */

.hub-md { color: var(--ink-dim); font-size: 0.97rem; line-height: 1.7; overflow-wrap: anywhere; }
.hub-md p { margin: 0 0 0.8em; }
.hub-md > :last-child { margin-bottom: 0; }
.hub-md strong { color: var(--ink); font-weight: 600; }
.hub-md em { font-style: italic; }
.hub-md code {
  font-family: var(--font-mono); font-size: 0.85em;
  background: var(--bg-2); border: 1px solid var(--line-soft);
  border-radius: 6px; padding: 1px 6px;
  color: var(--ink-dim);
}
/* Pasted Lua errors must keep their lines: they scroll sideways and
   stop growing at 420 px. */
.hub-md pre {
  margin: 0 0 0.9em;
  background: var(--bg-1); border: 1px solid var(--line-soft);
  border-radius: 10px;
  padding: 14px 16px;
  overflow: auto; max-height: 420px;
  font-family: var(--font-mono); font-size: 0.84rem; line-height: 1.55;
  color: var(--ink-dim);
  white-space: pre;
}
.hub-md pre code { background: none; border: 0; padding: 0; font-size: inherit; border-radius: 0; }
.hub-md blockquote {
  margin: 0 0 0.9em; padding: 2px 0 2px 14px;
  border-left: 3px solid var(--line-strong);
  color: var(--muted);
}
.hub-md blockquote p { margin-bottom: 0.4em; }
.hub-md ul, .hub-md ol { margin: 0 0 0.9em; padding-left: 1.4em; }
.hub-md li { margin-bottom: 0.25em; }
.hub-md li::marker { color: var(--muted-2); }
.hub-md .hub-md-h {
  font-family: var(--font-display); font-weight: 700; font-size: 1rem;
  color: var(--ink); margin: 1em 0 0.4em;
}
.hub-md .hub-md-h:first-child { margin-top: 0; }
.hub-md a.hub-md-link {
  color: var(--accent);
  border-bottom: 1px solid var(--line-accent);
  overflow-wrap: anywhere;
}
.hub-md a.hub-md-link:hover { color: var(--accent-bright); border-bottom-color: var(--accent); }
/* The destination, always visible. It is safety information, so it
   keeps --muted contrast, and it is isolated so nothing written before
   it can reorder its letters. */
.hub-md .hub-md-domain { color: var(--muted); font-size: 0.8em; unicode-bidi: isolate; }
.hub-md time { color: var(--ink); }

/* ---------------- rows ---------------- */

@keyframes hub-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* A card per request or bug. "backwards" rather than "both": a filled
   entrance would pin the transform and cancel the hover lift. */
.hub-row {
  position: relative;
  min-width: 0;
  display: flex; align-items: flex-start; gap: 16px;
  background: var(--panel);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: 16px 18px;
  transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.25s ease;
  animation: hub-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards;
  animation-delay: calc(var(--i, 0) * 40ms);
}
.hub-row.no-anim { animation: none; }
@media (hover: hover) {
  .hub-row:hover { border-color: var(--line); transform: translateY(-2px); box-shadow: 0 14px 34px rgba(0, 0, 0, 0.2); }
}
.hub-row:focus-within { border-color: var(--line); }

.hub-row-main { flex: 1; min-width: 0; display: grid; gap: 8px; align-content: start; }
.hub-row-title {
  margin: 0;
  font-family: var(--font-display); font-size: 1.02rem; font-weight: 700;
  line-height: 1.35; letter-spacing: -0.01em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.hub-row-title a { color: var(--ink); }
.hub-row-title a:hover { color: #fff; }
/* The whole card opens the item: the title link stretches over it. */
.hub-row-title a::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.hub-row-title a:focus-visible { outline: none; }
.hub-row-title a:focus-visible::after { box-shadow: 0 0 0 2px var(--accent), 0 0 0 5px var(--hub-ring); border-radius: var(--radius); }

/* Status, game mode, module - one line, the module the first to go. */
.hub-chips {
  display: flex; flex-wrap: wrap; gap: 6px;
  max-height: 26px; overflow: hidden;
  align-content: flex-start;
}
/* A status longer than the row is wide ("Investigating - Needs info" on
   the narrowest phones) ends in an ellipsis inside its own outline,
   instead of being cut off mid-word with its right edge gone. */
.hub-chips > .hub-status { max-width: 100%; min-width: 0; }
.hub-chips > .hub-status > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* The separator dot sits in the gap before each item. overflow is
   clipped so the dot of an item that wraps to the start of a new line
   falls outside the box and disappears, instead of dangling there. */
.hub-meta {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px 15px;
  overflow: hidden;
  font-size: 0.78rem; color: var(--muted);
}
.hub-meta > * { position: relative; display: inline-flex; align-items: center; gap: 5px; min-width: 0; }
/* Above a list row's stretched link, its meta line opens the row on a
   click too (HubUI metaOpensRow). */
.hub-row .hub-meta { cursor: pointer; }
.hub-meta > * + *::before {
  content: ""; position: absolute; left: -9px; top: 50%;
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--muted-2); transform: translateY(-50%);
}
.hub-meta svg { width: 13px; height: 13px; }
.hub-meta .hub-person { font-weight: 600; }
.hub-meta .hub-person-name { max-width: 180px; }
.hub-meta .hub-momentum { color: var(--ink-dim); }

.hub-row-note { position: relative; z-index: 1; font-size: 0.8rem; color: var(--muted); }
.hub-row-note a { font-weight: 600; }

.hub-row-thumb {
  width: 112px; height: 63px; flex: none;
  border-radius: 8px; border: 1px solid var(--line-soft);
  object-fit: cover; background: var(--bg-2);
}

@media (max-width: 720px) {
  .hub-row { gap: 12px; padding: 14px; }
  .hub-row-thumb { display: none; }
  .hub-vote.is-row .hub-vote-tile { width: 52px; height: 60px; }
}
/* A phone row is too narrow for a long status ("Investigating - Needs
   info") and the game mode on one line: the chips get a second line, so
   the mode always shows and only the module can still drop. */
@media (max-width: 560px) {
  .hub-row .hub-chips { max-height: 58px; }
}
/* The narrowest phones: the row's text gets 8 px more (2 px less padding on
   each side, 4 px less before the vote tile), so the longest status
   ("Implemented (Pending Approval)") reads whole at 375 px in the site's own
   font too; narrower still it ends in an ellipsis inside its outline. */
@media (max-width: 400px) {
  .hub-row { gap: 8px; padding-left: 12px; padding-right: 12px; }
}

/* ---------------- mini rows, match rows, shelves ---------------- */

.hub-mini {
  position: relative;
  min-width: 0;
  display: flex; align-items: center; gap: 12px;
  min-height: 56px;
  padding: 10px 12px;
  border: 1px solid transparent; border-radius: 10px;
  transition: background-color 0.18s ease, border-color 0.18s ease;
}
@media (hover: hover) {
  .hub-mini:hover { background: rgba(255, 255, 255, 0.025); border-color: var(--line-soft); }
}
.hub-mini-num {
  width: 36px; height: 36px; flex: none;
  display: grid; place-items: center;
  border-radius: 9px;
  background: #212124; border: 1px solid var(--line-soft);
  font-family: var(--font-display); font-weight: 800; font-size: 0.86rem;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.hub-mini-num.is-long { font-size: 0.68rem; }
.hub-mini-title {
  flex: 1; min-width: 0;
  font-weight: 600; font-size: 0.9rem; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hub-mini-title:hover { color: #fff; }
/* miniRow({ wrap: true }): the narrow asides of the request and bug pages
   let a related title take two lines instead of a few words, and put the
   status under it (.hub-mini-body), as a list tile does, so a long status
   never takes the title's width. A status wider than the aside ends in
   an ellipsis inside its own outline (its title keeps the hint). */
.hub-mini.is-wrap .hub-mini-title {
  white-space: normal; line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.hub-mini-body { flex: 1; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr); justify-items: start; gap: 6px; }
.hub-mini-body .hub-mini-title { max-width: 100%; }
.hub-mini-body .hub-mini-chip { max-width: 100%; }
.hub-mini-body .hub-mini-chip > .hub-status { max-width: 100%; min-width: 0; }
.hub-mini-body .hub-mini-chip > .hub-status > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.hub-mini-title::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.hub-mini-title:focus-visible { outline: none; }
.hub-mini-title:focus-visible::after { box-shadow: 0 0 0 2px var(--accent); }
.hub-mini-chip { flex: none; display: inline-flex; }
.hub-mini-act {
  position: relative; z-index: 1; flex: none;
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 0.8rem; font-weight: 600; color: var(--muted);
  padding: 10px 6px; min-height: 40px;
}
.hub-mini-act:hover { color: var(--ink-dim); }

.hub-match {
  position: relative;
  min-width: 0;
  display: flex; align-items: center; gap: 14px;
  padding: 12px 14px;
  background: var(--panel);
  border: 1px solid var(--line-soft); border-radius: 10px;
  transition: border-color 0.18s ease;
}
.hub-match + .hub-match { margin-top: 8px; }
.hub-match:hover { border-color: var(--line); }
.hub-match-main { flex: 1; min-width: 0; display: grid; gap: 6px; }
/* Status and game mode are what tell two similar matches apart (a Retail
   request is not a Forever one), so a match never clips its chips. */
.hub-match .hub-chips { max-height: none; }
.hub-match-title {
  color: var(--ink); font-weight: 600; font-size: 0.92rem; line-height: 1.4;
  overflow-wrap: anywhere;
}
/* The new-tab mark follows the last word rather than standing apart. */
.hub-match-title .hub-ext { margin-left: 5px; vertical-align: -1px; }
.hub-match-title:hover { color: #fff; }
/* The whole match opens it, as a list row does: the title link stretches
   over the card (a finger-sized target, not one line of text), while the
   vote tile and "Add your details" stay above it. */
.hub-match-title::after { content: ""; position: absolute; inset: 0; border-radius: 9px; }
.hub-match-title:focus-visible { outline: none; }
.hub-match-title:focus-visible::after { box-shadow: 0 0 0 2px var(--accent), 0 0 0 5px var(--hub-ring); }
.hub-match-week { display: inline-flex; align-items: center; gap: 5px; font-size: 0.76rem; color: var(--ink-dim); }
.hub-match-week svg { width: 12px; height: 12px; }
.hub-match-end { flex: none; display: flex; flex-direction: column; align-items: center; gap: 6px; }
.hub-match-closed { font-size: 0.78rem; color: var(--muted); text-align: right; max-width: 140px; line-height: 1.4; }
.hub-match-add { position: relative; z-index: 1; font-size: 0.76rem; font-weight: 600; white-space: nowrap; }
/* On a phone a closed match's ending ("Fixed - update EllesmereUI") is a
   muted line under the title instead of a column that squeezes it. */
@media (max-width: 560px) {
  .hub-match.is-closed { flex-wrap: wrap; row-gap: 4px; }
  .hub-match.is-closed .hub-match-end { flex: 1 1 100%; align-items: flex-start; }
  .hub-match.is-closed .hub-match-closed { max-width: none; text-align: left; }
}

/* The closing card of each home - the site's gradient hairline. */
.hub-shelf {
  border-radius: 14px;
  padding: 1px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.028));
  animation: hub-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.hub-shelf-in {
  border-radius: 13px;
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  padding: 22px 22px 14px;
}
.hub-shelf-head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.hub-shelf-icon {
  width: 34px; height: 34px; flex: none;
  display: grid; place-items: center;
  border-radius: 9px;
  background: var(--accent-soft); border: 1px solid var(--line-accent);
  color: var(--accent);
}
.hub-shelf-icon svg { width: 17px; height: 17px; }
.hub-shelf-title { flex: 1; margin: 0; font-family: var(--font-display); font-size: 1rem; font-weight: 700; letter-spacing: -0.01em; }
.hub-shelf-all {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 0.84rem; font-weight: 600; color: var(--accent);
  padding: 10px 0; min-height: 40px;
}
.hub-shelf-all svg { width: 14px; height: 14px; transition: transform 0.2s ease; }
.hub-shelf-all:hover { color: var(--accent-bright); }
.hub-shelf-all:hover svg { transform: translateX(3px); }
.hub-shelf-rows { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px 18px; margin: 0 -12px; }
@media (min-width: 1040px) {
  .hub-shelf-rows { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
.hub-shelf-empty { margin: 6px 12px 10px; font-size: 0.88rem; color: var(--muted); }

/* ---------------- fact tiles ---------------- */

/* Fact tiles (HubUI.facts; the two homes have a fun-fact line
   instead) are the site's tappable stat tiles; their labels are
   essential, so they get --muted rather than the stat tile's quieter
   default. */
.hub-facts .st-label { color: var(--muted); }

/* On a phone the tiles turn compact - number over label - instead of
   full stat cards, so the list starts near the first screen. Each tile
   stays a 72 px tap target. The content sits at the top of each tile,
   so the numbers in a row share one line however many lines a label
   wraps to. How many tiles share a row is the page's call. */
@media (max-width: 720px) {
  .staff-stats.hub-facts { gap: 8px; margin-bottom: 22px; }
  .hub-facts .stat-tile {
    min-height: 72px; padding: 11px 12px;
    flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 0;
  }
  .hub-facts .stat-tile .st-icon { display: none; }
  .hub-facts .stat-tile > span:not(.st-icon) { display: grid; gap: 3px; padding-left: 0; }
  .hub-facts .stat-tile .st-num { font-size: 1.3rem; }
  .hub-facts .stat-tile .st-label { margin: 0; font-size: 0.72rem; line-height: 1.3; letter-spacing: 0; }
  .hub-facts .hub-skel-tile { height: 72px; }
}

/* ---------------- the two homes: one template ----------------
   /features and /bugs are the same page with different content: the
   presets hero (with the fun-fact line), the list (the Feature Hub's
   with its weekly readout in the heading - bug votes have no budget),
   the toolbar, the active filter chips and notes, Load more, the end of
   the list and - in the default view - the EUI Insider invitation; the
   Feature Hub closes its default view with the "Recently implemented"
   shelf above that invitation. The header nav links both halves, so the
   heroes carry no switch between them. */

.hub-section { padding-top: 40px; }

/* The supporter banner under the header (.hub-ad) is at the end of
   css/site.css: the preset pages carry it too, and they load that file
   alone. */

/* Words on the left, the one primary action on the right, sitting on the
   heading's last line. */
.hub-hero-row {
  display: flex; flex-wrap: wrap; gap: 20px;
  align-items: flex-end; justify-content: space-between;
}
.hub-hero-copy { min-width: 0; }
/* The lede is always text, so the hero is as tall before the settings
   arrive as after: the Feature Hub's weekly budget is in the markup with
   a stand-in for its number ("a few votes"), which the settings replace
   in place. The Bug Tracker's second sentence - how bug voting works -
   is a plain line of its own under the first. */
.hub-lede-line { display: block; }
/* The Feature Hub's budget sentence ends "... every Tuesday with the
   weekly reset.", a little longer than the 560 px the site gives a hero
   paragraph, so the hub's lede runs to 600 px and the sentence keeps to
   one line on a desktop; where it has to wrap, no word is left alone on
   its last line. */
.page-hero p.hub-lede { max-width: 600px; text-wrap: pretty; }
.hub-hero-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; }

/* Each home's fun fact under the hero's words: how many features were
   added, or bugs fixed, in the last 30 days of patch notes, and a link to
   them. A quiet line in the lede's voice, never a control: the number in
   ink, the "Implemented" glyph (the one the "Recently implemented" shelf
   uses too) in the accent. Its 22 px line is held by a shimmer from the
   first paint, so the hero keeps its height until the scripts run; at
   zero the line folds shut (its one grid row goes from 1fr to 0fr) and
   leaves no gap. */
.hub-hero-copy .hub-fact {
  display: grid; grid-template-rows: 1fr;
  max-width: none;
  margin: 10px 0 0;
  font-size: 0.9rem; line-height: 22px; color: var(--muted);
  transition: grid-template-rows 0.3s ease, margin-top 0.3s ease, opacity 0.2s ease;
}
/* On the narrowest phones the words take two balanced lines; the glyph
   stays centred on the first (22 px line, 15 px glyph). */
.hub-fact-in { display: flex; align-items: flex-start; gap: 8px; min-width: 0; min-height: 0; overflow: hidden; }
.hub-fact-in svg { width: 15px; height: 15px; flex: none; margin-top: 3.5px; color: var(--accent); }
.hub-fact-in span { text-wrap: balance; }
.hub-fact-in b { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
.hub-fact.is-in .hub-fact-in { animation: hub-fade 0.4s ease both; }
.hub-hero-copy .hub-fact.is-gone { grid-template-rows: 0fr; margin-top: 0; opacity: 0; }
/* The shimmer is about as wide as the words and sits centred on their
   22 px line (12 px and 5 px above and below). */
.hub-skel-fact { width: 300px; max-width: 100%; height: 12px; margin: 5px 0; border-radius: 999px; }

/* On a phone the hero comes close to the presets hero's height, so the
   first rows show on the first screen: the lede steps aside (on the
   Feature Hub the budget readout right above the first row already says
   what the week brings). The fun fact stays - one short line under the
   heading. */
@media (max-width: 560px) {
  .hub-lede { display: none; }
  .hub-hero-copy .hub-fact { margin-top: 2px; font-size: 0.86rem; }
}

/* One grid, so the Feature Hub's budget readout can sit at the end of the
   heading row on wide screens and right above the first row on phones
   (the Bug Tracker has none, and its readout area stays empty). Margins,
   not row gaps, space the parts: a hidden part leaves no gap. */
.hub-browse {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "head  readout"
    "tools tools"
    "chips chips"
    "notes notes"
    "list  list"
    "more  more"
    "after after";
  align-items: center;
  scroll-margin-top: calc(var(--nav-h) + 16px);
}
/* The heading and the readout share one baseline. */
.hub-head {
  grid-area: head; align-self: baseline; min-width: 0;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px;
}
.hub-section-title {
  margin: 0;
  font-family: var(--font-display); font-size: 1.25rem; font-weight: 700;
  line-height: 1.3; letter-spacing: -0.01em;
  color: var(--ink);
}
.hub-total { font-size: 0.86rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.hub-head .hub-linkbtn { font-size: 0.82rem; }
.hub-readout { grid-area: readout; align-self: baseline; justify-self: end; padding-left: 16px; }

/* Row 1: the game mode and the sort; row 2: search, module, status. On
   phones: mode, search, module | status, sort - sorting comes last - and
   on the narrowest ones module, status and sort fold away behind the
   Filters button beside the search (below). */
.hub-toolbar {
  grid-area: tools;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  grid-template-areas:
    "mode   mode   sort"
    "search module status";
  gap: 12px;
  align-items: center;
  margin-top: 20px;
}
.hub-modes { grid-area: mode; justify-self: start; }
.hub-sort { grid-area: sort; justify-self: end; display: inline-flex; align-items: center; gap: 9px; min-height: 40px; }
/* The whole sort control is the tap target, not just its words. */
.hub-sort .cs-trigger { min-height: 40px; }
.hub-search { grid-area: search; min-width: 0; }
.hub-filter { min-width: 0; }
.hub-filter .cselect { width: 100%; }
.hub-filter-module { grid-area: module; width: 200px; }
.hub-filter-status { grid-area: status; width: 200px; }
/* The search and the phone-only Filters button share a wrapper that lays
   nothing out until a phone needs it: here the search is a grid item of
   the toolbar like the rest, and the button does not exist. */
.hub-searchbar { display: contents; }
.hub-filters-btn { display: none; }

/* Removable chips for each filter that is not the default. */
.hub-chiprow { grid-area: chips; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 14px; }
.hub-fchip {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 32px; max-width: 100%;
  padding: 5px 10px 5px 13px;
  background: #212124;
  border: 1px solid var(--line-soft); border-radius: 999px;
  font-family: var(--font-body); font-size: 0.8rem; font-weight: 600;
  color: var(--ink-dim);
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.hub-fchip span { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-fchip svg { width: 13px; height: 13px; flex: none; color: var(--muted); transition: color 0.18s ease; }
.hub-fchip:hover { border-color: var(--line-strong); color: #fff; }
.hub-fchip:hover svg { color: var(--ink); }
.hub-fchip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.hub-chiprow .hub-linkbtn { margin-left: 4px; }

/* The quiet line under the chips: why the status filter changed while
   searching. */
.hub-notes { grid-area: notes; }
.hub-note {
  display: flex; align-items: center; gap: 8px;
  margin: 14px 0 0;
  font-size: 0.84rem; line-height: 1.5; color: var(--muted);
}
.hub-note svg { width: 14px; height: 14px; flex: none; color: var(--muted); }
.hub-note + .hub-note { margin-top: 6px; }

.hub-list { grid-area: list; display: grid; gap: 12px; min-width: 0; margin-top: 22px; }
.hub-list > .hub-empty, .hub-list > .hub-error { margin-top: 4px; }
.hub-loadmore { grid-area: more; }
.hub-loadmore .load-more-wrap { margin-top: 30px; }
.hub-after { grid-area: after; }

/* The Feature Hub's closing shelf, well clear of the list and its last
   vote tile. */
.hub-shelf-slot > .hub-shelf { margin-top: 56px; }

/* The EUI Insider invitation closes the default view. Under the shelf the
   panel keeps its own 56 px; with no shelf above it (launch day, or no
   request shipped yet in this game mode) a hairline sets it apart, so it
   never reads as part of the list, a vote tile or the request prompt
   above. The Bug Tracker's strip brings its own hairline and gap. */
.hub-insider-slot.is-alone::before {
  content: ""; display: block;
  height: 1px; margin: 56px 0 40px;
  background: var(--line-soft);
}
.hub-insider-slot.is-alone > .hub-insider.is-panel { margin-top: 0; }

@media (max-width: 720px) {
  .hub-browse {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "head" "tools" "chips" "notes" "readout" "list" "more" "after";
  }
  .hub-readout { justify-self: start; padding-left: 0; margin-top: 18px; }
  .hub-readout:empty { display: none; }
  .hub-list { margin-top: 14px; }
  .hub-toolbar {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas: "mode mode" "search search" "module status" "sort sort";
    margin-top: 16px;
  }
  /* min-width: 0 lets the mode control scroll inside its own row on the
     narrowest phones instead of widening the page. */
  .hub-modes { justify-self: stretch; display: flex; min-width: 0; }
  .hub-modes button { flex: 1 0 auto; justify-content: center; }
  .hub-filter-module, .hub-filter-status { width: auto; }
  .hub-sort { justify-self: start; }
}

/* The narrowest phones: four rows of controls would push the first
   request off the first screen, so module, status and sort fold away
   behind a Filters button beside the search (HubUI.filtersToggle). What
   they are set to stays in view as the removable chips under the
   toolbar, the sort names the list's heading, and the button's badge
   counts the folded controls that are not at their default. */
@media (max-width: 560px) {
  .hub-searchbar { grid-area: search; display: flex; gap: 10px; min-width: 0; }
  .hub-searchbar .hub-search { flex: 1; }
  /* The readout's words sit as far below the search as the first row
     sits below them. */
  .hub-readout { margin-top: 12px; }
  .hub-filters-btn {
    position: relative;
    display: grid; place-items: center;
    width: 44px; flex: none;
    padding: 0;
    background: var(--panel-2);
    border: 1px solid var(--line-soft); border-radius: var(--radius-sm);
    color: var(--muted);
    cursor: pointer;
    transition: border-color 0.18s ease, color 0.18s ease, background-color 0.18s ease;
  }
  .hub-filters-btn svg { width: 18px; height: 18px; }
  .hub-filters-btn:hover { border-color: var(--line-strong); color: var(--ink); }
  .hub-filters-btn[aria-expanded="true"] { background: #2f2f33; border-color: var(--line-strong); color: var(--ink); }
  .hub-filters-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  /* Folded, the toolbar is two rows: the empty rows would still keep
     their gaps. */
  .hub-toolbar:not(.is-open) { grid-template-areas: "mode mode" "search search"; }
  .hub-toolbar:not(.is-open) .hub-filter,
  .hub-toolbar:not(.is-open) .hub-sort { display: none; }
  .hub-toolbar.is-open .hub-filter,
  .hub-toolbar.is-open .hub-sort { animation: hub-fold-in 0.24s cubic-bezier(0.22, 1, 0.36, 1) backwards; }
}
@keyframes hub-fold-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
/* The count is cut out of the button by a ring in the page colour, the
   shape of the notification bell's badge; neutral, since a filter is a
   state to notice rather than news to read. */
.hub-filters-count {
  position: absolute; top: -5px; right: -5px;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 999px;
  background: var(--ink); color: var(--bg-0);
  font-family: var(--font-display); font-size: 0.66rem; font-weight: 700;
  line-height: 18px; text-align: center;
  box-shadow: 0 0 0 2px var(--bg-0);
}
.hub-filters-count[hidden] { display: none; }

@media (max-width: 640px) {
  /* Both hero buttons keep their own width, the primary first, as on the
     presets page. */
  .hub-hero-actions { width: 100%; }
  .hub-hero-actions .btn-primary { order: -1; }
}

@media (max-width: 760px) {
  .hub-fchip { min-height: 40px; }
  .hub-sort .cs-trigger { min-height: 44px; }
  .hub-hero-actions .btn-sm { min-height: 40px; }
  .hub-toolbar .input { font-size: 16px; }
}

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

.hub-timeline { list-style: none; margin: 0; padding: 0; }
.hub-tl-item { position: relative; padding: 0 0 16px 22px; }
.hub-tl-item:last-child { padding-bottom: 0; }
.hub-tl-item.is-folded { display: none; }
.hub-tl-item::before {
  content: ""; position: absolute; left: 3.5px; top: 18px; bottom: 0;
  width: 1px; background: var(--line-soft);
}
.hub-tl-item:last-child::before { display: none; }
.hub-tl-dot {
  position: absolute; left: 0; top: 9px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--panel); border: 1px solid var(--line-strong);
}
.hub-tl-item.is-current .hub-tl-dot { background: var(--ink-dim); border-color: var(--ink-dim); }
.hub-tl-item.is-current .hub-tl-dot.t-good { background: var(--accent); border-color: var(--accent); }
.hub-tl-item.is-current .hub-tl-dot.t-accent { background: var(--accent); border-color: var(--accent); }
.hub-tl-item.is-current .hub-tl-dot.t-info { background: var(--hub-info); border-color: var(--hub-info); }
.hub-tl-item.is-current .hub-tl-dot.t-wip { background: var(--warn); border-color: var(--warn); }
.hub-tl-item.is-current .hub-tl-dot.t-strong { background: var(--ink); border-color: var(--ink); }
.hub-tl-item.is-current .hub-tl-dot.t-dead { background: var(--muted); border-color: var(--muted); }
.hub-tl-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; min-height: 26px; }
.hub-tl-date { font-size: 0.76rem; color: var(--muted); }
.hub-tl-desc { margin: 6px 0 0; font-size: 0.8rem; line-height: 1.5; color: var(--muted); }
.hub-tl-note {
  margin: 8px 0 0; padding: 8px 11px;
  font-size: 0.84rem; line-height: 1.55; color: var(--ink-dim);
  background: var(--bg-2);
  border: 1px solid var(--line-soft); border-left: 2px solid var(--line-strong);
  border-radius: var(--radius-sm);
  overflow-wrap: anywhere;
}
.hub-tl-merge { display: inline-flex; align-items: center; gap: 6px; font-size: 0.84rem; color: var(--ink-dim); flex-wrap: wrap; }
.hub-tl-merge svg { width: 14px; height: 14px; color: var(--muted); }
/* The merged request's link is a finger-sized target without moving the
   line: 9 px of padding above and below grow its hit area, and the same
   9 px of negative margin give the room back to the layout. */
.hub-tl-merge a { padding: 9px 0; margin: -9px 0; }
.hub-tl-more { display: block; margin-bottom: 14px; }

/* ---------------- the detail template ----------------
   A request's page and a bug's page are one template, part for part, and
   one grid holds the whole page. The left column opens with the lead -
   the back link, notices, the title block and the phone quick bar - then
   the content (main) and the half's own section (own: the discussion,
   "Also reported as"); the aside's cards fill the right column from the
   very top, level with the back link. The first two rows are exactly as
   tall as their content, so the own section starts right under the
   content and a tall aside only lengthens the last row. Up to 920 px it
   is one column: lead, main, aside, own. The grid shows one face at a
   time (HubUI.detailFace): the outline while the item loads, a
   whole-page state under the back link with no aside (.is-solo), or the
   item itself (.is-live). */

.hub-detail { padding-top: 40px; }

.hub-detail-head { margin-bottom: 30px; animation: hub-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) backwards; }
.hub-detail-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 10px; }
.hub-detail-hint { margin: 0 0 14px; font-size: 0.85rem; line-height: 1.5; color: var(--muted); }
.hub-detail-title {
  margin: 0 0 12px;
  font-family: var(--font-display); font-size: clamp(1.7rem, 3.6vw, 2.4rem); font-weight: 800;
  line-height: 1.18; letter-spacing: -0.02em;
  color: var(--ink);
  overflow-wrap: anywhere;
}
/* The separator dots sit in the gap before each item; the row clips them
   (with room for focus rings) so a dot never dangles at a line start. */
.hub-detail-meta {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px 17px;
  overflow: hidden;
  padding: 3px; margin: -3px;
  font-size: 0.9rem; color: var(--muted);
}
.hub-detail-meta > * { position: relative; display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.hub-detail-meta > * + *::before {
  content: ""; position: absolute; left: -10px; top: 50%;
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--muted-2); transform: translateY(-50%);
}
.hub-detail-meta .hub-person { font-weight: 500; color: var(--muted); }
.hub-detail-meta .hub-person-name { max-width: 260px; }

.hub-detail-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  grid-template-rows: auto auto 1fr;
  grid-template-areas:
    "lead aside"
    "main aside"
    "own  aside";
  column-gap: 36px;
  align-items: start;
}
.hub-detail-lead { grid-area: lead; min-width: 0; }
.hub-detail-main { grid-area: main; min-width: 0; }
.hub-detail-aside { grid-area: aside; min-width: 0; }
.hub-detail-own { grid-area: own; min-width: 0; }
/* A whole-page state has no aside, so it takes the page's width under
   the back link. */
.hub-detail-grid.is-solo { display: block; }
/* The item's parts arrive with the hub's entrance; the back link was
   there all along and stays still (the title block brings its own). */
.hub-detail-grid.is-live > :not(.hub-detail-lead) { animation: hub-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) 0.06s backwards; }
/* The half's own section under the content. */
.hub-detail-section { margin-top: 48px; scroll-margin-top: calc(var(--nav-h) + 16px); }

/* The phone quick bar: the number, Vote, how many votes are left (a
   request's only - bug votes have no budget), then Follow and one more
   icon - shown only where the aside drops below. */
.hub-quickbar { display: none; }
.hub-qb-left { order: 2; font-size: 0.84rem; color: var(--muted); white-space: nowrap; }
.hub-qb-left b { color: var(--ink); font-weight: 700; }
.hub-qb-icons { order: 3; display: flex; align-items: center; gap: 8px; margin-left: auto; }
.hub-qb-btn {
  width: 44px; height: 44px; flex: none;
  display: grid; place-items: center;
  background: var(--panel-2);
  border: 1px solid var(--line-soft); border-radius: 10px;
  color: var(--muted);
  cursor: pointer;
  transition: border-color 0.18s ease, color 0.18s ease;
}
.hub-qb-btn svg { width: 19px; height: 19px; }
.hub-qb-btn:hover { border-color: var(--line-strong); color: var(--ink); }
.hub-qb-btn[aria-pressed="true"] { color: var(--ink); border-color: var(--line-strong); }
.hub-qb-btn[aria-pressed="true"] svg { fill: currentColor; }
.hub-qb-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.hub-quickbar .go-discord { width: 44px; height: 44px; border-radius: 10px; }

/* The aside's cards. */
.hub-card-title {
  margin: 0 0 14px;
  font-family: var(--font-display); font-size: 0.95rem; font-weight: 700;
  color: var(--ink-dim);
}
.hub-vote-card .hub-meter { margin-top: 18px; }
.hub-vote-lines:empty { display: none; }
.hub-detail-line { margin: 12px 0 0; font-size: 0.86rem; line-height: 1.55; color: var(--muted); }
.hub-follow-line {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px;
  margin: 12px 0 0;
  font-size: 0.86rem; color: var(--ink-dim);
}
.hub-follow-line[hidden] { display: none; }
.hub-follow-line svg { width: 14px; height: 14px; flex: none; color: var(--accent); }
.hub-stats {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;
  margin-top: 18px; padding-top: 16px;
  border-top: 1px solid var(--line-soft);
}
.hub-stat { display: grid; gap: 2px; min-width: 0; }
.hub-stat b {
  font-family: var(--font-display); font-size: 1.08rem; font-weight: 700;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.hub-stat span { font-size: 0.74rem; color: var(--muted); }
.hub-card-foot { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 18px; }
.hub-card-foot .btn { width: 100%; padding-left: 10px; padding-right: 10px; }
.hub-card-foot .btn:only-child { grid-column: 1 / -1; }
.hub-card-foot .btn svg { width: 15px; height: 15px; }

/* Loading: the shape of the title block, and the aside's cards. */
.hub-skel-head { display: grid; gap: 12px; margin-bottom: 30px; }
.hub-skel-chips { display: flex; gap: 6px; }
.hub-skel-chips .hub-skel { width: 92px; height: 26px; border-radius: 999px; }
.hub-skel-meta { width: 38%; height: 18px; border-radius: 999px; }
.hub-skel-aside { display: grid; gap: 18px; }

@media (max-width: 920px) {
  .hub-detail-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: none;
    grid-template-areas: "lead" "main" "aside" "own";
  }
  .hub-detail-main { margin-bottom: 36px; }
  .hub-quickbar {
    display: flex; align-items: center; flex-wrap: wrap; gap: 10px 12px;
    margin: -8px 0 28px; padding: 12px 14px;
    background: linear-gradient(180deg, var(--panel-2), var(--panel));
    border: 1px solid var(--line-soft); border-radius: var(--radius);
  }
  /* The quick bar owns Vote and Follow at this width; the card keeps the
     numbers, a request's budget and Copy link. */
  .hub-vote-card .hub-follow-btn { display: none; }
  .hub-vote-card .hub-card-foot { grid-template-columns: 1fr; }
  .hub-detail-section { margin-top: 18px; }
}

@media (max-width: 760px) {
  .hub-card-foot .btn, .hub-vote-card .btn-sm { min-height: 40px; }
}

/* Phones: the number, Vote and the two icons on one line; how many votes
   are left drops to its own line under them when the width runs out. */
@media (max-width: 400px) {
  .hub-qb-left { order: 4; flex-basis: 100%; margin-top: -4px; }
  .hub-qb-icons { gap: 6px; }
  .hub-qb-btn, .hub-quickbar .go-discord { width: 40px; height: 40px; }
}

/* ---------------- EUI Insider ---------------- */

/* panel: the support page's footnote strip, kept deliberately quieter
   than the shelf above it. Its icon square is neutral here - the one
   accent the invitation keeps is its text link. */
.hub-insider.is-panel {
  position: relative;
  max-width: 920px;
  margin: 56px auto 0;
  padding-right: 52px;
}
.hub-insider .mb-icon { background: #212124; border-color: var(--line-soft); color: var(--ink-dim); }
.hub-insider .mb-copy span { line-height: 1.5; }
.hub-insider-note { font-size: 0.78rem; color: var(--muted); }
.hub-insider .mb-copy .hub-insider-note { margin-top: 4px; font-size: 0.78rem; }
/* --muted, not --muted-2: an icon-only control needs 3:1 contrast to be
   found at all. */
.hub-insider .hub-insider-hide {
  position: absolute; top: 9px; right: 9px;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  background: none; border: 0; border-radius: 8px; cursor: pointer;
  color: var(--muted);
  transition: color 0.18s ease, background-color 0.18s ease;
}
.hub-insider .hub-insider-hide svg { width: 15px; height: 15px; }
.hub-insider .hub-insider-hide:hover { color: var(--ink-dim); background: rgba(255, 255, 255, 0.04); }
.hub-insider .hub-insider-hide:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* inline: one subtle row under a request's discussion. */
.hub-insider.is-inline {
  display: flex; align-items: flex-start; gap: 14px;
  margin-top: 40px; padding: 16px 0;
  border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft);
}
.hub-insider-icon {
  width: 30px; height: 30px; flex: none;
  display: grid; place-items: center;
  border-radius: 8px;
  background: #212124; border: 1px solid var(--line-soft);
  color: var(--ink-dim);
}
.hub-insider-icon svg { width: 15px; height: 15px; }
.hub-insider.is-inline p { flex: 1; min-width: 0; margin: 0; font-size: 0.88rem; line-height: 1.6; color: var(--muted); }
.hub-insider.is-inline p b { color: var(--ink-dim); font-weight: 600; }
.hub-insider.is-inline .hub-insider-note { display: block; margin-top: 4px; }
.hub-insider-link {
  flex: none;
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.88rem; font-weight: 600;
}
.hub-insider-link svg { width: 14px; height: 14px; }
.hub-insider.is-inline .hub-insider-link { padding-top: 4px; }

/* strip: the least prominent - one muted, centred line. */
.hub-insider.is-strip {
  margin-top: 56px; padding: 18px 0;
  border-top: 1px solid var(--line-soft);
  text-align: center;
}
.hub-insider.is-strip p { margin: 0; font-size: 0.82rem; line-height: 1.7; color: var(--muted); }
.hub-insider.is-strip .hub-insider-link { display: inline-flex; font-size: inherit; color: var(--muted); text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 3px; }
.hub-insider.is-strip .hub-insider-link:hover { color: var(--ink-dim); text-decoration-color: var(--line-strong); }
.hub-insider.is-strip .hub-insider-link svg { width: 12px; height: 12px; }

@media (max-width: 640px) {
  /* Wrapped like the support page's strip: icon and copy, then the link
     indented under the copy. The icon stays level with the first line
     instead of drifting to the middle of a tall paragraph. */
  .hub-insider.is-panel { padding-right: 22px; align-items: flex-start; }
  .hub-insider.is-panel .mb-copy { padding-right: 30px; }
  .hub-insider.is-inline { flex-wrap: wrap; }
  .hub-insider.is-inline p { flex: 1 1 calc(100% - 44px); }
  .hub-insider.is-inline .hub-insider-link { margin-left: 44px; padding-top: 0; }
}
@media (max-width: 760px) {
  .hub-insider .hub-insider-hide { width: 40px; height: 40px; top: 4px; right: 4px; }
}

/* ---------------- Want to contribute? ---------------- */

/* HubUI.contributeCta: the Bug Tracker's invitation to Contributors to
   be. On the home it is the Insider's inline row (.hub-insider.is-inline
   above); on a bug page a plain aside card whose one accent is its text
   link, like the inline row's. */
.hub-contribute-text { margin: 0 0 12px; font-size: 0.88rem; line-height: 1.6; color: var(--muted); }
.hub-contribute-channel { white-space: nowrap; }
.hub-contribute.is-card .hub-insider-link { padding: 9px 0; margin: -9px 0; }

/* ---------------- popover and menu ---------------- */

/* Absolute, in page coordinates: they scroll with the content they
   explain rather than floating loose over it. */
.hub-pop, .hub-menu {
  position: absolute; z-index: 260;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  animation: hub-float-in 0.16s ease both;
}
@keyframes hub-float-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
.hub-pop { max-width: 300px; padding: 14px 16px; outline: none; }
.hub-pop-text { margin: 0; font-size: 0.86rem; line-height: 1.55; color: var(--ink-dim); }
.hub-pop-act { display: flex; margin-top: 12px; }
.hub-pop-act .btn svg { width: 15px; height: 15px; }

.hub-menu { min-width: 200px; padding: 6px; }
.hub-menu button {
  display: flex; width: 100%; align-items: center; gap: 10px;
  min-height: 44px;
  padding: 10px 12px;
  background: none; border: 0; border-radius: 8px; cursor: pointer;
  text-align: left;
  font-family: var(--font-body); font-size: 0.92rem; font-weight: 500;
  color: var(--ink-dim);
}
.hub-menu button svg { width: 15px; height: 15px; opacity: 0.7; }
.hub-menu button:hover, .hub-menu button:focus-visible { background: var(--accent-soft); color: #fff; outline: none; }
.hub-menu button.is-danger { color: #ff8b97; }
.hub-menu button.is-danger:hover, .hub-menu button.is-danger:focus-visible { background: rgba(255, 92, 108, 0.12); color: #ffb3ba; }

/* The "..." button that opens a menu. */
.hub-more {
  width: 40px; height: 40px; flex: none;
  display: inline-grid; place-items: center;
  background: none; border: 1px solid transparent; border-radius: 8px; cursor: pointer;
  color: var(--muted);
  transition: color 0.18s ease, background-color 0.18s ease, border-color 0.18s ease;
}
.hub-more svg { width: 18px; height: 18px; }
.hub-more:hover, .hub-more[aria-expanded="true"] { color: var(--ink-dim); background: #212124; border-color: var(--line-soft); }
.hub-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---------------- dialogs ---------------- */

.hub-dialog-overlay {
  position: fixed; inset: 0; z-index: 240;
  display: flex; align-items: center; justify-content: center;
  padding: 24px 16px;
  overflow-y: auto;
  background: rgba(9, 9, 9, 0.8);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  animation: hub-fade 0.18s ease both;
}
@keyframes hub-fade { from { opacity: 0; } to { opacity: 1; } }
.hub-dialog {
  position: relative;
  width: 100%; max-width: 520px;
  margin: auto;
  padding: 28px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  outline: none;
  animation: hub-dialog-in 0.22s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes hub-dialog-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}
.hub-dialog.is-wide { max-width: 720px; }
.hub-dialog-head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 12px; }
.hub-dialog-title { flex: 1; margin: 4px 0 0; font-size: 1.2rem; }
.hub-dialog-x {
  width: 36px; height: 36px; flex: none; margin: -4px -8px 0 0;
  display: grid; place-items: center;
  background: none; border: 0; border-radius: 8px; cursor: pointer;
  color: var(--muted);
}
.hub-dialog-x svg { width: 18px; height: 18px; }
.hub-dialog-x:hover { color: var(--ink); background: rgba(255, 255, 255, 0.05); }
.hub-dialog-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.hub-dialog-body { color: var(--muted); font-size: 0.95rem; line-height: 1.6; }
.hub-dialog-body > p { margin: 0 0 12px; }
.hub-dialog-body > :last-child { margin-bottom: 0; }
.hub-dialog-actions { display: flex; justify-content: flex-end; gap: 12px; flex-wrap: wrap; margin-top: 24px; }
/* A minor action (HubUI.dialog's action.minor): the smaller, quieter
   choice beside the main one, at the row's far start - Claim Feature's
   Skip Review and Claim. */
.hub-dialog-actions .btn.hub-dlg-minor {
  align-self: center; margin-right: auto; padding: 7px 12px; font-size: 0.8rem; color: var(--ink-dim);
}
.hub-dialog-actions .btn.hub-dlg-minor:hover { color: var(--ink); }
.hub-dialog .btn.is-busy { opacity: 1; }
.hub-dialog.is-busy .hub-dialog-x { opacity: 0.4; cursor: default; }

/* The one-time Discord pings offer after a Follow (HubUI.offerDiscordPings):
   the Discord glyph in the site's Discord tile, then the question and its
   small print, which links the channel. */
.hub-ping-offer { display: flex; align-items: flex-start; gap: 16px; }
.hub-ping-offer-ic {
  width: 44px; height: 44px; flex: none;
  display: grid; place-items: center;
  border-radius: 12px;
  background: rgba(88, 101, 242, 0.14);
  border: 1px solid rgba(88, 101, 242, 0.35);
  color: #8891f2;
}
.hub-ping-offer-ic svg { width: 22px; height: 22px; }
.hub-ping-offer-txt { flex: 1; min-width: 0; }
.hub-ping-offer-txt p { margin: 0; color: var(--ink-dim); }
.hub-ping-offer-txt .hub-ping-offer-s { margin-top: 8px; font-size: 0.86rem; color: var(--muted); }
.hub-ping-offer-s a { color: var(--ink-dim); text-decoration: underline; text-underline-offset: 2px; }
.hub-ping-offer-s a:hover { color: var(--ink); }
.hub-ping-offer-s a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

.hub-inline-error {
  display: flex; align-items: flex-start; gap: 9px;
  margin-top: 18px; padding: 10px 13px;
  font-size: 0.86rem; line-height: 1.5; color: #ffb3ba;
  background: rgba(255, 92, 108, 0.08);
  border: 1px solid rgba(255, 92, 108, 0.3);
  border-radius: var(--radius-sm);
}
.hub-inline-error svg { width: 16px; height: 16px; flex: none; margin-top: 2px; color: var(--danger); }
.hub-inline-error[hidden] { display: none; }

.hub-field-label { display: block; font-family: var(--font-display); font-weight: 600; font-size: 0.9rem; color: var(--ink); margin: 4px 0 8px; }
.hub-prompt-input { width: 100%; min-height: 96px; resize: vertical; }
.hub-prompt-foot { display: flex; align-items: flex-start; gap: 12px; margin-top: 8px; min-height: 18px; }
.hub-field-error { flex: 1; font-size: 0.8rem; color: #e5484d; }
.hub-field-error[hidden] { display: block; visibility: hidden; }
.hub-counter { margin-left: auto; font-size: 0.78rem; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.hub-counter.is-over { color: #e5484d; }

/* While a dialog is open, custom-select menus opened inside it sit
   above it, and the floating Discord button steps aside so it never
   covers a dialog's buttons on a phone. */
body.hub-dialog-open .cs-menu { z-index: 270; }
body.hub-dialog-open .fab-wrap { display: none; }

/* The parts of the team's dialogs (HubUI.statusDialog, removeDialog and
   the others), and of any dialog body built from them: a stack of parts
   16 px apart, the item the dialog acts on, the fields, one quiet line
   of consequence. */
.hub-dlg { display: grid; gap: 16px; }
.hub-dlg > p { margin: 0; }
.hub-dlg-target {
  display: grid; gap: 7px;
  padding: 12px 14px;
  background: var(--panel); border: 1px solid var(--line-soft); border-radius: 10px;
}
.hub-dlg-target b { color: var(--ink); font-weight: 600; font-size: 0.92rem; line-height: 1.4; overflow-wrap: anywhere; }
.hub-dlg-target .hub-chips { max-height: none; }
.hub-dlg-sub { font-size: 0.8rem; line-height: 1.45; color: var(--muted); overflow-wrap: anywhere; }
.hub-dlg-line { margin: 0; font-size: 0.86rem; line-height: 1.55; color: var(--muted); }
.hub-dlg-field { min-width: 0; }
.hub-dlg-field .hub-field-label { margin: 0 0 8px; }
.hub-dlg-field .cselect { width: 100%; }
.hub-dlg-hint { margin: 8px 0 0; font-size: 0.8rem; line-height: 1.45; color: var(--muted); }
.hub-dlg-hint[hidden] { display: none; }

/* The error line of a field with no counter under it (the Contributor
   picker, the pull request link) takes room only while it shows. */
.hub-dlg-claimant .hub-field-error, .hub-pr-dlg .hub-field-error { display: block; margin-top: 8px; }
.hub-dlg-claimant .hub-field-error[hidden], .hub-pr-dlg .hub-field-error[hidden] { display: none; }

/* PR Submitted's link field (HubUI.prDialog), and the link to a pull
   request wherever a page shows one (HubUI.prLink): a short label that
   wraps as one piece, so its new-tab icon never starts a line alone. */
.hub-pr-dlg .hub-pr-input { width: 100%; }
/* The word a typed confirm asks for (HubUI.typedConfirm). */
.hub-typed-dlg .hub-typed-input { width: 100%; }
.hub-pr-link { font-weight: 600; white-space: nowrap; }
.hub-pr-link .hub-ext { margin-left: 4px; vertical-align: -1px; }

/* A dialog's checkbox is site.css's .check (HubUI.checkbox draws it with
   .is-wrap, so its sentence wraps beside the box) - the same box the
   Admin page uses. In a dialog it takes the field rhythm. */
.hub-dlg .check.is-wrap { min-height: 24px; }
/* A hint that belongs to the checkbox above it: tucked under its label
   (the 18 px box plus the 11 px gap). */
.hub-dlg > .hub-dlg-hint-check { margin: -10px 0 0 29px; }

/* The people picker of the assignee dialog. */
.hub-people .search-wrap { min-width: 0; }
.hub-people-list { display: grid; gap: 4px; max-height: 320px; overflow-y: auto; }
.hub-people-list > .hub-skel + .hub-skel { margin-top: 4px; }
.hub-people-hint { margin: 6px 2px; font-size: 0.88rem; color: var(--muted); }
.hub-person-row {
  display: flex; align-items: center; gap: 12px; width: 100%;
  min-height: 48px; padding: 8px 12px;
  background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
  font: inherit; text-align: left; cursor: pointer;
  transition: border-color 0.18s ease, background-color 0.18s ease;
}
.hub-person-row .hub-person { flex: 1; min-width: 0; }
.hub-person-row:hover:not([disabled]) { background: rgba(255, 255, 255, 0.03); border-color: var(--line-soft); }
.hub-person-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.hub-person-row[disabled] { cursor: default; opacity: 0.5; }
.hub-person-row.is-current { border-color: var(--line-strong); }
.hub-person-role { flex: none; font-size: 0.78rem; color: var(--muted); white-space: nowrap; }

/* Edit histories (a comment's, a request's): each version a quiet card,
   the current one outlined. The text is shown whole - the dialog itself
   scrolls - so no line is ever cut in half. */
.hub-revs { display: grid; gap: 12px; }
.hub-rev {
  padding: 14px 16px;
  background: var(--panel);
  border: 1px solid var(--line-soft); border-radius: var(--radius);
}
.hub-rev.is-current { border-color: var(--line); }
.hub-rev-head {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px;
  font-size: 0.8rem; color: var(--muted);
}
.hub-rev-head b { font-family: var(--font-display); font-size: 0.92rem; color: var(--ink); }
.hub-rev-body { margin-top: 10px; font-size: 0.9rem; }
.hub-rev-none { margin: 0; font-size: 0.88rem; color: var(--muted); }

/* The cross-mode question of a merge. */
.notice.hub-merge-cross { margin: 0; align-items: flex-start; }
.hub-merge-cross-body { flex: 1; min-width: 0; display: grid; gap: 14px; }
.hub-merge-cross-body p { margin: 0; }
.hub-merge-cross-body .check-text { color: var(--ink-dim); }

@media (max-width: 760px) {
  .hub-dialog { padding: 22px 20px; }
  .hub-dialog-x { width: 40px; height: 40px; }
  .hub-dialog-actions .btn { flex: 1 1 auto; min-height: 40px; }
  /* The minor action keeps its own small width; the main one fills the rest. */
  .hub-dialog-actions .btn.hub-dlg-minor { flex: 0 1 auto; margin-right: 0; }
  /* A 16 px field, so iOS does not zoom into the dialog. */
  .hub-pr-dlg .hub-pr-input, .hub-typed-dlg .hub-typed-input { font-size: 16px; }
}

/* ---------------- legends ---------------- */

.hub-legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.hub-legend li { display: flex; align-items: flex-start; gap: 14px; }
/* Wide enough for the longest chip ("Implemented (Pending Approval)"),
   so every meaning starts on the same line. */
.hub-legend-chip { flex: none; width: 256px; display: flex; }
.hub-legend-text { font-size: 0.9rem; line-height: 1.55; color: var(--ink-dim); padding-top: 3px; }
.hub-legend-ico {
  width: 30px; height: 30px; flex: none;
  display: grid; place-items: center;
  border-radius: 8px;
  background: #212124; border: 1px solid var(--line-soft);
  color: var(--ink-dim);
}
.hub-legend-ico svg { width: 15px; height: 15px; }
.hub-help li { align-items: center; }
.hub-help .hub-legend-text { padding-top: 0; }
@media (max-width: 560px) {
  .hub-status-legend li { flex-direction: column; gap: 6px; }
  .hub-legend-chip { width: auto; }
}

/* ---------------- notices ---------------- */

.notice.hub-neutral { background: var(--panel-2); border-color: var(--line); color: var(--ink-dim); }
.notice.hub-neutral svg { color: var(--muted); }
.notice.hub-danger { background: rgba(255, 92, 108, 0.08); border-color: rgba(255, 92, 108, 0.3); }
.notice.hub-danger svg { color: var(--danger); }

/* ---------------- skeletons ---------------- */

/* The site's shimmer (its own keyframes), so a hub page loads the way
   every other page does. */
.hub-skel {
  display: block;
  border-radius: var(--radius);
  background: linear-gradient(100deg, var(--panel) 40%, var(--panel-3) 50%, var(--panel) 60%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
  border: 1px solid var(--line-soft);
}
.hub-skel-row { height: 88px; }
.hub-skel-row.is-56 { height: 56px; }
.hub-skel-shelf { height: 260px; border-radius: 14px; }
.hub-skel-title { height: 40px; width: 60%; border-radius: 10px; }
.hub-skel-block { height: 120px; }
.hub-skel-card { height: 200px; }
.hub-skel-tile { height: 80px; }
.hub-skel-comment { height: 76px; }
.hub-skel-readout { display: inline-block; width: 150px; height: 16px; border-radius: 999px; vertical-align: middle; }

/* ---------------- empty and error states ---------------- */

/* The site's designed empty state (the team pages had it first):
   dashed card, ring icon, a title, one helpful line and a way forward. */
.hub-empty, .hub-error {
  text-align: center;
  padding: 56px 24px;
  border: 1px dashed var(--line-soft);
  border-radius: var(--radius-lg);
  background: rgba(255, 255, 255, 0.012);
  animation: hub-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.hub-empty-ring {
  width: 62px; height: 62px; margin: 0 auto 18px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: #212124; border: 1px solid var(--line);
  color: var(--ink-dim);
}
.hub-empty-ring svg { width: 26px; height: 26px; }
.hub-empty.is-celebrate .hub-empty-ring { background: #16302a; border-color: var(--line-accent); color: var(--accent); }
.hub-error .hub-empty-ring { background: #2a1a1c; border-color: rgba(255, 92, 108, 0.35); color: var(--danger); }
.hub-empty h3, .hub-error h3 { font-size: 1.05rem; color: var(--ink-dim); margin: 0 0 6px; }
.hub-empty p, .hub-error p { font-size: 0.9rem; line-height: 1.6; color: var(--muted); margin: 0 auto; max-width: 420px; }
.hub-empty-actions { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px 18px; margin-top: 22px; }
.hub-empty-link { font-size: 0.88rem; font-weight: 600; }
button.hub-empty-link { background: none; border: 0; padding: 10px 0; cursor: pointer; font-family: inherit; color: var(--accent); }

/* ---------------- comments ---------------- */

.hub-comment { padding: 18px 0; border-bottom: 1px solid var(--line-soft); }
.hub-comment:last-child { border-bottom: 0; }
.hub-comment.is-reply { padding: 14px 0 0; border-bottom: 0; }
.hub-comment-row { display: flex; gap: 14px; }
.hub-comment-main { flex: 1; min-width: 0; }
.hub-comment-head {
  display: flex; align-items: center; gap: 4px 9px; flex-wrap: wrap;
  font-size: 0.78rem; color: var(--muted);
  min-height: 24px;
}
.hub-comment-name { font-size: 0.94rem; font-weight: 600; color: var(--ink); }
.hub-comment-to, .hub-comment-moved, .hub-comment-pin { display: inline-flex; align-items: center; gap: 5px; }
.hub-comment-to svg, .hub-comment-moved svg, .hub-comment-pin svg { width: 13px; height: 13px; }
.hub-comment-to { color: var(--muted); }
.hub-comment-pin {
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-dim);
}
.hub-comment-moved {
  font-size: 0.7rem; font-weight: 600;
  padding: 1px 8px; border-radius: 999px;
  background: #212124; border: 1px solid var(--line-soft); color: var(--ink-dim);
}
.hub-comment-edited { color: var(--muted); text-decoration: underline dotted; text-decoration-color: var(--line-strong); text-underline-offset: 3px; cursor: help; }
.hub-comment-body { margin-top: 4px; }
.hub-comment-body .hub-md { font-size: 0.95rem; }
.hub-comment-gone { margin: 2px 0 0; font-size: 0.92rem; font-style: italic; color: var(--muted); }
.hub-comment-kept {
  margin-top: 10px; padding: 10px 13px;
  background: var(--bg-2);
  border: 1px dashed var(--line); border-radius: var(--radius-sm);
}
.hub-comment-kept-label {
  display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; color: var(--muted);
}
.hub-comment-kept-label svg { width: 13px; height: 13px; }
.hub-comment-kept .hub-md { color: var(--muted); font-size: 0.9rem; }
.hub-comment.is-pinned > .hub-comment-row { position: relative; }
.hub-comment.is-pinned { background: linear-gradient(90deg, rgba(255, 255, 255, 0.02), transparent 60%); }
.hub-comment.is-sending { opacity: 0.55; }

.hub-comment-shots { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.hub-shot {
  width: 120px; aspect-ratio: 16 / 9;
  padding: 0; overflow: hidden; cursor: zoom-in;
  border: 1px solid var(--line-soft); border-radius: 8px;
  background: var(--bg-2);
  transition: border-color 0.18s ease;
}
.hub-shot img { width: 100%; height: 100%; object-fit: cover; }
.hub-shot:hover { border-color: var(--line-strong); }
.hub-shot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.hub-comment-actions { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; margin: 6px 0 -6px -10px; }
.hub-comment-act {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 36px; padding: 6px 10px;
  background: none; border: 0; border-radius: 8px; cursor: pointer;
  font-family: var(--font-body); font-size: 0.8rem; font-weight: 600;
  color: var(--muted);
  transition: color 0.15s ease, background-color 0.15s ease;
}
.hub-comment-act svg { width: 14px; height: 14px; }
.hub-comment-act:hover { color: var(--ink); background: rgba(255, 255, 255, 0.04); }
.hub-comment-act.is-danger:hover { color: var(--danger); }
.hub-comment-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.hub-comment-actions .hub-more { width: 36px; height: 36px; }

/* Replies hang 50 px in on a 2 px rail (28 px on phones). */
.hub-replies { margin: 6px 0 0 32px; padding-left: 16px; border-left: 2px solid var(--line-soft); }
@media (max-width: 640px) {
  .hub-replies { margin-left: 12px; padding-left: 14px; }
  .hub-comment-row { gap: 11px; }
  .hub-comment-act { min-height: 40px; }
  .hub-comment-actions .hub-more { width: 40px; height: 40px; }
}

/* ---------------- editor ---------------- */

textarea.hub-autogrow { overflow-y: hidden; }
.hub-editor-empty { margin: 0; font-size: 0.9rem; font-style: italic; color: var(--muted); }

/* ---------------- motion helpers ---------------- */

/* Removal: slide out to the right, then the gap closes. */
.hub-leaving {
  overflow: hidden;
  opacity: 0;
  transform: translateX(24px);
  max-height: 0 !important;
  margin-top: 0 !important; margin-bottom: 0 !important;
  padding-top: 0 !important; padding-bottom: 0 !important;
  border-color: transparent !important;
  transition: opacity 0.3s ease, transform 0.3s ease,
              max-height 0.38s ease 0.06s, margin 0.38s ease 0.06s,
              padding 0.38s ease 0.06s, border-color 0.3s ease;
}

/* The item a member came back to, outlined for two seconds. */
.hub-pulse { animation: hub-pulse 2s ease-out; }
@keyframes hub-pulse {
  0%, 60% { box-shadow: 0 0 0 2px var(--line-accent); }
  100%    { box-shadow: 0 0 0 2px transparent; }
}

/* ---------------- the small segmented control ----------------
   For filters and switches inside a section (the mode filters, the
   discussion's Oldest / Newest, Write / Preview). Declared before the
   phone rule below, which must win on phones. */

.seg.hub-seg-sm { padding: 4px; }
.seg.hub-seg-sm::before { top: 4px; bottom: 4px; }
.seg.hub-seg-sm button { padding: 7px 15px; font-size: 0.82rem; }

/* A strip wider than its box scrolls sideways (the site's .seg does).
   HubUI.segmented marks it and keeps the chosen button in view; the
   edges fade towards the buttons that are out of sight. */
.seg.hub-seg-overflow {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 28px, #000 calc(100% - 44px), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 28px, #000 calc(100% - 44px), transparent 100%);
}
.seg.hub-seg-overflow.hub-seg-start {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 44px), transparent 100%);
  mask-image: linear-gradient(90deg, #000 calc(100% - 44px), transparent 100%);
}
.seg.hub-seg-overflow.hub-seg-end {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 28px);
  mask-image: linear-gradient(90deg, transparent 0, #000 28px);
}

/* ---------------- phones: tap targets ---------------- */

@media (max-width: 760px) {
  .hub-pop .btn-sm, .hub-empty .btn, .hub-error .btn { min-height: 40px; }
  .hub-linkbtn { padding: 12px 2px; margin: -12px -2px; }
  .seg.hub-seg-sm button { padding: 10px 14px; min-height: 40px; }
  /* The floating Discord button sits in the bottom-right corner; a tab
     strip that scrolls can always bring its last tab out from under it. */
  .seg.hub-seg-overflow[role="tablist"] { padding-right: 64px; }
}

/* ---------------- reduced motion ---------------- */

@media (prefers-reduced-motion: reduce) {
  .hub-row, .hub-shelf, .hub-empty, .hub-error,
  .hub-pop, .hub-menu, .hub-dialog-overlay, .hub-dialog,
  .hub-detail-head, .hub-detail-grid.is-live > :not(.hub-detail-lead),
  .hub-toolbar.is-open .hub-filter, .hub-toolbar.is-open .hub-sort,
  .hub-fact.is-in .hub-fact-in { animation: none; }
  .hub-fchip, .hub-fchip svg, .hub-qb-btn, .hub-person-row, .hub-filters-btn,
  .hub-hero-copy .hub-fact { transition: none; }
  .hub-vote-num > span.is-in, .hub-vote-num > span.is-out { animation: none; }
  .hub-vote.is-pop .hub-vote-chev svg, .hub-vote.is-pop .hub-vote-ico svg { animation: none; }
  .hub-skel { animation: none; background: var(--panel); }
  .hub-spin { animation: none; }
  .hub-pulse { animation: none; box-shadow: 0 0 0 2px var(--line-accent); }
  .hub-leaving { transition: opacity 0.2s ease; transform: none; }
  .hub-pips i, .hub-vote-chev, .hub-back svg, .hub-shelf-all svg,
  .hub-row, .hub-mini, .hub-match { transition: none; }
  @media (hover: hover) {
    .hub-row:hover { transform: none; }
    .hub-vote:not(.is-blocked) button.hub-vote-tile:hover .hub-vote-chev { transform: none; }
  }
}
