/* Glass Box demo UI, analysis.html / query.html.
   Built on tokens.css's Midnight Ledger system, second register: deep
   navy header (dark register, unchanged), light powder blue body
   (body register, see theme_token_spec.md), one typeface (Futura) for
   every role, one accent blue for every interactive/live signal, the
   signal amber demoted to its one honest job, idle/warning states
   only. This page keeps its own calm, forensic register: no glass
   texture on the working cards, hairline rules instead of boxed
   shadows, restraint stays the point even on the new light ground.
   The evidence rail deliberately stays dark register (tokens.css
   applies dark values to the .rail selector directly): a deep panel
   sliding over the light page is the stronger premium gesture, and
   the rail is a genuinely floating surface, not page content.

   Semantic diagram tokens (--good/--bad/--alert from tokens.css) mean
   the same thing across every traversal diagram on this page: good =
   the accepted/good outcome, bad = rejected/infeasible/remaining slip,
   alert/accent = a solved/computed outcome. */

:root {
  /* Local aliases kept so the rest of this file, and any inline SVG
     class built by app.js/shared.js, can keep referencing --sans/
     --mono/--fs-* exactly as before; values now resolve through
     tokens.css rather than a second, competing set. */
  --sans: var(--font-body);
  --mono: var(--font-mono);
}

* { box-sizing: border-box; margin: 0; }

/* The hidden attribute must always win over any component's own
   display property. Without this, a rule like .loading { display:
   flex } or .exec-loading { display: flex } silently defeats
   `el.hidden = true` in JS, since the browser's built-in [hidden] {
   display: none } rule loses to a more specific class selector. This
   is what caused both loading indicators to stay visibly stuck after
   their answers had already finished rendering. tokens.css already
   declares this rule; repeated here only as a safety net in case
   style.css is ever loaded without it. */
[hidden] { display: none !important; }

body {
  font-family: var(--font-body); font-weight: 300;
  min-height: 100vh;
}

header {
  position: relative;
  padding: 32px 40px 24px;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  text-align: center;
  background:
    radial-gradient(ellipse 700px 240px at 15% -60%, rgba(127, 160, 242, 0.22), transparent 65%),
    linear-gradient(135deg, var(--bg-0) 0%, var(--bg-1) 55%, var(--bg-2) 100%);
  /* The seam, decision 2 of the light-body overhaul (theme_token_spec.md):
     a crisp accent-tinted hairline plus a soft shadow cast onto the light
     body below. Defined once here, never per page. The hairline uses the
     dark-register accent because it belongs to the header, a dark object,
     not to the body. */
  border-bottom: 1px solid rgba(127, 160, 242, 0.6);
  box-shadow: 0 1px 0 rgba(127, 160, 242, 0.3),
              0 14px 30px -12px rgba(8, 13, 34, 0.45);
  z-index: 1;
}
.mark {
  font-family: var(--font-body); color: var(--ink-0);
  font-weight: 600; text-transform: uppercase; font-size: var(--fs-statement); letter-spacing: 0.3em;
  display: flex; align-items: center; justify-content: center; gap: 10px;
}
.mark-logo {
  height: 1.15em; width: 1.15em; object-fit: contain;
}
.thesis {
  font-family: var(--font-body); font-weight: 400; font-size: 11.5px;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--accent-bright);
}
.header-link {
  position: absolute; top: 28px;
  font-family: var(--font-body); font-weight: 500; font-size: var(--fs-small);
  background: transparent; border: 1px solid var(--hairline-strong); color: var(--ink-1);
  padding: 6px 15px; border-radius: var(--radius-pill); cursor: pointer;
  letter-spacing: 0.04em; text-decoration: none; display: inline-block;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.header-link:hover, .header-link:focus-visible {
  background: var(--accent-dim); border-color: var(--accent); color: #fff;
  outline: none;
}
.header-link--left { left: 40px; }
.header-link--right { right: 40px; }

/* ---- Page title: the project this page is about, made prominent --------
   A gradient text fill on the heading itself, the one deliberately
   bold flourish on this otherwise restrained page: it is the second
   thing anyone reads after the wordmark, since everything below it
   (the ask box, the diagrams) is scoped to this one project. */
.page-title-wrap {
  text-align: center; padding: 40px 24px 8px;
}
.page-title {
  font-family: var(--font-body); font-size: var(--fs-display); font-weight: 700;
  letter-spacing: -0.01em;
  background: linear-gradient(120deg, var(--ink-0) 0%, var(--accent-bright) 55%, var(--accent) 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: var(--ink-0);
}
.page-subtitle {
  font-size: var(--fs-body); color: var(--ink-2); margin-top: 6px; font-weight: 300;
}

main { max-width: none; margin: 0 auto; padding: 40px 5% 80px; }
.nomatch { max-width: 900px; margin-left: auto; margin-right: auto; }

/* ---- Section divider: separates the ask controls from the answer below --
   Without this, .ask and #split-layout share the same ground color and
   read as one continuous block once an answer renders. A plain hairline,
   consistent with the hairline-over-boxes restraint used everywhere else
   on this page, is enough to mark the break. */
.section-divider {
  border: none; border-top: 1px solid var(--hairline);
  margin: 40px 0 0;
}

/* ---- Split layout: Black Box (left, 40%) vs Glass Box (right, 60%) -------
   Always on: the Black Box runs by default now, no opt-in checkbox, so
   both columns are visible from first paint. The 40:60 ratio is fixed
   via grid-template-columns, not flex-grow, so it holds steady
   regardless of which panel's content is taller. Both column heads
   (.split-col-head) sit at the same vertical position so the two
   answers stay aligned from the top down, even before either has
   answered anything yet. */
.split-layout {
  display: grid;
  grid-template-columns: 40fr 60fr;
  gap: 32px;
  align-items: start;
  margin-top: 40px;
}
.split-left { min-width: 0; }
.split-right { min-width: 0; }
.answer, #chain, .traversal-wrap {
  max-width: none; margin-left: 0; margin-right: 0;
}
.compare-wrap { max-width: none; margin: 0; }

.split-col-head {
  display: flex; align-items: center; justify-content: center; gap: 13px;
  margin-bottom: 16px;
}
.split-col-icon {
  width: 36px; height: 36px; object-fit: contain; flex: 0 0 auto;
  border-radius: 6px;
}
.split-col-title {
  font-family: var(--font-body); font-weight: 700; font-size: 34px;
  color: var(--ink-0); letter-spacing: -0.01em;
}

/* ---- Headline row: the one row that IS a table, deliberately ------------
   Both sides answer the same question ("what did each side claim"), so
   this is the one place a shared row boundary makes sense: everything
   after it (Black Box's grounding scan vs. Glass Box's traversal diagram
   and factor cards) is a structurally different kind of evidence and
   stays freeform. syncSplitHeadlineRow() in app.js measures both
   .split-row-headline elements after each answer renders and sets the
   shorter one's min-height to match the taller, so the hairline below
   both lands at the same y position regardless of which side's prose ran
   longer, the "borderless table" feeling without an actual <table>.
   box-sizing: border-box so the JS-set min-height already accounts for
   this element's own padding, matching offsetHeight's own box model. */
.split-row-headline {
  padding-bottom: 20px; margin-bottom: 20px;
  border-bottom: 1px solid var(--hairline);
  box-sizing: border-box;
}

/* ---- Pre-generated content, set apart from the live answer above it -----
   The Black Box column leads with the real-time answer to the question
   just asked, exactly like the Glass Box column does; the pre-logged
   benchmark (many runs, computed ahead of time, not this one) is a
   different kind of evidence and sits below its own divider so it is
   never mistaken for part of the live exchange. */
.split-pregenerated {
  margin-top: 28px; padding-top: 24px;
  border-top: 1px solid var(--hairline);
}

@media (max-width: 980px) {
  /* Side by side stops making sense once each column would be too
     narrow to read comfortably: stack instead, Black Box above Glass
     Box, same order the columns already read in. Row height syncing
     stops applying too (synced min-height would just leave a large
     dead gap in the shorter, usually Black Box, column); JS handles
     the normal case, this is the defensive CSS fallback for before
     JS has run or if ResizeObserver is unsupported. */
  .split-layout {
    grid-template-columns: 1fr;
  }
  .split-row-headline { min-height: 0 !important; }
}

.eyebrow {
  font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--accent);
}

.ask { text-align: center; }
.askrow {
  display: flex; gap: 10px; max-width: 700px; margin: 32px auto 0;
}
#q {
  flex: 1; font-family: var(--font-mono); font-size: var(--fs-body-lg);
  padding: 13px 16px; border: 1.5px solid var(--hairline-strong);
  background: var(--surface); color: var(--ink-0); border-radius: 6px;
  text-align: center;
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
#q:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
#go {
  font-family: var(--font-body); font-size: var(--fs-body-lg); font-weight: 600;
  padding: 13px 28px; border: none;
  background: linear-gradient(135deg, var(--accent), var(--accent-grad-end)); color: var(--on-accent-ink);
  border-radius: 6px; cursor: pointer;
  box-shadow: 0 6px 20px -8px rgba(47, 85, 184, 0.4);
  transition: background 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease;
}
#go:hover { transform: translateY(-1px); box-shadow: 0 10px 26px -8px rgba(47, 85, 184, 0.5); }
#go:active { transform: translateY(0); }
#go:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

.chip-hint {
  display: inline-block; margin-top: 8px;
}

/* Five theme cards, one per story, each its own numbered header and a
   ten question dropdown. This is the comprehensive guided tour: wider
   than a row of chips, but still one closed control per theme so the
   page reads as five clear sections, not fifty visible questions. */
.themegrid {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px;
  margin-top: 28px;
}
/* Five flat cards, one per story, each carrying its own verb accent so
   the guided tour doubles as a legend for the color system used once a
   question is asked. Flat solid top rule instead of a glass hover lift,
   consistent with the rest of the premium-flat treatment: depth comes
   from a crisper border on hover, not a shadow bloom. */
.themecard {
  --tc-accent: var(--accent);
  border: 1px solid var(--hairline); border-top: 3px solid var(--tc-accent);
  background: var(--surface-hi); border-radius: var(--radius);
  padding: 18px 16px; display: flex; flex-direction: column;
  align-items: center; text-align: center; gap: 6px;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.themecard:nth-child(1) { --tc-accent: var(--verb-forensics); }
.themecard:nth-child(2) { --tc-accent: var(--verb-capacity); }
.themecard:nth-child(3) { --tc-accent: var(--verb-timetravel); }
.themecard:nth-child(4) { --tc-accent: var(--verb-ripple); }
.themecard:nth-child(5) { --tc-accent: var(--verb-honesty); }
.themecard:hover, .themecard:focus-within {
  border-color: var(--tc-accent);
  transform: translateY(-2px);
}
.tc-num {
  font-family: var(--font-mono); font-size: var(--fs-label); font-weight: 700;
  color: var(--on-accent-ink); background: var(--tc-accent);
  width: 20px; height: 20px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
}
.tc-title {
  font-family: var(--font-body); font-size: var(--fs-body-lg); font-weight: 700;
  color: var(--ink-0); margin-top: 2px;
}
.tc-desc { font-size: var(--fs-small); color: var(--ink-2); margin-bottom: 4px; font-weight: 300; }
.theme-q {
  font-family: var(--font-body); font-size: var(--fs-small); color: var(--ink-1);
  background: var(--surface); border: 1.5px solid var(--hairline); border-radius: 4px;
  padding: 8px 10px; width: 100%; cursor: pointer; margin-top: auto;
  text-align: center; text-align-last: center;
  transition: border-color 0.15s ease;
}
.theme-q:hover { border-color: var(--tc-accent); }
.theme-q:focus-visible { outline: 3px solid var(--tc-accent); outline-offset: 1px; }

@media (max-width: 1100px) {
  .themegrid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 720px) {
  .themegrid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
  .themegrid { grid-template-columns: 1fr; }
}

/* No top margin: .answer and .nomatch are the first element directly
   under .split-col-head in the right column now (same position
   #compare-wrap/.contrast-panel occupies in the left column), so both
   columns' panels start at the exact same y position. */
.answer, .nomatch { margin-top: 0; }

/* ---- Verb identity: one flat, high-chroma accent per story ---------------
   Set once via a single class on #answer (app.js's applyVerbTheme), then
   consumed everywhere on this page through --verb-accent/--verb-accent-dim
   rather than five components each hardcoding a color. A story with no
   theme applied (the refusal path, #nomatch) simply falls back to the
   page's one neutral accent, unchanged from before this system existed. */
#answer { --verb-accent: var(--accent); --verb-accent-dim: var(--accent-dim); }
#answer.verbtheme--forensics  { --verb-accent: var(--verb-forensics);  --verb-accent-dim: var(--verb-forensics-dim); }
#answer.verbtheme--capacity   { --verb-accent: var(--verb-capacity);   --verb-accent-dim: var(--verb-capacity-dim); }
#answer.verbtheme--timetravel { --verb-accent: var(--verb-timetravel); --verb-accent-dim: var(--verb-timetravel-dim); }
#answer.verbtheme--ripple     { --verb-accent: var(--verb-ripple);     --verb-accent-dim: var(--verb-ripple-dim); }
#answer.verbtheme--honesty    { --verb-accent: var(--verb-honesty);    --verb-accent-dim: var(--verb-honesty-dim); }

.narration-head {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  text-align: center;
  border-bottom: 1px solid var(--hairline); padding-bottom: 10px;
}
.source { font-family: var(--font-mono); font-size: var(--fs-label); color: var(--ink-2); }
.engine-tag {
  display: block; width: fit-content; margin: 12px auto 0;
  font-family: var(--font-mono); font-weight: 700;
  font-size: var(--fs-label); letter-spacing: 0.08em; color: var(--on-accent-ink);
  background: var(--verb-accent); border: none; border-radius: var(--radius-pill);
  padding: 5px 16px; text-align: center; text-transform: uppercase;
}
#narration { margin-top: 14px; font-size: var(--fs-answer); line-height: 1.65; text-align: center; color: var(--ink-0); font-weight: 300; }

/* ---- Headline card: the answer's own flat, bordered surface --------------
   Premium-flat treatment: a solid card with a colored top rule in the
   story's own accent (not a gradient, not a glass blur), so the answer
   reads as one confident statement instead of bare text floating in the
   page. The border-top width carries the "this is the headline" weight;
   everything below it (factor cards, diagrams) stays the quieter hairline
   register so the hierarchy is unambiguous at a glance. */
.answer {
  background: var(--surface-hi);
  border: 1px solid var(--hairline);
  border-top: 4px solid var(--verb-accent);
  border-radius: var(--radius-lg);
  padding: 28px clamp(18px, 4vw, 40px) 32px;
}

.loading {
  margin-top: 36px; font-size: var(--fs-body); color: var(--ink-2);
  display: flex; align-items: center; justify-content: center; gap: 10px;
}
#loading-copy, .exec-loading {
  display: inline-block;
}
@media (prefers-reduced-motion: no-preference) {
  #loading-copy { animation: copyfade 1.4s ease-in-out infinite; }
  @keyframes copyfade {
    0% { opacity: 0.4; }
    15% { opacity: 1; }
    85% { opacity: 1; }
    100% { opacity: 0.4; }
  }
}
.loading-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--accent);
  display: inline-block;
}
@media (prefers-reduced-motion: no-preference) {
  .loading-dot { animation: pulse 1s ease-in-out infinite; }
  @keyframes pulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
}

/* ---- Executive summary: a second, separate register --------------------
   Deliberately looks different from the technical .factor cards above
   it: a tinted glass panel instead of a flat card, no chips, no
   clickable evidence, since business language doesn't carry the same
   three-click provenance the technical answer already does. This is a
   second READING of the same answer, not a second proof of it. */
.exec-wrap {
  margin: 28px auto 0; max-width: 780px;
  background: linear-gradient(135deg, var(--accent-dim), var(--signal-dim));
  border: 1px solid var(--accent-dim);
  border-radius: var(--radius);
  padding: 18px 22px;
}
.exec-head {
  display: flex; flex-direction: column; align-items: center;
  gap: 4px; margin-bottom: 8px; text-align: center;
}
.exec-eyebrow {
  font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--accent); font-weight: 700;
}
.exec-source { font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--ink-3); }
.exec-loading {
  font-size: var(--fs-body); color: var(--ink-2); font-style: italic;
  display: flex; align-items: center; gap: 8px;
}
.exec-loading::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent); display: inline-block; flex: 0 0 auto;
}
@media (prefers-reduced-motion: no-preference) {
  .exec-loading::before { animation: pulse 1s ease-in-out infinite; }
}
.exec-text {
  font-size: var(--fs-body-lg); line-height: 1.6; color: var(--ink-0); font-weight: 300;
}

.trustfoot {
  max-width: 860px; margin: 20px auto 0; padding: 20px 24px 60px;
  font-size: var(--fs-small); color: var(--ink-3); line-height: 1.6; text-align: center;
  border-top: 1px solid var(--hairline); font-weight: 300;
}
/* The two proof buttons live in the Glass Box column itself (they prove
   claims about Glass Box specifically: read-only, SHACL), while the one
   sentence above them stays as the page's own full-width footer. */
.trustfoot--split {
  max-width: none; margin: 28px 0 0; padding: 20px 0 0;
  border-top: 1px solid var(--hairline);
}

/* The read-only proof button: runs a real simulate_scenario call and
   shows the events table hash before and after in the evidence rail.
   Chip idiom on purpose, it resolves a claim to evidence, exactly
   like every chip on the page. */
.verify-btn {
  display: inline-block; margin-top: 14px;
  font-family: var(--font-mono); font-size: var(--fs-small);
  border: 1.5px solid var(--accent); color: var(--accent);
  background: transparent; border-radius: 6px;
  padding: 8px 16px; cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.verify-btn:hover, .verify-btn:focus-visible {
  background: var(--accent-dim); color: var(--chip-hover-ink); outline: none;
}

/* Phase 5: the LLM-suggested rephrasing, shown only after a genuine
   no-match refusal. Signal amber, not accent, deliberately: accent/
   chip styling is reserved for things that resolve to verified
   evidence, this is explicitly the opposite, an unverified model
   guess the user must choose to try. The label text on the button
   itself already says so; this border color is the second, ambient
   signal. */
.suggest-result {
  margin-top: 12px; padding: 10px 14px;
  border: 1.5px dashed var(--signal); border-radius: 6px;
  font-size: var(--fs-small); color: var(--ink-1);
  background: var(--signal-dim);
}
.suggest-result .suggest-label {
  display: block; font-family: var(--font-mono); font-size: var(--fs-label);
  color: var(--signal); margin-bottom: 4px; text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* The signature element: every computed value is an evidence chip.
   Clicking one opens the evidence rail with the raw rows behind it. */
.chip {
  font-family: var(--font-mono); font-size: 0.92em; font-weight: 600;
  border: none; color: var(--on-accent-ink);
  background: var(--verb-accent); border-radius: 5px;
  padding: 2px 9px; cursor: pointer; white-space: nowrap;
  transition: filter 0.15s ease, transform 0.1s ease;
  /* Visual padding stays tight (chips sit inline in prose, dozens per
     answer), but the tap target itself is widened to a touch-friendly
     minimum via a transparent border acting as extra hit area, so a
     phone tap doesn't need to land on the thin visible chip itself. */
  margin: 6px 1px;
  box-shadow: 0 0 0 6px transparent;
}
@media (hover: none) and (pointer: coarse) {
  .chip {
    display: inline-block;
    min-height: 32px; line-height: 28px;
    padding: 2px 12px;
  }
}
.chip:hover, .chip:focus-visible {
  filter: brightness(1.1); outline: none; transform: translateY(-1px);
}
.chip.amber { background: var(--signal); color: var(--on-accent-ink); }
.chip.amber:hover, .chip.amber:focus-visible { filter: brightness(1.1); }

#chain { margin-top: 28px; display: grid; gap: 14px; }
.factor {
  border: 1px solid var(--hairline); border-left: 3px solid var(--verb-accent);
  padding: 14px 18px; background: var(--surface-hi); text-align: center; border-radius: 0 var(--radius) var(--radius) 0;
}
.factor h3 {
  font-family: var(--font-mono); font-size: var(--fs-small); letter-spacing: 0.12em;
  text-transform: uppercase; margin-bottom: 8px; color: var(--verb-accent);
}
.factor p { font-size: var(--fs-body); line-height: 1.6; text-align: left; color: var(--ink-1); font-weight: 300; }
.factor.counterfactual { border-left-color: var(--signal); }

.nomatch { border: 1px dashed var(--hairline-strong); padding: 18px 20px; text-align: center; border-radius: var(--radius); }
.nomatch p { margin-top: 8px; font-size: var(--fs-body-lg); line-height: 1.6; text-align: left; color: var(--ink-1); font-weight: 300; }

/* ---- Traversal diagram --------------------------------------------------
   The same JSON already narrated above, drawn a second way. Node
   meanings, four fixed treatments: trigger (outline, ink), derived
   (signal amber, matching the idle period convention used everywhere
   else on this page), computed (accent blue, matching the chip
   color), outcome (solid accent-on-dark). Counterfactual gets its own
   dashed amber treatment since it's the one node that isn't a fact,
   it's a "what if". */
.traversal-wrap {
  margin: 28px auto 0; max-width: 900px;
  border: 1px solid var(--hairline); border-radius: var(--radius-lg);
  padding: 20px 22px 16px; background: var(--surface-hi);
  box-shadow: var(--shadow);
}
.traversal-head {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  text-align: center; margin-bottom: 10px;
}
.traversal-head .eyebrow { color: var(--verb-accent); }
.traversal-hint { font-size: var(--fs-label); color: var(--ink-3); }
#traversal-svg-holder,
#panel-secondary-svg-holder,
#panel-traversal-svg-holder { display: flex; justify-content: center; }
.tv-svg { width: 100%; max-width: 820px; height: auto; }

/* On a narrow phone, scaling these SVGs to fit the viewport width
   shrinks their fixed-px internal text (13px labels, 9.5-11.5px
   sublabels) well past a legible size. Below 560px, hold the SVG at a
   fixed legible pixel width instead and let its holder scroll
   horizontally, the same tradeoff already used for the dashboard's
   Gantt chart on mobile: less convenient than a full-width fit, but
   the text stays readable rather than shrinking to illegibility. */
@media (max-width: 560px) {
  #traversal-svg-holder,
  #panel-secondary-svg-holder,
  #panel-traversal-svg-holder {
    justify-content: flex-start; overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    scrollbar-color: var(--hairline-strong) transparent;
  }
  .tv-svg { width: auto; min-width: 560px; max-width: none; flex: 0 0 auto; }
}

.tv-node rect {
  fill: var(--surface-hi); stroke-width: 1.6;
  transition: transform 0.15s ease, filter 0.15s ease;
}
.tv-node--trigger rect   { stroke: var(--ink-2); }
.tv-node--derived rect   { stroke: var(--signal); stroke-width: 2; fill: var(--signal-dim); }
.tv-node--computed rect  { stroke: var(--verb-accent); stroke-width: 2; fill: var(--verb-accent-dim); }
.tv-node--accepted rect  { stroke: var(--good); stroke-width: 2; fill: rgba(46, 138, 97, 0.13); }
.tv-node--rejected rect  { stroke: var(--bad); stroke-width: 2; fill: rgba(192, 81, 56, 0.13); }
.tv-node--outcome rect   { stroke: var(--verb-accent); fill: var(--verb-accent); }
.tv-node--counterfactual rect {
  stroke: var(--signal); stroke-dasharray: 4 3; fill: var(--surface-hi);
}
.tv-node--outcome .tv-label,
.tv-node--outcome .tv-sublabel { fill: var(--on-accent-ink); }
.tv-node--accepted .tv-sublabel { fill: var(--good); font-weight: 600; }
.tv-node--rejected .tv-sublabel { fill: var(--bad); font-weight: 600; }

.tv-label {
  font-family: var(--font-body); font-size: 13px; font-weight: 700;
  fill: var(--ink-0); text-anchor: middle; dominant-baseline: middle;
}
.tv-sublabel {
  font-family: var(--font-mono); font-size: 9.5px; fill: var(--ink-2);
  text-anchor: middle; dominant-baseline: middle;
}
.tv-node--outcome .tv-sublabel { fill: var(--on-accent-ink); opacity: 0.8; }

.tv-node:hover rect, .tv-node:focus-visible rect {
  filter: brightness(1.15);
}
.tv-node.tv-active rect {
  stroke-width: 3;
  filter: drop-shadow(0 0 6px var(--verb-accent-dim));
}

.tv-edge { stroke: var(--hairline-strong); stroke-width: 1.5; fill: none; }
.tv-edge--dashed { stroke-dasharray: 4 3; stroke: var(--signal); opacity: 0.75; }
.tv-arrowhead { fill: var(--hairline-strong); }

/* ---- reconstruct_asof: split-timeline shape -----------------------------
   Two horizontal tracks, one vertical query-date line. A fact's two
   clock positions connect with a diagonal "gap line" whenever they
   differ, which is the bi-temporal claim made visible: the same fact,
   two dates, drawn as literal distance between two points instead of
   two numbers in a sentence. The tx_time track additionally carries a
   shaded known/not-yet-known band either side of the query line, since
   that's the single claim this whole verb rests on: everything left of
   the line is knowledge, everything right of it doesn't exist to the
   twin yet, regardless of what's already true in the world. */
.tv-track { stroke: var(--hairline-strong); stroke-width: 2; }
.tv-tracklabel {
  font-family: var(--font-mono); font-size: 10px; fill: var(--ink-2);
  dominant-baseline: middle;
}
.tv-known-band { fill: var(--good); opacity: 0.16; }
.tv-unknown-band { fill: var(--bad); opacity: 0.18; }
.tv-bandlabel {
  font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.03em;
  text-transform: uppercase;
}
.tv-bandlabel--known { fill: var(--good); }
.tv-bandlabel--unknown { fill: var(--bad); }
.tv-queryline { stroke: var(--verb-accent); stroke-width: 1.5; stroke-dasharray: 3 2; }
.tv-querylabel {
  font-family: var(--font-mono); font-size: 10px; fill: var(--verb-accent);
  text-anchor: middle; font-weight: 700;
}
.tv-gapline { stroke: var(--signal); stroke-width: 1.3; stroke-dasharray: 2 3; opacity: 0.8; }
.tv-point { fill: var(--surface-hi); stroke: var(--verb-accent); stroke-width: 2; cursor: pointer; }
.tv-point--known { fill: rgba(46, 138, 97, 0.2); stroke: var(--good); stroke-width: 2.5; }
.tv-point--unknown { fill: var(--surface-hi); stroke: var(--bad); stroke-width: 2.5; stroke-dasharray: 2 2; opacity: 0.85; }
.tv-point.tv-active { stroke-width: 3.5; filter: drop-shadow(0 0 6px var(--verb-accent-dim)); }
.tv-pointlabel {
  font-family: var(--font-body); font-size: 10.5px; font-weight: 600; fill: var(--ink-0);
  text-anchor: middle;
}

/* ---- simulate_scenario: before/after bar --------------------------------
   The magnitude of the cascade made visible: two short horizontal
   bars, original slip and total slip after the scenario, both read
   directly from the same numbers already in the prose above, never
   recomputed here, so the bar can't silently drift from the narration
   it sits beside. */
.tv-barlabel {
  font-family: var(--font-mono); font-size: 9.5px; fill: var(--ink-2);
}
.tv-bar--before { fill: var(--signal); }
.tv-bar--after { fill: var(--bad); }
.tv-barvalue {
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 700; fill: var(--ink-0);
}
.tv-barvalue--after { fill: var(--bad); }

/* ---- run_scheduler: constraint-axis dots --------------------------------
   Three small dots per row, timeline / cost / headcount, one lit per
   path to show which single constraint that path relaxes. This is the
   real structural claim from Story B made visible: each path holds two
   constraints fixed and relaxes exactly one, not three arbitrary
   variations dressed up as distinct solutions. */
.tv-axisdot { fill: var(--hairline-strong); }
.tv-axisdot--lit { fill: var(--verb-accent); }

/* ---- team_allocation: per-person allocation bars ------------------------
   Not a timeline, a straightforward percentage comparison: one bar per
   person, length = their real allocation_pct. Zero-allocation people
   still get a small visible marker rather than an invisible zero-width
   bar, so "genuinely unallocated" reads as a checkable fact, not a
   missing row. */
.tv-track-bg { fill: var(--hairline-strong); opacity: 0.5; }
.tv-allocbar { fill: var(--good); cursor: pointer; }
.tv-allocbar--zero { fill: var(--ink-3); }
.tv-allocbar.tv-active { filter: drop-shadow(0 0 6px rgba(46, 138, 97, 0.5)); }

/* ---- Story E refusal: the no-match shape --------------------------------
   The one diagram with a deliberately broken edge: a dashed stub ending
   in an "x" rather than an arrowhead, terminating in nothing, next to
   the actual closed verb list it was checked against. Less to draw is
   the correct, honest shape here, not a simplified version of the
   other four. */
.tv-edge--nomatch { stroke: var(--signal); stroke-width: 1.5; stroke-dasharray: 3 3; opacity: 0.7; }
.tv-nomatch-x {
  font-family: var(--font-mono); font-size: 15px; font-weight: 700; fill: var(--signal);
  text-anchor: middle;
}
.tv-verblist {
  font-family: var(--font-mono); font-size: 11.5px; fill: var(--ink-3);
  dominant-baseline: middle;
}
.tv-verblist-label {
  font-family: var(--font-body); font-size: 10px; font-style: italic; fill: var(--ink-3);
}

/* The chip pulse: a brief highlight so clicking a diagram node visibly
   calls attention back to the matching chip in the narration above,
   the same cross-reference in the other direction. */
.chip.chip-active {
  background: var(--accent); color: var(--on-accent-ink);
  transition: background 0.15s ease, color 0.15s ease;
}

@media (max-width: 640px) {
  .traversal-wrap { padding: 12px 10px 8px; }
}

.rail {
  position: fixed; top: 0; right: 0; width: min(460px, 100vw);
  /* height: 100vh reports against the browser's largest possible
     viewport on mobile (address bar hidden), which can push #rail-close
     below the real visible area when the bar is showing. dvh tracks the
     actual visible viewport; the vh line stays as a fallback for
     browsers without dvh support. */
  height: 100vh; height: 100dvh; background: var(--bg-0); color: var(--ink-0);
  padding: 24px; overflow-y: auto; z-index: 10;
  border-left: 1px solid var(--hairline-strong);
  box-shadow: -12px 0 40px rgba(4, 8, 24, 0.5);
}
.rail .eyebrow { color: var(--accent-bright); }
.rail-head { display: flex; justify-content: space-between; align-items: center; }
#rail-close {
  font-family: var(--font-body); font-size: var(--fs-small); padding: 6px 14px;
  background: transparent; color: var(--ink-0);
  border: 1px solid var(--hairline-strong); border-radius: 6px; cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
#rail-close:hover, #rail-close:focus-visible { background: var(--accent-dim); border-color: var(--accent); outline: none; }
#rail-body { margin-top: 18px; display: grid; gap: 12px; }
.event-row {
  font-family: var(--font-mono); font-size: var(--fs-small); line-height: 1.7;
  border: 1px solid var(--hairline); padding: 10px 12px; border-radius: 6px;
  color: var(--ink-1);
}
.event-row .k { color: var(--ink-2); }
.event-row .clock { color: var(--signal); }
.rail-note { margin-top: 20px; font-size: var(--fs-small); color: var(--ink-2); line-height: 1.6; font-weight: 300; }

@media (prefers-reduced-motion: no-preference) {
  .rail { animation: slidein 160ms ease-out; }
  @keyframes slidein { from { transform: translateX(24px); opacity: 0; } }
}

/* ---- Compare mode: the Black Box panel -----------------------------------
   analysis.html/query.html: the Black Box now runs by default in its own
   40% column (see .split-layout above), left of the real Glass Box
   answer, so the contrast is read left to right rather than top to
   bottom. Its own visual register is deliberately plain: no chips, no
   traversal diagram, no proof buttons, so the absence of that machinery
   reads as a real property of this panel, not a missing feature. */

.compare-toggle-row {
  display: flex; flex-direction: column;
  align-items: center; gap: 4px;
}

.contrast-panel {
  border: 1px solid var(--hairline-strong); border-radius: var(--radius);
  padding: 20px 22px; background: var(--surface);
  /* A solid black top rule, the Black Box's own "identity" color (see
     .split-col-icon), mirroring .answer's verb-accent top rule on the
     Glass Box side without borrowing any of its five story colors. */
  border-top: 4px solid #111318;
}
.contrast-head {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  text-align: center; border-bottom: 1px solid var(--hairline); padding-bottom: 12px;
}
.contrast-eyebrow { color: var(--ink-2); }
.contrast-sub { font-size: var(--fs-small); color: var(--ink-3); font-weight: 300; max-width: 620px; }

.contrast-loading {
  margin-top: 18px; display: flex; align-items: center; justify-content: center;
  gap: 10px; font-size: var(--fs-body); color: var(--ink-2);
}

.contrast-answer {
  margin-top: 16px; font-size: var(--fs-body-lg); line-height: 1.65;
  text-align: center; color: var(--ink-0); font-weight: 300;
}
/* Inline claim marks inside the prose itself, not just the separate
   claim list below: the exact phrase the grounding scan matched, so a
   viewer sees WHERE in the sentence a number or name landed, not just
   that it landed somewhere. Deliberately quieter than the .chip
   idiom, thin underline instead of a bordered pill, since this panel
   earns no chip styling. */
.contrast-mark {
  border-bottom: 2px solid var(--good); cursor: default;
}
.contrast-mark--bad {
  border-bottom: 2px solid var(--bad);
  background: rgba(192, 81, 56, 0.08);
}
/* The replay-fallback banner: shown only when a live call failed and
   a previously logged real answer is being shown in its place. Signal
   amber, the same "this is not the pristine live path" register the
   suggested-rephrasing feature already uses, and always names the
   original prov_id so the replay itself resolves to a real logged
   row, never an unlabeled cache. */
.contrast-replay-banner {
  margin-top: 14px; padding: 8px 14px; border-radius: 6px;
  border: 1.5px dashed var(--signal); background: var(--signal-dim);
  font-size: var(--fs-small); color: var(--signal); text-align: center;
  font-weight: 500;
}
/* Unscoreable sentences: prose the grounding scan could not check at
   all (no number, date, or entity name in it), listed honestly rather
   than silently dropped. Neutral ink, not --bad: this is not a flagged
   error, it is a disclosed limit of what the scan verified. */
.contrast-unscoreable {
  margin-top: 14px; text-align: left; font-size: var(--fs-small);
  color: var(--ink-2); font-weight: 300; line-height: 1.6;
}
.contrast-unscoreable-label {
  display: block; font-family: var(--font-mono); font-size: var(--fs-label);
  color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.04em;
  margin-bottom: 6px;
}
.contrast-unscoreable ul { margin: 0; padding-left: 18px; }
.contrast-unscoreable li { margin: 3px 0; font-style: italic; }
.contrast-meta {
  margin-top: 10px; text-align: center; font-family: var(--font-mono);
  font-size: var(--fs-label); color: var(--ink-3);
}
/* Self-disagreement badge: appears only from the second time a given
   question has been asked in compare mode this session (including via
   "run again"). Neutral by default (the same answer came back, itself
   a real and useful confirmation); --bad when the answer genuinely
   changed, since two different answers to an identical question is a
   fact about this system's determinism, not a subjective read. */
.contrast-disagree-badge {
  margin-top: 8px; text-align: center; font-size: var(--fs-small);
  color: var(--ink-3); font-weight: 300;
}
.contrast-disagree-badge--changed {
  color: var(--bad); font-weight: 500;
}
/* Center the Run Again / Show what it was given pair as a unit: their
   own display: inline-block only centers them if their parent asks for
   it, which #contrast-body otherwise never does (everything else in it
   sets its own text-align individually). */
#contrast-body { text-align: center; }
.contrast-rerun, #contrast-context-toggle {
  display: inline-block; margin: 14px 6px 0;
  border-color: var(--ink-3); color: var(--ink-2);
}
.contrast-rerun:hover, .contrast-rerun:focus-visible,
#contrast-context-toggle:hover, #contrast-context-toggle:focus-visible {
  background: var(--hairline); color: var(--ink-0);
}
.contrast-context {
  margin: 14px auto 0; max-width: 780px; max-height: 280px; overflow-y: auto;
  text-align: left; font-family: var(--font-mono); font-size: var(--fs-small);
  line-height: 1.6; color: var(--ink-2); background: var(--surface-hi);
  border: 1px solid var(--hairline); border-radius: 6px; padding: 12px 14px;
  white-space: pre-wrap; word-break: break-word;
}

/* The verdict strip: the deterministic grounding scan, rendered as
   claim chips colored by whether each traces to a real event row.
   --good/--bad/--alert carry their frozen tokens.css meanings: a
   traceable claim is good, an untraceable one is bad, and the scope
   note that follows uses --alert to flag what this scan does NOT
   check, so a clean score is never read as more than it verified. */
.contrast-verdict { margin-top: 20px; border-top: 1px solid var(--hairline); padding-top: 14px; }
.contrast-verdict-head {
  display: flex; align-items: baseline; justify-content: center; gap: 10px;
  flex-wrap: wrap; font-family: var(--font-mono); font-size: var(--fs-small);
  color: var(--ink-2);
}
.contrast-rate { font-weight: 700; }
.contrast-rate--zero { color: var(--good); }
.contrast-rate--some { color: var(--bad); }
.contrast-claims { margin-top: 10px; text-align: center; }
.contrast-claim {
  display: inline-flex; align-items: center; font-family: var(--font-mono);
  font-size: 0.85em; border-radius: 4px; padding: 1px 8px; margin: 4px 3px;
  border: 1.5px solid var(--good); color: var(--good); cursor: default;
}
.contrast-claim--bad { border-color: var(--bad); color: var(--bad); }
.contrast-scope-note {
  margin-top: 12px; padding: 8px 12px; border-radius: 6px;
  border: 1px dashed var(--alert); background: rgba(181, 50, 110, 0.06);
  font-size: var(--fs-small); color: var(--ink-2); font-weight: 300; text-align: center;
}
.contrast-empty {
  margin-top: 16px; text-align: center; font-size: var(--fs-body);
  color: var(--ink-2); font-weight: 300;
}
.contrast-empty .contrast-reason { display: block; margin-top: 6px; font-size: var(--fs-small); color: var(--ink-3); }

/* Relational claim checking, Phase 6: does the twin connect the two
   named entities at all. --bad for a claim linking entities that
   share no edge (the flagship prj_datacenter distractor case), quiet
   ink for a claim that does check out, since a supported relational
   claim is expected, not itself noteworthy. The note beneath states
   the real boundary (connection only, not which relation or
   direction) every time this section renders, never just once. */
.contrast-relational {
  margin-top: 14px; text-align: left; font-size: var(--fs-small);
  color: var(--ink-2); font-weight: 300; line-height: 1.6;
}
.contrast-relational ul { margin: 0; padding-left: 18px; }
.contrast-relational li { margin: 4px 0; }
.contrast-relational-bad { color: var(--bad); font-weight: 500; }
.contrast-relational-ok { color: var(--ink-2); }
.contrast-relational-note {
  margin-top: 8px; font-size: var(--fs-micro); color: var(--ink-3);
  font-style: italic;
}

/* The scoreboard: a small running tally across every compare-mode
   question asked this session, pinned near the ask row so it's always
   visible without scrolling back up to it. */
.benchmark-toggle-btn { margin-top: 10px; }

/* The pre-logged benchmark panel: a stable headline number computed
   from many offline-run reps, not the noisy sample of a single live
   answer. Reuses .contrast-panel's plain construction (same dashed
   top rule, same register) since this is still reporting on the Black
   Box, just aggregated rather than live. */
.benchmark-panel { margin-bottom: 28px; }
.benchmark-summary {
  display: flex; gap: 28px; justify-content: center; flex-wrap: wrap;
  margin-top: 18px;
}
.benchmark-stat {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  max-width: 240px; text-align: center;
}
.benchmark-stat b {
  font-family: var(--font-mono); font-size: var(--fs-display); color: var(--ink-0);
}
.benchmark-stat span { font-size: var(--fs-small); color: var(--ink-2); font-weight: 300; }
.benchmark-meta {
  margin-top: 16px; text-align: center; font-size: var(--fs-label);
  color: var(--ink-3); font-family: var(--font-mono);
}
.benchmark-table { margin-top: 18px; border-top: 1px solid var(--hairline); }
.benchmark-row {
  display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 10px;
  padding: 10px 4px; border-bottom: 1px solid var(--hairline);
  font-size: var(--fs-small); color: var(--ink-1); align-items: center;
}
.benchmark-row .benchmark-q { font-weight: 500; color: var(--ink-0); text-align: left; }
.benchmark-row span:not(.benchmark-q) { font-family: var(--font-mono); text-align: center; }

@media (max-width: 640px) {
  header { padding: 60px 20px 18px; }
  .header-link { top: 16px; right: 16px; font-size: var(--fs-label); padding: 5px 10px; }
  .askrow { flex-direction: column; }
  .contrast-panel { padding: 16px 14px; }
  .benchmark-row { grid-template-columns: 1fr; text-align: left; gap: 2px; }
  .benchmark-row span:not(.benchmark-q) { text-align: left; }
}
