/* ─────────────────────────────────────────────────────────────────────────
   qx-ui.css — shared UI + accessibility layer (run #63 Segment C)

   WHY A SHARED FILE ON A SITE THAT IS OTHERWISE INLINE-STYLED. Every page
   here carries its own inline styles by convention, and that convention is
   kept for layout. But the things in this file are the same on every page by
   definition — a focus ring, a skip link, a back-to-top button — and copying
   them 18 times is how they drift apart. Same-origin, so the enforcing CSP
   (`style-src 'self'`) allows it with no new host.

   The colour tokens live in each page's own :root block; this file only
   consumes them, so it never fights a page for control of the palette.
   ──────────────────────────────────────────────────────────────────────── */

/* ── FOCUS (WCAG 2.4.7) ───────────────────────────────────────────────────
   The site had no focus styling at all, so a keyboard user could not see
   where they were. :focus-visible so a mouse click does not paint a ring. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--qx-link);
  outline-offset: 3px;
  border-radius:var(--qx-radius-tight);
}
/* Never remove the ring without replacing it. */
:focus:not(:focus-visible) { outline: none; }

/* ── SKIP LINK (WCAG 2.4.1) — first tab stop, visible only on focus ─────── */
.qx-skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
}
.qx-skip:focus {
  left: 12px; top: 12px;
  padding: 10px 16px;
  background: var(--qx-brand); color: #fff;
  border-radius:var(--qx-radius-tight); font-size: 14px; font-weight: 600;
  font-family: 'Inter Tight', system-ui, sans-serif;
  text-decoration: none;
}

/* ── LINKS IN PROSE (WCAG 1.4.1) ──────────────────────────────────────────
   axe flagged `link-in-text-block` on nine pages: a link sitting inside a
   paragraph was distinguished from the surrounding text by COLOUR ALONE,
   which anyone with a colour-vision deficiency cannot see. Underlining the
   ones inside running text fixes it without touching nav links, buttons or
   card links, where the link is obvious from position and shape. */
main p a:not([class]),
main li a:not([class]),
main div[role="note"] a,
main section > div > a:not([class]),
.qx-storage p a,
footer div a[href^="mailto"],
p.qx-sum a {
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
main p a:not([class]):hover,
main li a:not([class]):hover { text-decoration-thickness: 2px; }

/* ── THE FLOATING RAIL (run #69) — ONE owner for the bottom-right stack ──
   THE DEFECT, measured on live production at 1440x900 before it was touched:
   three independently-positioned fixed controls piled into the same corner.

     .qx-top          right:20px bottom:20px (74px when stacked)  z-index 90
     .qx-contact      right:20px bottom:20px                      z-index 90
     #qx-theme-toggle right:16px bottom:16px                      z-index 2147483000

   The toggle was injected inline by run #67 and was never told the other two
   existed. Being four pixels off and nine orders of magnitude above them, its
   91px-wide "◑ System" pill painted straight over the contact button:
   `document.elementFromPoint()` at the contact button's own centre returned
   the toggle, at BOTH 1440px and 390px. The floating contact button was not
   merely ugly — it was unclickable on every page of the site.

   So geometry stops being three opinions and becomes one flex column. The
   rail owns the edge offset, the gap and the alignment; the controls own only
   their own chrome. Each keeps a fixed-position fallback for the instant
   before it is adopted, and `.qx-rail >` overrides it once it is in.

   `pointer-events:none` on the rail with `auto` on its children matters: the
   rail is a tall invisible column and without it the GAPS between the buttons
   would swallow clicks meant for the page underneath. */
.qx-rail {
  position: fixed;
  right: var(--qx-rail-edge); bottom: var(--qx-rail-edge);
  z-index: 90;
  --qx-rail-edge: 20px;
  --qx-rail-gap: 10px;
  --qx-rail-size: 42px;
  display: flex; flex-direction: column; align-items: flex-end;
  gap: var(--qx-rail-gap);
  pointer-events: none;
}
.qx-rail > * { pointer-events: auto; }
/* NOT the legal pages' old `#qxTop`. Those five shipped their own back-to-top
   INSIDE their `<x-dc>` template, so the DC runtime owned that node; moving it
   into the rail made the runtime render a REPLACEMENT and the page ended up
   with two identical ids on two focusable buttons — a duplicate active id, on
   a site that measures 0 axe violations. Verified against live first: unmodified
   production has exactly one, so the reparenting caused it. The answer was not
   a workaround but to stop having two implementations of one button, so #69
   deleted the inline one. Never reparent a node the DC runtime templates. */
.qx-rail > .qx-top,
.qx-rail > .qx-contact,
.qx-rail > #qx-theme-toggle {
  position: static; right: auto; bottom: auto; margin: 0;
}
.qx-rail > .qx-top,
.qx-rail .qx-contact__btn,
.qx-rail > #qx-theme-toggle {
  width: var(--qx-rail-size); height: var(--qx-rail-size);
}
/* RUN #72. This used to read `--qx-rail-size: 40px`, i.e. the three floating
   controls got SMALLER on a phone than on a desktop — backwards, and 40 is
   under the 44px WCAG 2.5.5 target. Alex's whole round of feedback came from
   a phone, and the theme toggle is one of the three. The edge and the gap
   still tighten (a phone has less margin to give away); the TARGET does not. */
@media (max-width: 420px) {
  .qx-rail { --qx-rail-edge: 14px; --qx-rail-gap: 9px; --qx-rail-size: 44px; }
}

/* ── THEME TOGGLE (run #67, re-homed here by #69) ──────────────────────────
   It used to carry its own <style> built by the inline script. That is how it
   ended up with a geometry nothing else knew about, and a hard-coded
   `box-shadow: 0 6px 22px rgba(0,0,0,.28)` that is a black bruise under a
   light theme. It now matches its two neighbours exactly — same size, same
   scrim, same hairline, same blur — so the three read as one control group
   rather than three accidents. */
#qx-theme-toggle {
  position: fixed; right: 20px; bottom: 20px; z-index: 90;
  width: 42px; height: 42px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font: 600 15px/1 'Inter Tight', system-ui, sans-serif;
  background: var(--qx-scrim-card);
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  color: var(--qx-ink-3); cursor: pointer;
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  transition: color .15s, border-color .15s, transform .15s;
}
#qx-theme-toggle:hover { color: var(--qx-ink); border-color: var(--qx-line-loud, rgba(var(--qx-hairline-rgb),0.18)); }
#qx-theme-toggle:active { transform: scale(0.94); }
#qx-theme-toggle .qx-tt-i { line-height: 1; }

/* The global focus rule below sets a small radius on whatever takes focus,
   which is right for a link or a text input and squares off a round button the
   moment it takes focus. These three are circles; keep them circular. */
.qx-top:focus-visible,
.qx-contact__btn:focus-visible,
#qx-theme-toggle:focus-visible { border-radius: 50%; }

/* ── BACK TO TOP (C6) ─────────────────────────────────────────────────── */
.qx-top {
  position: fixed; right: 20px; bottom: 20px; z-index: 90;
  width: 42px; height: 42px; border-radius: 50%;
  display: none; align-items: center; justify-content: center;
  background: var(--qx-scrim-card);
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  color: var(--qx-ink-3); cursor: pointer;
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  transition: color .15s, border-color .15s, transform .15s;
}
.qx-top[data-show="1"] { display: flex; }
.qx-top:hover { color: var(--qx-ink); border-color: var(--qx-line-loud, rgba(var(--qx-hairline-rgb),0.18)); }
.qx-top:active { transform: scale(0.94); }

/* ── FLOATING CONTACT (C16) — an icon, not a widget with a pitch ───────── */
.qx-contact {
  position: fixed; right: 20px; bottom: 20px; z-index: 90;
  display: flex; align-items: center; gap: 0;
}
.qx-contact__btn {
  width: 42px; height: 42px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--qx-scrim-card);
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  color: var(--qx-ink-3); cursor: pointer;
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
}
.qx-contact__btn:hover { color: var(--qx-ink); }
.qx-contact__menu {
  position: absolute; right: 0; bottom: 52px;
  min-width: 178px; padding: 6px;
  background: var(--qx-surface);
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  border-radius:var(--qx-radius-base);
  box-shadow: var(--qx-elev-overlay);
  display: none; flex-direction: column; gap: 2px;
}
.qx-contact[data-open="1"] .qx-contact__menu { display: flex; }
.qx-contact__menu a {
  display: flex; align-items: center; gap: 9px;
  padding: 9px 11px; border-radius:var(--qx-radius-tight);
  font-size: 13px; color: var(--qx-ink-2); text-decoration: none;
  font-family: 'Inter Tight', system-ui, sans-serif;
}
.qx-contact__menu a:hover { background: var(--qx-line-soft, rgba(var(--qx-hairline-rgb),0.06)); color: var(--qx-ink); }

/* ── STORAGE NOTICE (C4) ──────────────────────────────────────────────────
   One sentence and two words of button. There are no third-party tracking
   cookies to consent to — the site sets no cookies at all — so this is a
   disclosure, not a consent gate, and it does not block the page. */
.qx-storage {
  position: fixed; left: 16px; right: 16px; bottom: 16px; z-index: 95;
  max-width: 560px; margin: 0 auto;
  display: none; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 13px 16px;
  background: var(--qx-scrim-card);
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  border-radius:var(--qx-radius-base);
  box-shadow: var(--qx-elev-overlay);
  font-family: 'Inter Tight', system-ui, sans-serif;
  font-size: 12.5px; line-height: 1.55; color: var(--qx-ink-3);
}
.qx-storage[data-show="1"] { display: flex; }
/* Leave room for the floating contact button rather than sitting under it. */
@media (max-width: 700px) {
  .qx-storage { right: 74px; }
}
.qx-storage p { margin: 0; flex: 1 1 300px; }
.qx-storage a { color: var(--qx-link); }
.qx-storage button {
  flex: 0 0 auto; cursor: pointer;
  background: var(--qx-line-soft, rgba(var(--qx-hairline-rgb),0.06));
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  color: var(--qx-ink);
  font-family: inherit; font-size: 12.5px; font-weight: 600;
  padding: 7px 15px; border-radius:var(--qx-radius-tight);
}
.qx-storage button:hover { background: var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11)); }

/* ── MOBILE MENU (C9) ─────────────────────────────────────────────────── */
/* 44px, not 38px (run #72) — these two are touch-only controls by definition. */
.qx-burger {
  display: none; width: 44px; height: 44px; border-radius:var(--qx-radius-tight);
  align-items: center; justify-content: center;
  background: transparent;
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  color: var(--qx-ink-2); cursor: pointer;
}
.qx-drawer {
  position: fixed; inset: 0; z-index: 150; display: none;
}
.qx-drawer[data-open="1"] { display: block; }
.qx-drawer__scrim { position: absolute; inset: 0; background: var(--qx-overlay-soft); }
.qx-drawer__panel {
  position: absolute; top: 0; right: 0; bottom: 0; width: min(84vw, 320px);
  background: var(--qx-bg);
  border-left: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  padding: 18px; display: flex; flex-direction: column; gap: 4px;
  overflow-y: auto;
}
.qx-drawer__panel a {
  padding: 13px 12px; border-radius:var(--qx-radius-base);
  font-family: 'Inter Tight', system-ui, sans-serif;
  font-size: 15px; color: var(--qx-ink-2); text-decoration: none;
}
.qx-drawer__panel a:hover { background: var(--qx-line-soft, rgba(var(--qx-hairline-rgb),0.06)); color: var(--qx-ink); }
.qx-drawer__close {
  align-self: flex-end; width: 44px; height: 44px; border-radius:var(--qx-radius-tight);
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  color: var(--qx-ink-2); cursor: pointer; margin-bottom: 8px;
}
body.qx-noscroll { overflow: hidden; }
@media (max-width: 720px) {
  .qx-burger { display: flex; }
}

/* ── PASSWORD VISIBILITY (C13) ────────────────────────────────────────── */
/* RUN #77: the wrapper is gone — it reparented a React-owned input. The
   input's EXISTING parent becomes the positioning context instead, and the
   button is anchored to the input's own box by inline top/right that qx-ui.js
   re-measures on every pass. .qx-pw-wrap is kept for one release so a cached
   qx-ui.js paired with a fresh qx-ui.css still positions correctly. */
.qx-pw-wrap { position: relative; display: flex; flex-direction: column; }
.qx-pw-host { position: relative; }
.qx-pw-toggle {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  width: 30px; height: 30px; border-radius:var(--qx-radius-tight);
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: none; cursor: pointer;
  color: var(--qx-ink-4);
}
.qx-pw-toggle:hover { color: var(--qx-ink-2); }
.qx-pw-wrap input,
.qx-pw-host input[data-qx-pw="1"] { padding-right: 40px !important; }

/* ── COPY BUTTON (C8) ─────────────────────────────────────────────────── */
.qx-copy {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  background: var(--qx-line-soft, rgba(var(--qx-hairline-rgb),0.06));
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
  color: var(--qx-ink-2);
  font-family: 'Inter Tight', system-ui, sans-serif;
  font-size: 11.5px; font-weight: 600; padding: 6px 12px; border-radius:var(--qx-radius-tight);
}
.qx-copy:hover { color: var(--qx-ink); }
.qx-copy[data-done="1"] { color: var(--qx-success); border-color: rgba(var(--qx-success-rgb),0.45); }

/* ── SCROLL PROGRESS (C7) — long documents only ───────────────────────── */
.qx-progress {
  position: fixed; top: 0; left: 0; height: 2px; width: 0%;
  background: var(--qx-brand); z-index: 160;
  transition: width .1s linear;
}

/* ── DISCLOSURE (run #69) ───────────────────────────────────
   The same mechanics as .qx-faq, for a single collapsible block rather than a
   list of them. Used by macro.html's "How to read the week": the writing is
   the only thing on the site explaining the product's premise, so it is
   collapsed, not deleted — and its thesis sentence stays visible in the
   summary so a reader who never opens it still gets the claim. */
.qx-disclose > summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: flex-start; gap: 14px;
}
.qx-disclose > summary::-webkit-details-marker { display: none; }
.qx-disclose > summary::marker { content: ''; }
.qx-disclose__chev { transition: transform .18s ease; flex: 0 0 auto; margin-left: auto; margin-top: 2px; }
.qx-disclose[open] .qx-disclose__chev { transform: rotate(180deg); }

/* ── STICKY DAY HEADER (run #69, macro wire) ───────────────────────
   MEASURED, not assumed: the nav is position:fixed and stands 69px at 1440px
   but 157px at 390px, where it wraps to three rows. Pinning a second bar under
   a 157px nav would spend a fifth of an 844px viewport on chrome, so the day
   header sticks on desktop only and scrolls normally on a phone. */
@media (min-width: 721px) {
  [data-qx-stream-day] { position: sticky; top: 69px; z-index: 2; }
}

/* ── MACRO WIRE ROW (run #69) ────────────────────────────────
   One dense line per event instead of an 82px card. The layout lives here
   rather than inline because a narrow viewport has to reflow it and an inline
   grid-template-columns cannot be overridden by a media query.

   MEASURED: three columns at 390px leave ~150px for the headline, so it wrapped
   to four lines and the row came out at 115px — TALLER than the card it
   replaced. Below 560px the row stacks instead: impact and age share the first
   line, the headline and its numbers take the second. */
.qx-wire-row {
  display: grid; grid-template-columns: 76px minmax(0,1fr) auto;
  gap: 12px; align-items: baseline; padding: 8px 2px;
}
.qx-wire-imp { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.qx-wire-main { min-width: 0; }
.qx-wire-time { white-space: nowrap; }
@media (max-width: 560px) {
  .qx-wire-row { grid-template-columns: 1fr auto; gap: 5px 10px; align-items: center; }
  .qx-wire-row > .qx-wire-imp  { order: 1; }
  .qx-wire-row > .qx-wire-time { order: 2; }
  .qx-wire-row > .qx-wire-main { order: 3; grid-column: 1 / -1; }
}

/* ── FAQ ACCORDION (C15) ──────────────────────────────────────────────── */
.qx-faq summary { cursor: pointer; list-style: none; }
.qx-faq summary::-webkit-details-marker { display: none; }
.qx-faq summary::marker { content: ''; }
.qx-faq__chev { transition: transform .18s ease; flex: 0 0 auto; }
.qx-faq details[open] .qx-faq__chev { transform: rotate(180deg); }
.qx-faq details { scroll-margin-top: 96px; }
.qx-faq details:target { border-color: var(--qx-link) !important; }

/* ── MOTION (WCAG 2.3.3) — applies to what already existed too ────────── */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .qx-progress { transition: none !important; }
}

/* ── PRINT (C10) ──────────────────────────────────────────────────────── */
@media print {
  .qx-rail, .qx-top, .qx-contact, .qx-storage, .qx-progress, .qx-skip,
  #qx-theme-toggle,
  .qx-burger, .qx-drawer, nav { display: none !important; }
  body { background: #fff !important; color: #000 !important; }
  a { color: #000 !important; text-decoration: underline; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; color: #555; }
  .qx-faq details { page-break-inside: avoid; }
  .qx-faq details > div { display: block !important; }
}

/* ── TOUCH TARGETS (run #72) ───────────────────────────────────────────────
   Measured, not guessed: a 390px probe across all 18 pages in both themes
   listed every visible control under 44px. Most of what it returned is a
   4-7px shortfall on a button 288px wide, which nobody misses with a thumb.
   These two are the ones that are genuinely hard to hit, and one of them is
   the exact control Alex reported as broken.

   Inline links inside a paragraph are deliberately NOT touched: WCAG 2.5.8
   exempts them, and padding them out would break the line box they sit in. */

/* "LINK TO THIS ANSWER" measures 132 x 13. Making it copy (see faq.html) is
   worth little if it stays a 13px-tall target. The hit area is enlarged with
   an overlay rather than with padding, so the answer card does not grow by
   30px on every one of nine answers — and because the click handler is
   delegated and resolves with closest(), a tap on the overlay still lands on
   the anchor. Nothing above or below it is interactive, so the enlarged box
   cannot steal a tap from anything else. */
.qx-faq a[href^="#"] { position: relative; }
.qx-faq a[href^="#"]::after {
  content: ''; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%);
  min-width: 44px; height: 44px;
}

/* Six 15px-tall legal links in one row is the fiddliest thing in the footer,
   and on a phone it is the row people reach for when they want the privacy
   policy. The horizontal margin is negative so the row does not get WIDER
   and start wrapping differently; only the height grows. */
@media (max-width: 720px) {
  footer a {
    display: inline-flex; align-items: center;
    min-height: 44px; min-width: 24px;
    padding-inline: 8px; margin-inline: -8px;
  }
}


/* ── RUN #73 · THE MOBILE HEADER IS ONE BAR ───────────────────────────────
   MEASURED BEFORE (live, 390px): the marketing nav was 136px and FIVE visual
   rows — wordmark, tagline, Download, Log in, then Features/Pricing/FAQ. The
   member nav was 119px and three. Run #69 measured 157px and solved it by
   padding every h1 clear of it, which treated the symptom; the tab row was
   even FORCED onto its own line by an `order:3; flex:1 0 100%` rule.

   The drawer was never missing. #63B's is focus-trapped, closes on Escape and
   on navigation, and locks body scroll, and qx-ui.js re-attaches the burger
   when the DC runtime removes it. What was missing is that NOTHING HID THE
   LINKS THE DRAWER DUPLICATES — so the burger opened a panel listing the five
   links already visible underneath it.

   So: hide them, and the bar is one row. Nothing here invents a new
   component. */
@media (max-width: 720px) {
  #qxNav {
    height: 60px !important;
    grid-template-columns: 1fr auto !important;   /* wordmark | burger */
    gap: 12px !important;
    padding: 0 16px !important;
  }
  /* Everything the drawer already carries leaves the bar. */
  #qxNavTabs { display: none !important; }
  #qxNavActions > a { display: none !important; }   /* Log in, Download */
  #qxNavActions { gap: 0 !important; justify-self: end !important; }
  /* Decoration that was costing a whole row. It returns at desktop unchanged.
     Three selectors because three pages already named this span themselves —
     index/faq call it #qxLockupTag, the auth pages #qxNavTag — and adopting the
     names they already have beats editing four files to invent a fourth. */
  .qx-nav__tag, #qxLockupTag, #qxNavTag { display: none !important; }

  /* CLEARANCE, RE-DERIVED from the 60px bar rather than padded against the old
     one. Every one of these was solved against a 130-157px nav, so leaving them
     alone would have left a 70-100px hole under the header on all 18 pages.
     #qxAuthMain is DELIBERATELY ABSENT: login and reset-password are
     full-height `min-height:100vh` split-screen grids, not padded content
     columns, and their nav is not fixed. Padding them does not clear a header,
     it just shoves the whole auth layout down — measured, 131px -> 215px,
     which is how this exclusion was found rather than guessed. */
  #qxMain, #qxFaqMain, #qxDoc,
  nav ~ main:not(#qxAuthMain) { padding-top: 84px !important; }
  #qxHero { padding-top: 84px !important; }
}

/* The drawer's Download is the main action, so it looks like one. Marked by
   qx-ui.js from the link's own href, never by matching its label — the label
   is copy and copy changes. */
.qx-drawer__panel .qx-drawer__sep {
  height: 1px; margin: 10px 2px;
  background: var(--qx-line-strong, rgba(var(--qx-hairline-rgb),0.11));
}
.qx-drawer__panel a[data-qx-primary] {
  margin-top: 2px; text-align: center; justify-content: center;
  background: linear-gradient(180deg, var(--qx-brand-2), var(--qx-brand-3));
  color: #fff; font-weight: 600;
  box-shadow: 0 8px 22px -8px var(--qx-brand-glow-2);
}
.qx-drawer__panel a[data-qx-primary]:hover { filter: brightness(1.12); color: #fff; }


/* ── RUN #73 · TAP TO EXPAND ──────────────────────────────────────────────
   Measured at 390px: the product screenshots render at 10-25% of natural size
   (a 1906x1007 GEX profile into 339x179), and none of them was clickable.
   A dense dashboard at 18% is not a small picture, it is an unreadable one.

   No external lightbox: the CSP is `self` and adding a host would be a policy
   change this does not need. ~90 lines of CSS and ~90 of JS instead. */
.qx-zoomable { position: relative; }
/* The affordance. Alex's words were "clicking on these things" — he WAS
   tapping them, so the fix is not only that a tap works but that it looks
   like it will. */
.qx-zoom-btn {
  position: absolute; inset: 0; z-index: 3;
  display: flex; align-items: flex-end; justify-content: flex-end;
  padding: 10px; background: transparent; border: 0; cursor: zoom-in;
  color: var(--qx-ink); font: inherit;
}
.qx-zoom-btn__hint {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 6px 10px;
  border-radius: var(--qx-radius-tight);
  background: var(--qx-scrim-strong, rgba(var(--qx-ground-rgb), 0.86));
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb), 0.11));
  color: var(--qx-ink-2);
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase;
  transition: background 120ms ease, color 120ms ease;
}
.qx-zoom-btn:hover .qx-zoom-btn__hint,
.qx-zoom-btn:focus-visible .qx-zoom-btn__hint {
  background: var(--qx-surface); color: var(--qx-ink);
}
/* The tap target is the whole image, so on a phone the hint is a LABEL, not a
   button-within-a-button. 44px is satisfied by the parent, not by this chip. */
@media (max-width: 720px) { .qx-zoom-btn { padding: 8px; } }

.qx-zoom {
  position: fixed; inset: 0; z-index: 200; display: none;
  background: var(--qx-overlay, rgba(0,0,0,0.75));
}
.qx-zoom[data-open="1"] { display: flex; flex-direction: column; }
.qx-zoom__bar {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 10px 12px;
  background: var(--qx-scrim-strong, rgba(var(--qx-ground-rgb), 0.9));
  border-bottom: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb), 0.11));
}
.qx-zoom__label {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10px; letter-spacing: 0.14em; color: var(--qx-ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qx-zoom__close {
  flex: 0 0 auto; width: 44px; height: 44px; border-radius: var(--qx-radius-tight);
  display: flex; align-items: center; justify-content: center;
  background: transparent;
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb), 0.11));
  color: var(--qx-ink-2); cursor: pointer;
}
.qx-zoom__close:hover { color: var(--qx-ink); }
/* Scrollable at NATURAL size — that is the whole point. Pinch-zoom still works
   on top of this because the page does not disable user-scalable. */
.qx-zoom__scroll {
  flex: 1 1 auto; overflow: auto; -webkit-overflow-scrolling: touch;
  display: flex; align-items: flex-start; justify-content: flex-start;
  padding: 12px;
}
.qx-zoom__scroll img {
  display: block; max-width: none; height: auto;
  border-radius: var(--qx-radius-tight);
  border: 1px solid var(--qx-line-strong, rgba(var(--qx-hairline-rgb), 0.11));
}
/* A wide screenshot that already fits gets centred instead of pinned left. */
.qx-zoom__scroll[data-fits="1"] { align-items: center; justify-content: center; }
.qx-zoom__scroll[data-fits="1"] img { max-width: 100%; }
@media (prefers-reduced-motion: reduce) {
  .qx-zoom-btn__hint { transition: none; }
}

/* ─────────────────────────────────────────────────────────────────────────
   RUN #77 — A RAW DOM EXCEPTION IS NOT MEMBER-FACING COPY.

   A member on /dashboard was shown, twice, in a red box over the page:

     dashboard: Failed to execute 'removeChild' on 'Node':
     The node to be removed is not a child of this node.

   That is the DC runtime's error boundary printing `__name + ": " + __err`
   into `.sc-logic-error` (support.js, and the matching `.sc-placeholder-error`
   inside its Placeholder). support.js is GENERATED from dc-runtime/src and
   says so at the top, its sources are not in this repo, and there is no build
   step here — so the runtime cannot be changed from qxsite. What CAN be
   changed is whether the text reaches a member's eyes.

   WHAT THIS DOES AND DOES NOT DO. It suppresses the two elements that render
   RAW EXCEPTION TEXT. It does NOT touch:
     · `console.error("[dc-runtime] render error in <name>:", e, componentStack)`
       — the runtime still logs the error and the full component stack, so
       diagnosis is unchanged and a future run can still see breakage;
     · the neutral `.sc-placeholder` skeleton the boundary renders in place of
       the failed subtree — the region still shows SOMETHING, so a genuinely
       broken page does not become a silent blank.
   A calm generic banner was considered and deliberately not added: the page
   around the failure renders normally, the runtime recovers, and a permanent
   "something went wrong" strip would be a second, larger piece of noise for a
   fault the member can neither cause nor fix.

   !important, and why: the runtime injects its own <style> into <head> at
   runtime, i.e. AFTER this linked stylesheet, so at equal specificity it wins
   the cascade. This is the one place in this file that needs to outrank it. */
.sc-logic-error,
.sc-host.sc-has-error > .sc-logic-error,
.sc-placeholder-error {
  display: none !important;
}
