/* =====================================================================
 * markdown-display.css — THE ONE stylesheet for assistant markdown.
 * =====================================================================
 *
 * CANONICAL SOURCE, and the only file any visual change to assistant output
 * is made in. `MarkdownDisplay.tsx` beside it emits almost no classes on
 * purpose: headings, paragraphs, lists, bold, blockquotes and table cells are
 * plain semantic elements, styled from here by descendant selectors. Restyling
 * bold text across ExactOps, ExactEstate and every published WBP site is
 * therefore a ONE LINE edit in ONE file — the owner's actual test.
 *
 * Copies are produced by `scripts/sync_markdown_display.py` and pinned by a
 * SHA-256 drift guard in each consumer. Do not edit a copy.
 *
 * WHY var() CHAINS INSTEAD OF ONE TOKEN VOCABULARY
 * ------------------------------------------------
 * The three surfaces do not share a token namespace and must not be forced to.
 * ExactOps paints `--color-*`; a published WBP site paints `--site-*` chosen by
 * the tenant's theme. Each rule therefore reads
 *   var(<ExactOps token>, var(<WBP token>, <neutral relative fallback>))
 * so the SAME bytes adapt to whichever host they land in, and a surface that
 * defines neither still renders legibly by deriving from `currentColor`.
 * That last link in the chain matters: it means this file contains NO raw hex
 * and NO palette literal, so it satisfies the token-discipline gate
 * (`web-react/scripts/check-token-discipline.mjs`) rather than needing an
 * exemption. Every var() here carries a fallback, which is also exactly what
 * that gate's `VAR_NO_FALLBACK` check requires of a token it cannot resolve.
 *
 * MOTION
 * ------
 * The breathing glow is defined as REAL @keyframes in this file, not borrowed
 * from a utility class. That is deliberate and was measured: this tree does not
 * install `tailwindcss-animate`, so its vocabulary (`animate-in`, `fade-in-0`,
 * `zoom-in-95`, `slide-in-from-*`) computes `animation-name: none` — a class
 * that animates nothing reads exactly like a fix. ExactOps' own
 * `.eo-glow-pulse` IS a real keyframe, but it lives in ExactOps' globals.css,
 * which a published WBP site never loads; a shared module that depended on it
 * would work in one repo and silently do nothing in the other. So the keyframes
 * ship HERE, with the markup that uses them.
 * ===================================================================== */

.md-display {
  /* ---- host token bridge. Change a colour for every surface here. ---- */
  --md-fg: var(--color-text, var(--site-fg, currentColor));
  --md-muted: var(--color-muted, var(--site-muted, currentColor));
  --md-accent: var(--color-accent, var(--site-link, var(--site-accent, currentColor)));
  --md-border: var(--color-border, var(--site-border, color-mix(in srgb, currentColor 18%, transparent)));
  --md-code-bg: var(--color-code-surface, var(--site-surface-2, color-mix(in srgb, currentColor 7%, transparent)));
  --md-code-border: var(--color-code-border, var(--site-border, color-mix(in srgb, currentColor 16%, transparent)));
  --md-head-bg: var(--color-surface-3, var(--site-surface-3, color-mix(in srgb, currentColor 5%, transparent)));
  --md-radius: var(--radius-input, 0.5rem);
  --md-radius-mark: var(--radius-mark, 0.25rem);
  --md-ease: var(--ease-out, cubic-bezier(0.2, 0.8, 0.2, 1));

  color: var(--md-fg, currentColor);
  font-size: 0.875rem;
  line-height: 1.6;
  /* min-width:0 lets this block SHRINK inside a flex/grid parent. Without it a
     long code line or wide table forces the whole chat column wider than the
     phone viewport and the page scrolls sideways. */
  min-width: 0;
}

/* A mixed-producer field whose text carried no markdown structure: keep the
   author's line breaks rather than collapsing them into one paragraph. */
.md-display--plain {
  white-space: pre-wrap;
  word-break: break-word;
}

/* ---------------------------------------------------------------------
   BLOCK RHYTHM. First/last margins collapse to zero so the renderer never
   adds phantom space at the top or bottom of a chat bubble.
   --------------------------------------------------------------------- */
.md-display > :first-child { margin-top: 0; }
.md-display > :last-child { margin-bottom: 0; }

.md-display p {
  margin: 0.4rem 0;
  overflow-wrap: break-word;
  word-break: break-word;
}

.md-display h1,
.md-display h2,
.md-display h3,
.md-display h4,
.md-display h5,
.md-display h6 {
  margin: 0.85rem 0 0.3rem;
  line-height: 1.3;
  font-weight: 650;
  color: var(--md-fg, currentColor);
  overflow-wrap: break-word;
}
.md-display h1 { font-size: 1.15em; }
.md-display h2 { font-size: 1.08em; }
.md-display h3 { font-size: 1em; }
/* h4-h6 become an eyebrow rather than yet another size step — below h3 a size
   ramp stops reading as hierarchy and starts reading as noise. */
.md-display h4,
.md-display h5,
.md-display h6 {
  font-size: 0.82em;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--md-muted, currentColor);
}

/* THE OWNER'S DEFECT: `**bold**` printed with its asterisks visible. Bold is
   the single most load-bearing mark in an assistant answer — it is how the
   model says "this is the part you asked for" — so it gets real weight and the
   full-strength foreground, not the body's inherited colour. */
.md-display strong,
.md-display b {
  font-weight: 700;
  color: var(--md-fg, currentColor);
}

.md-display em { font-style: italic; }

.md-display del { text-decoration: line-through; opacity: 0.7; }

/* ---------------------------------------------------------------------
   LISTS. The second half of the owner's defect: `- ` items rendered as plain
   hyphens. Markers are drawn with ::marker so their colour is the accent
   while the text stays foreground — the shape a reader scans by.
   --------------------------------------------------------------------- */
.md-display ul,
.md-display ol {
  margin: 0.45rem 0;
  padding-left: 1.35rem;
}
.md-display ul { list-style: disc; }
.md-display ol { list-style: decimal; }
.md-display ul ul { list-style: circle; }
.md-display li {
  margin: 0.18rem 0;
  overflow-wrap: break-word;
  word-break: break-word;
}
.md-display li::marker {
  color: color-mix(in srgb, var(--md-accent, currentColor) 70%, transparent);
}
/* Nested lists must not inherit the outer list's top/bottom margin, or three
   levels of nesting drift apart into separate-looking blocks. */
.md-display li > ul,
.md-display li > ol { margin: 0.15rem 0; }

.md-display hr {
  margin: 0.9rem 0;
  border: 0;
  border-top: 1px solid var(--md-border, currentColor);
}

.md-display blockquote {
  margin: 0.6rem 0;
  padding: 0.1rem 0 0.1rem 0.75rem;
  border-left: 2px solid color-mix(in srgb, var(--md-accent, currentColor) 50%, transparent);
  color: var(--md-muted, currentColor);
  font-style: italic;
}

.md-link {
  color: var(--md-accent, currentColor);
  text-decoration: underline;
  text-underline-offset: 0.16em;
  overflow-wrap: break-word;
  word-break: break-word;
}
/* A keyboard user must be able to see which citation they are on. The ring is
   painted HERE rather than as a utility class on the anchor, because a Tailwind
   `focus-visible:` class would style the link in ExactOps and do nothing at all
   on a published WBP site, which loads no Tailwind. `--shadow-focus` is
   ExactOps' token; the fallback is a plain two-tone outline so the ring still
   appears on a surface that never defined it. */
.md-link:focus-visible {
  outline: 2px solid var(--md-accent, currentColor);
  outline-offset: 2px;
  border-radius: var(--md-radius-mark, 0.25rem);
  box-shadow: var(--shadow-focus, none);
}

/* ---------------------------------------------------------------------
   INLINE CODE. The fill is a DERIVED code surface, never the panel's own
   background — when those two are the same token, inline code inside an
   assistant panel is invisible-on-invisible and the "code" reading is lost.
   --------------------------------------------------------------------- */
.md-display :not(pre) > code {
  padding: 0.1em 0.34em;
  border: 1px solid var(--md-code-border, currentColor);
  border-radius: var(--md-radius-mark, 0.25rem);
  background: var(--md-code-bg, transparent);
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: 0.85em;
  overflow-wrap: break-word;
  word-break: break-all;
}

/* ---------------------------------------------------------------------
   FENCED CODE. A titled block, not a bare <pre>: the language band tells the
   reader what they are looking at and gives the copy control a home.
   --------------------------------------------------------------------- */
.md-code {
  margin: 0.55rem 0;
  border: 1px solid var(--md-code-border, currentColor);
  border-radius: var(--md-radius, 0.5rem);
  overflow: hidden;
}
.md-code__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.2rem 0.55rem;
  border-bottom: 1px solid var(--md-code-border, currentColor);
  background: var(--md-code-bg, transparent);
}
.md-code__lang {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: 0.7em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--md-muted, currentColor);
}
/* The body scrolls INSIDE ITS OWN box. This is the layout-invariant rule: a
   140-column line must never widen the page, only this element. */
.md-code__body {
  margin: 0;
  padding: 0.6rem;
  overflow-x: auto;
  background: var(--md-code-bg, transparent);
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: 0.85em;
  line-height: 1.55;
}
.md-code__body code {
  font-family: inherit;
  font-size: inherit;
  white-space: pre;
}

/* ---------------------------------------------------------------------
   TABLES. Same rule as code: the wide thing scrolls in its own container.
   --------------------------------------------------------------------- */
.md-table {
  margin: 0.55rem 0;
  border: 1px solid var(--md-border, currentColor);
  border-radius: var(--md-radius, 0.5rem);
  overflow-x: auto;
}
.md-table table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
  font-size: 0.92em;
}
.md-table thead { background: var(--md-head-bg, transparent); }
.md-table th {
  padding: 0.4rem 0.6rem;
  font-weight: 650;
  color: var(--md-fg, currentColor);
  /* nowrap on a HEADER is safe and keeps the label readable; nowrap on a body
     cell would widen an auto-layout table to its longest string instead of
     constraining anything. */
  white-space: nowrap;
}
.md-table td {
  padding: 0.4rem 0.6rem;
  vertical-align: top;
  color: var(--md-fg, currentColor);
}
.md-table tr { border-top: 1px solid var(--md-border, currentColor); }
.md-table thead tr { border-top: 0; }

/* =====================================================================
   THE BREATHING GLOW — "items being added".
   =====================================================================
   Applied by `MarkdownDisplay` while `streaming` is true, and mirrored onto
   `data-streaming="true"` so a browser test can assert the state without
   reaching into React.

   TWO animations, one idea. The block breathes a soft halo so the whole answer
   reads as LIVE, and a caret breathes at the insertion point so the eye knows
   WHERE the next word lands. The halo is box-shadow only — it paints outside
   the border box and so cannot reflow a single character of text while tokens
   stream in, which a width/padding animation would.
   ===================================================================== */
@keyframes md-display-breathe {
  0%,
  100% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--md-accent, currentColor) 0%, transparent);
  }
  50% {
    box-shadow: 0 0 0.9rem 0.1rem color-mix(in srgb, var(--md-accent, currentColor) 26%, transparent);
  }
}

@keyframes md-display-caret {
  0%,
  100% {
    opacity: 0.28;
    transform: scaleY(0.78);
  }
  50% {
    opacity: 1;
    transform: scaleY(1);
  }
}

.md-display--streaming {
  border-radius: var(--md-radius, 0.5rem);
  animation: md-display-breathe 2.4s var(--md-ease, ease-out) infinite;
}

/* The caret rides the LAST block so it sits exactly where text is arriving. */
.md-display--streaming > :last-child::after {
  content: "";
  display: inline-block;
  width: 0.42em;
  height: 1em;
  margin-left: 0.14em;
  vertical-align: -0.14em;
  border-radius: var(--md-radius-mark, 0.25rem);
  background: var(--md-accent, currentColor);
  animation: md-display-caret 1.3s var(--md-ease, ease-out) infinite;
}

/* REDUCED MOTION. Not "less" motion — none. The caret stays VISIBLE but
   static, so a reader who asked for no animation still sees where the answer
   is being written; removing the affordance entirely would cost them
   information rather than movement. */
@media (prefers-reduced-motion: reduce) {
  .md-display--streaming {
    animation: none;
  }
  .md-display--streaming > :last-child::after {
    animation: none;
    opacity: 0.75;
  }
}

/* =====================================================================
   THE GROUNDING RECEIPT — "What I checked (6)".
   =====================================================================
   A collapsed <details> listing the sources an answer was grounded in.

   WHAT WAS WRONG: it was a native disclosure triangle in front of a native
   `list-style: disc` list, at 0.72 opacity — i.e. the browser's defaults with
   the colour turned down. Against a designed answer bubble that reads as
   unfinished, and the repeated source titles stacked into an undifferentiated
   grey wall.

   WHAT IT IS NOW: the summary is a real CHIP — bordered, tappable, with a
   chevron that rotates on open — and the list is a card of rows, each led by a
   small accent rule rather than a bullet. The 44px min-height is the tap
   target, kept without a media query so it is correct on a phone and harmless
   on a desktop.
   ===================================================================== */
.md-sources {
  margin-top: 0.5rem;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--md-fg, currentColor);
}

.md-sources__summary {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 44px;
  padding: 0.25rem 0.6rem 0.25rem 0.5rem;
  border: 1px solid var(--md-border, currentColor);
  border-radius: var(--md-radius, 0.5rem);
  background: var(--md-code-bg, transparent);
  color: var(--md-muted, currentColor);
  font-weight: 600;
  cursor: pointer;
  /* Kills BOTH native markers — the WebKit pseudo-element and the modern
     list-item display — so the chevron below is the only glyph. */
  list-style: none;
  transition: color var(--motion-fast, 140ms) var(--md-ease, ease-out),
    border-color var(--motion-fast, 140ms) var(--md-ease, ease-out);
}
.md-sources__summary::-webkit-details-marker { display: none; }
.md-sources__summary::marker { content: ""; }

/* The chevron is drawn, not typed: a bare "▸" character renders at whatever
   size and baseline the host font decides, which is why the old one looked
   like a stray glyph rather than a control. */
.md-sources__summary::before {
  content: "";
  flex: none;
  width: 0.4rem;
  height: 0.4rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--motion-fast, 140ms) var(--md-ease, ease-out);
}
.md-sources[open] > .md-sources__summary::before {
  transform: rotate(45deg);
}
.md-sources__summary:hover,
.md-sources__summary:focus-visible {
  color: var(--md-fg, currentColor);
  border-color: color-mix(in srgb, var(--md-accent, currentColor) 55%, transparent);
}

.md-sources__list {
  margin: 0.4rem 0 0;
  padding: 0.35rem 0.1rem;
  border-radius: var(--md-radius, 0.5rem);
  /* Rows, not bullets. A disc in front of a source title reads as prose; a
     ruled row reads as a record, which is what a grounding receipt is. */
  list-style: none;
  display: grid;
  gap: 0.1rem;
}
.md-sources__item {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.3rem 0.15rem;
  color: var(--md-muted, currentColor);
  overflow-wrap: break-word;
  word-break: break-word;
}
.md-sources__item::before {
  content: "";
  flex: none;
  width: 2px;
  height: 0.95em;
  margin-top: 0.12em;
  border-radius: var(--md-radius-mark, 0.25rem);
  background: color-mix(in srgb, var(--md-accent, currentColor) 55%, transparent);
}
/* A count badge for a source cited more than once. The duplicates in the live
   screenshot are an UPSTREAM data question, not a styling one — this only
   stops the same title stacking into a wall when the server does send repeats. */
.md-sources__count {
  flex: none;
  margin-left: auto;
  padding: 0 0.3rem;
  border-radius: var(--md-radius-mark, 0.25rem);
  background: color-mix(in srgb, var(--md-accent, currentColor) 14%, transparent);
  font-size: 0.9em;
  font-variant-numeric: tabular-nums;
}
