/* ai-agent-eval-harness-healthtech --- live demo UI
 * Hand-rolled dark-editorial stylesheet. Inter for UI, JetBrains Mono
 * for code and trace data. No framework, no build step. */

:root {
  --color-bg: #0a0a0c;
  --color-surface: #141418;
  --color-surface-hover: #1c1c22;
  --color-border: #27272d;
  --color-border-subtle: #1f1f25;
  --color-text: #fafafa;
  --color-text-muted: #a1a1aa;
  --color-text-faint: #6b6b75;
  --color-accent: #8b8bff;
  --color-accent-strong: #6e6eff;
  --color-accent-fg: #ffffff;
  /* Interactive button background: darker than --color-accent so white
     label text clears WCAG 2 AA 4.5:1 contrast (dark theme: 5.47:1). */
  --color-accent-btn: #5b5bcf;
  --color-ok: #4ade80;
  --color-refusal: #fbbf24;
  --color-error: #f87171;
  --color-code-bg: #0d0d10;
  --color-bubble-user: #1a1a22;
  --font-ui: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45);
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
  --transition-fast: 120ms cubic-bezier(0.16, 1, 0.3, 1);
  --transition-base: 200ms cubic-bezier(0.16, 1, 0.3, 1);

  /* ----- Agent Execution Graph ----- */
  /* Graph node fill/stroke per state. */
  --color-node-pending-fill: transparent;
  --color-node-pending-stroke: var(--color-border);
  --color-node-pending-text: var(--color-text-muted);
  --color-node-active-fill: color-mix(in oklab, var(--color-accent) 18%, transparent);
  --color-node-active-stroke: var(--color-accent);
  --color-node-executed-fill: color-mix(in oklab, var(--color-ok) 10%, transparent);
  --color-node-executed-stroke: color-mix(in oklab, var(--color-ok) 50%, transparent);
  --color-node-skipped-stroke: var(--color-border);
  --color-node-skipped-text: var(--color-text-faint);
  --color-node-paused-fill: color-mix(in oklab, var(--color-refusal) 15%, transparent);
  --color-node-paused-stroke: var(--color-refusal);
  /* Sub-check states. */
  --color-subcheck-pass: var(--color-ok);
  --color-subcheck-fail: var(--color-error);
  --color-subcheck-na: var(--color-text-faint);
  /* The outcome accent for escalation. Rose, not orange, so it does not
     collide with --color-refusal amber. */
  --color-escalation: #fb7185;
  /* Graph structural / terminal markers. */
  --color-graph-edge: var(--color-border);
  --color-graph-edge-active: var(--color-accent);
  --color-graph-marker: var(--color-text-faint);
  /* The desktop min-height is a real budget:
     56px header + 224px canvas wrap (the SVG is 220px tall, 1:1, + 4px).
     v1.2.0: .graph-region now uses clamp(240px, 28vh, 320px); this
     variable remains as a harmless no-op (the clamp floor matches the
     mobile override value below). */
  --graph-region-min-height: 300px;
  /* v1.2.0: canvas wrap max-width cap for ultra-wide viewports (D-02b).
     Exceeds default RAG (~974px) and HITL (~1120px) topology widths. */
  --graph-canvas-max-width: 1200px;

  /* ----- Agent Execution Graph: visual-impact layer -----
     These tokens are var() references that resolve per theme, so they
     need no [data-theme="light"] override. finalises the full
     contract; builds to these pinned values. */
  --color-pulse: var(--color-accent);
  --color-node-highlight: var(--color-accent);
  --color-trace-highlight-bg: color-mix(in oklab, var(--color-accent) 14%, transparent);
  --color-duration-bar-track: var(--color-border);
  --color-duration-bar-fill: var(--color-accent);
  --color-timeline-segment: color-mix(in oklab, var(--color-accent) 55%, var(--color-surface));
  --pulse-duration: 340ms;
  /* A genuine ease-in-out for the pulse travel; NOT the house ease-out
     (cubic-bezier(0.16, 1, 0.3, 1)), which would lurch then crawl. */
  --pulse-easing: cubic-bezier(0.4, 0, 0.2, 1);
  --inter-turn-duration: 320ms;
}

[data-theme="light"] {
  --color-bg: #fafaf9;
  --color-surface: #ffffff;
  --color-surface-hover: #f5f5f4;
  --color-border: #e4e4e7;
  --color-border-subtle: #f1f1f4;
  --color-text: #18181b;
  --color-text-muted: #52525b;
  --color-text-faint: #a1a1aa;
  --color-accent: #5b5bff;
  --color-accent-strong: #4f4fee;
  --color-accent-fg: #ffffff;
  --color-accent-btn: #5b5bff;
  --color-ok: #16a34a;
  --color-refusal: #d97706;
  --color-error: #dc2626;
  --color-code-bg: #f4f4f5;
  --color-bubble-user: #f3f3f6;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06);

  /* Agent Execution Graph: light-theme graph-token overrides.
     Most --color-node-* / --color-graph-* tokens are var() references
     that resolve per theme and clear SC 1.4.11 (3:1 non-text contrast)
     on the white light surface with no override. Three do not and get a
     measured override here; the SC 1.4.11 verification records
     the per-token yes/no.
       --color-escalation        : the rose escalation hue (kept from
         ), distinct from --color-refusal amber.
       --color-node-executed-stroke : the dark value (--color-ok at a
         ~50% transparent mix) renders far too pale on white (measured
         ~1.5:1). The light override uses the full opaque --color-ok so
         the executed node stroke clears 3:1 (measured ~3.3:1).
       --color-timeline-segment  : the dark value (--color-accent 55%
         over surface) is too pale on white (measured ~1.8:1). The light
         override raises the accent mix to 90% so the mini-timeline
         segment fill is perceivable against the strip (measured
         ~3.5:1). */
  --color-escalation: #e11d48;
  --color-node-executed-stroke: var(--color-ok);
  --color-timeline-segment: color-mix(in oklab, var(--color-accent) 90%, var(--color-surface));
}

/* WCAG 2 AA contrast overrides for light theme (phase 23 a11y gate).
   The dark-editorial accent (#8b8bff) and ok (#4ade80) colors have good
   contrast on the dark surface but fail 4.5:1 on light backgrounds.
   These overrides raise the chip text and cite-chip colors to accessible
   values without changing the brand expression on the dark theme. */

/* outcome chip (answered): darker green clears 4.5:1 on both white and
   the 14% tint (#dee9e2). Opacity is removed from chip-tail so the
   dimmed metric text stays above the 4.5:1 threshold on the tint. */
[data-theme="light"] .graph-outcome-chip[data-outcome="answered"] {
  color: #166534;
  border-color: color-mix(in oklab, #166534 45%, transparent);
  background: color-mix(in oklab, #166534 14%, transparent);
}
[data-theme="light"] .graph-outcome-chip[data-outcome="answered"] .graph-outcome-chip-tail {
  opacity: 1;
  color: #166534;
}

/* cite-chip: remove the tinted background so the accent (#5b5bff) text
   sits on the white surface, achieving 4.79:1. */
[data-theme="light"] .cite-chip {
  background: transparent;
}

/* brand-name wordmark: oklch(0.86 0.09 235) ~#95dbff reads fine on the
   dark topbar but is 1.44:1 on the light #fafaf9 surface (A11Y-01).
   Override to --color-text (#18181b) for 16.96:1 in light mode. */
[data-theme="light"] .brand-name {
  color: var(--color-text);
}

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

html, body {
  height: 100%;
  background-color: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-ui);
  font-size: 14px;
  line-height: 1.55;
  font-feature-settings: 'cv11', 'ss01', 'ss03';
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* 3g Track 1 (Foundation): track the DYNAMIC viewport so the locked,
   non-scrolling column follows the mobile URL bar / home indicator instead of
   sizing to the large viewport and hiding the composer + footer underneath.
   `100dvh` is the live-resizing height; `100svh` is the small-viewport-stable
   floor. Gated behind @supports with the `height:100%` rule above as the
   universal fallback (headless Playwright + older engines). Pixel-neutral where
   dvh==svh==vh (fixed viewports), so it adds no VR drift. */
@supports (height: 100dvh) {
  html,
  body {
    height: 100dvh;
  }
  body {
    min-height: 100svh;
  }
}

a {
  color: var(--color-accent);
  text-decoration: none;
  transition: color var(--transition-fast);
}

a:hover { color: var(--color-accent-strong); }

button {
  font-family: inherit;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

input, textarea, select {
  font-family: inherit;
  color: inherit;
}

/* ---------------- Scrollbars (unified 2026 look) ----------------
   One thin, themed, rounded scrollbar everywhere: a subtle hairline thumb
   by default, brighter on hover, over a transparent track so it floats on
   any surface (chat, graph, trace, drawers) and lets panels/modals keep
   their rounded corners. Component-specific scrollbar rules are intentionally
   removed in favour of this single source of truth. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
}
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--color-border);
  border-radius: 999px;
}
*::-webkit-scrollbar-thumb:hover { background: var(--color-text-faint); }
*::-webkit-scrollbar-corner { background: transparent; }

/* ---------------- Top bar ---------------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 56px;
  padding: 0 24px;
  border-bottom: 1px solid var(--color-border);
  background: var(--color-bg);
  flex-shrink: 0;
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  font-weight: 500;
  font-size: 13px;
  letter-spacing: -0.005em;
}

.brand-mark {
  display: inline-flex;
  width: 24px;
  height: 24px;
  border-radius: 6px;
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  flex-shrink: 0;
}

.brand-mark img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Brand name in the editorial display face used by the -docs site
   (Instrument Serif, accent-high blue) so the demo and docs share one
   wordmark treatment. */
.brand-name {
  color: oklch(0.86 0.09 235);
  font-family: 'Instrument Serif', Georgia, 'Times New Roman', serif;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1;
}

.brand-tag {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 0 9px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  color: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: lowercase;
  white-space: nowrap;
}

.topbar-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* The "load example" <select> sizes to its longest option (~520px), which by
   itself overflows the topbar row on smaller desktops and forces horizontal
   page scroll. Cap its rendered width; the full option text still shows in the
   open dropdown. Tightened further to 130px at <=480 (inside the drawer). */
#example { max-width: 200px; }

/* Phase 23 E: overflow button hidden at desktop; drawer flows inline as contents */
#topbar-overflow-btn { display: none; }
/* At desktop the drawer is transparent to flex layout; its children appear in the topbar row.
   The .hidden class is overridden here so desktop always shows the controls. */
#topbar-drawer { display: contents; }

/* 3g drawer grouping (OQ-D): the group wrappers are ALSO transparent at desktop
   so the controls still flow inline in the topbar; the tiny group labels are
   hidden. Original desktop control order is preserved via `order` in the >=961
   block below, so grouping the DOM does not reflow the desktop topbar (no 1280
   VR drift). At <=960 the groups become stacked columns inside the drawer (see
   the @media(max-width:960px) drawer block). */
.drawer-group { display: contents; }
.drawer-group-label { display: none; }

/* Desktop-only (>=961): restore the pre-3g visual order of the topbar controls
   (privacy, session-cost, example, reset, theme, end-session). `order` applies
   to the leaf controls hoisted into .topbar-controls through the two
   display:contents levels (#topbar-drawer + .drawer-group). Scoped to >=961 so
   it never reorders the stacked drawer rows at <=960. */
@media (min-width: 961px) {
  #topbar-drawer #privacy-btn { order: 1; }
  #topbar-drawer #example { order: 3; }
  #topbar-drawer #reset { order: 4; }
  #topbar-drawer #theme { order: 5; }
  #topbar-drawer #session-end-btn { order: 6; }
}

.control {
  height: 32px;
  padding: 0 12px;
  font-size: 13px;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  transition: background-color var(--transition-fast), border-color var(--transition-fast);
}

.control:hover {
  background-color: var(--color-surface-hover);
  border-color: var(--color-text-faint);
}

.control:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

select.control {
  /* Suppress every flavour of native dropdown chevron so only the
     custom CSS arrow below renders. Safari and older Edge need both
     prefixes; Firefox 100+ honours the unprefixed property. */
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  padding-right: 28px;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 14px) calc(50% + 1px), calc(100% - 10px) calc(50% + 1px);
  background-size: 4px 4px;
  background-repeat: no-repeat;
}

/* Firefox renders a focus dotted-outline on the selected option text;
   neutralise it to match the rest of the editorial styling. */
select.control:-moz-focusring {
  color: transparent;
  text-shadow: 0 0 0 var(--color-text);
}

.locale-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  height: 32px;
}

.locale-toggle button.control {
  height: 30px;
  border: none;
  border-radius: 0;
  padding: 0 8px;
  font-size: 12px;
  font-weight: 600;
  min-width: 36px;
}

.locale-toggle button.control + button.control {
  border-left: 1px solid var(--color-border);
}

.locale-toggle button.control.active {
  background: var(--color-accent-btn);
  color: var(--color-accent-fg);
  border-color: var(--color-accent-btn);
}

.locale-toggle button.control.active + button.control {
  border-left-color: var(--color-accent-btn);
}

.theme-btn {
  width: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
}

.theme-btn:hover { color: var(--color-text); }

/* Crossfade between the moon and sun glyphs based on the active
   theme. Both icons live in the DOM; CSS handles visibility so the
   JS toggle just flips data-theme on <html> and never touches the
   button's children. */
.theme-btn svg {
  display: none;
  transition: opacity var(--transition-fast);
}

[data-theme="dark"] .theme-btn .icon-moon,
[data-theme="light"] .theme-btn .icon-sun {
  display: inline-block;
}

/* ---------------- Layout ---------------- */

.layout {
  flex: 1;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-columns: 100%;
  min-height: 0;
}

.body-row {
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
  min-height: 0;
  overflow: hidden;
}

@media (max-width: 960px) {
  .body-row {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, 1.4fr) minmax(0, 1fr);
  }
}

/* ---------------- Chat pane ---------------- */

.chat-pane {
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--color-border);
  min-height: 0;
  position: relative;
  /* Phase 24: per-pane container; inline-size ONLY — never size (PITFALLS #5) */
  container-type: inline-size;
  container-name: chat-pane;
}

@media (max-width: 960px) {
  .chat-pane {
    border-right: none;
    border-bottom: 1px solid var(--color-border);
  }

  /* Agent Execution Graph responsive band (M7-T08, NFR-013).
     Below 960px the two columns stack into two grid rows and vertical
     room is scarce. Two graph-specific changes (pinned, OQ18):
       1. --graph-region-min-height drops to its certified 240px floor
          at THIS 960px breakpoint, not only at 640px. Holding the graph
          region at its 280px desktop min-height across the 640-960px
          stacked band crushes the trace panel; 240px gives the trace
          panel 40px more room across the whole band. (The 640px block
          re-sets the same 240px - harmless and explicit.)
       2. The mini-timeline strip hides at the 960px breakpoint too.
          Below 960px the per-node duration bars and the rich chip still
          convey timing, so the dedicated strip is the surface that
          yields its ~26px. The strip is `display: none`d here regardless
          of its data-open flag - a flat removal from the layout, not a
          height collapse: the strip is a flex item of the column-flex
          .graph-region and a flex item keeps min-height:auto, so a bare
          `height: 0` on a populated strip is defeated by its content's
          min-content height. `display: none` is unambiguous and removes
          it from the layout entirely. The strip element stays in the DOM
          (display:none does not break _renderMiniTimeline's content
          writes). The canvas keeps its fixed-height band and scrolls
          horizontally via the existing overflow-x: auto. */
  :root { --graph-region-min-height: 240px; }
  .graph-timeline-strip,
  .graph-timeline-strip[data-open="true"] {
    display: none;
  }
}

.empty-state {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: stretch;
  padding: 28px 36px;
  width: 100%;
  gap: 10px;
  pointer-events: none;
  overflow-y: auto;
  box-sizing: border-box;
  /* In-flow so it never overlaps the composer; flex:1 fills the chat
     pane both vertically and horizontally so a reviewer never sees a
     half-empty pane before the first turn lands. After the first turn
     `.hidden` removes it completely and `.messages` takes the slot. */
}

/* Re-enable pointer events on links inside the empty-state so the
   "executive brief" anchor stays clickable (the parent sets
   `pointer-events: none` to keep the chat pane transparent to clicks
   through the empty space). */
.empty-state a,
.empty-state .pill-strip,
.empty-state .scorer-legend {
  pointer-events: auto;
}

.empty-state.hidden {
  display: none;
}

.empty-state h2 {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.3;
  color: var(--color-text);
}

.empty-state p {
  color: var(--color-text-muted);
  font-size: 14.5px;
  line-height: 1.6;
  max-width: 640px;
}

.empty-state p a {
  color: var(--color-accent);
  /* Underline distinguishes the link from muted paragraph text
     (WCAG 2 AA link-in-text-block). */
  text-decoration: underline;
}

.empty-state .pill-strip {
  display: flex;
  gap: 6px;
  margin-top: 4px;
  flex-wrap: wrap;
}

.empty-state .pill {
  font-family: var(--font-mono);
  font-size: 11px;
  padding: 3px 9px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  color: var(--color-text-muted);
}

.empty-state .hint {
  margin-top: 8px;
  font-size: 13px;
  /* Use --color-text-muted to clear WCAG 2 AA 4.5:1 on both themes. */
  color: var(--color-text-muted);
}

/* Pre-conversation legend - compact chip layout. Each chip names a
 * scoring dimension with its initial highlighted and the one-word
 * meaning to the right; deterministic chips share the surface border,
 * judge chips carry an accent border so the four-plus-three split is
 * legible at a glance. The chip row wraps naturally so the legend never
 * forces an internal scrollbar in the chat pane. */
.scorer-legend {
  margin-top: 6px;
  padding: 8px 10px;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
  max-width: 100%;
  width: 100%;
  text-align: left;
  box-sizing: border-box;
}
.scorer-legend-title {
  font-size: 11px !important;
  font-weight: 600 !important;
  margin: 0 0 6px 0 !important;
  color: var(--color-text) !important;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  display: flex;
  align-items: baseline;
  gap: 8px;
  line-height: 1.2 !important;
}
.scorer-legend-sub {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--color-text-muted);
}
.scorer-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.scorer-chip {
  font-family: var(--font-mono);
  font-size: 11px;
  padding: 3px 9px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  color: var(--color-text);
  background: var(--color-bg, transparent);
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  line-height: 1.45;
  white-space: nowrap;
}
.scorer-chip b {
  font-weight: 700;
  color: var(--color-accent, var(--color-text));
  font-size: 12px;
}
.scorer-chip em {
  font-style: normal;
  font-size: 10px;
  color: var(--color-text-muted);
  text-transform: lowercase;
  padding-left: 4px;
  border-left: 1px solid var(--color-border);
}
.scorer-chip.judge {
  border-color: var(--color-warn, var(--color-border));
}
.scorer-chip.judge em {
  color: var(--color-warn, var(--color-text-muted));
  border-left-color: var(--color-warn, var(--color-border));
}
.scorer-legend.hidden {
  display: none;
}

.empty-state .hint kbd {
  font-family: var(--font-mono);
  font-size: 11px;
  padding: 1px 6px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 4px;
  color: var(--color-text);
}

.messages {
  flex: 1;
  overflow-y: auto;
  padding: 28px 32px 16px;
  display: flex;
  flex-direction: column;
  gap: 22px;
  list-style: none;
  scroll-behavior: smooth;
  min-height: 0;
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
}

/* When the empty-state is showing, .messages still exists as an empty ol
   in the DOM but should not take any flex space - empty-state holds the
   slot. */
.empty-state:not(.hidden) + .messages {
  flex: 0;
  padding-top: 0;
  padding-bottom: 0;
}

.message {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  animation: slideUp 240ms cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes slideUp {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

.message-role {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  margin-top: 2px;
}

.message[data-role="user"] .message-role {
  background: linear-gradient(135deg, var(--color-accent), var(--color-accent-strong));
  color: var(--color-accent-fg);
  border-color: transparent;
}

/* Assistant avatar: the brand mark fills the role square. The container keeps
   its border so the data-generating accent ring (below) still reads as the
   active-turn signal. */
.message[data-role="assistant"] .message-role {
  padding: 0;
  overflow: hidden;
}
.message-role .role-avatar {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: inherit;
}

.message-bubble {
  flex: 1;
  min-width: 0;
  padding-top: 3px;
  position: relative;
}

/* Phase 23 D: copy-to-clipboard button (C-05) */
.copy-btn {
  width: 24px;
  height: 24px;
  padding: 0;
  background: transparent;
  border: none;
  color: var(--color-text-faint);
  border-radius: var(--radius-sm);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.copy-btn:hover {
  color: var(--color-text-muted);
  background: var(--color-surface);
}

.copy-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Message copy button: faint by default so it is discoverable (Checkpoint-2
   #3), full on hover or keyboard focus. Touch devices (no hover) keep it fully
   visible via the any-hover rule below. */
.message .copy-btn {
  position: absolute;
  top: 0;
  right: 0;
  opacity: 0.4;
  transition: opacity var(--transition-fast);
}

.message:hover .copy-btn,
.message .copy-btn:focus-visible {
  opacity: 1;
}

/* Always visible on touch devices (no hover capability) */
@media (any-hover: none) {
  .message .copy-btn {
    opacity: 1;
  }
}

.message-content {
  color: var(--color-text);
  white-space: pre-wrap;
  word-wrap: break-word;
  font-size: 15px;
  line-height: 1.65;
}

.message-content code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  padding: 1px 5px;
  background: var(--color-code-bg);
  border: 1px solid var(--color-border-subtle);
  border-radius: 3px;
}

.cite-chip {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.82em;
  padding: 1px 6px;
  margin: 0 2px;
  background: color-mix(in oklab, var(--color-accent) 12%, transparent);
  border: 1px solid color-mix(in oklab, var(--color-accent) 30%, transparent);
  border-radius: 4px;
  color: var(--color-accent);
  vertical-align: baseline;
  cursor: pointer;
}

/* Phase 23 C2: cite-chip interactive states (button, focus ring, open indicator) */
.cite-chip[aria-expanded="true"] {
  border-color: var(--color-accent);
}

.cite-chip:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Phase 26: citation chip strip — trailing child of .message-content on answered+cited turns */
.cite-chip-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  margin-top: 16px;
}

.cite-chip-label {
  font-size: 0.82em;
  color: var(--color-text-muted); /* --color-text-muted clears WCAG 2 AA; --color-text-faint does not */
  margin-right: 8px;
  white-space: nowrap;
  user-select: none;
}

/* Scoped padding override for WCAG 2.5.8 (24px min target height).
   The shared .cite-chip at 1px 6px is untouched (dead [cite:] path reuses it). */
.cite-chip-strip .cite-chip {
  padding: 4px 8px;
  min-height: 24px; /* WCAG 2.5.8 target size — NFR-023 */
  box-sizing: border-box;
}

/* Transient trace-row highlight driven by chip click (D-04).
   Mirrors .trace-decision[data-trace-highlight-*] at app.css:1481-1484.
   Added/removed by JS — no transition needed (instant highlight per UI-SPEC). */
.trace-citations li.cite-trace-highlight {
  background: var(--color-trace-highlight-bg);
  border-left: 2px solid var(--color-node-highlight);
}

/* Markdown rendered inside assistant messages (the Markdown parser
   in app.js converts the LLM output into safe DOM nodes). Styling
   stays editorial: tight inline emphasis, padded code blocks,
   compact lists, lightweight heading hierarchy. */

.message-content p {
  margin: 0;
}

.message-content p + p,
.message-content p + ul,
.message-content p + ol,
.message-content p + pre,
.message-content p + h3,
.message-content p + h4,
.message-content p + h5,
.message-content p + h6,
.message-content ul + p,
.message-content ol + p,
.message-content pre + p,
.message-content h3 + p,
.message-content h4 + p,
.message-content h5 + p,
.message-content h6 + p {
  margin-top: 10px;
}

.message-content strong {
  font-weight: 600;
  color: var(--color-text);
}

.message-content em {
  font-style: italic;
}

.message-content a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.message-content a:hover {
  color: var(--color-accent-strong);
}

.message-content ul,
.message-content ol {
  margin: 8px 0 0 0;
  padding-left: 22px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.message-content li {
  line-height: 1.6;
}

.message-content li::marker {
  color: var(--color-text-faint);
}

.message-content pre {
  margin: 10px 0 0 0;
  padding: 12px 14px;
  background: var(--color-code-bg);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.55;
}

.message-content pre code {
  font-family: inherit;
  font-size: inherit;
  padding: 0;
  background: transparent;
  border: 0;
}

.message-content h2,
.message-content h3,
.message-content h4,
.message-content h5,
.message-content h6 {
  margin: 12px 0 4px 0;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.3;
  color: var(--color-text);
}

.message-content h2 { font-size: 17px; }
.message-content h3 { font-size: 15.5px; }
.message-content h4 { font-size: 14.5px; }
.message-content h5 { font-size: 13.5px; }
.message-content h6 { font-size: 12.5px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-muted); }

.message[data-error="true"] .message-content {
  color: var(--color-error);
  font-family: var(--font-mono);
  font-size: 13px;
}

/* Thinking indicator */
.thinking {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 2px;
}

.thinking-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--color-text-muted);
  animation: thinking 1.3s infinite ease-in-out both;
}

.thinking-dot:nth-child(1) { animation-delay: -0.32s; }
.thinking-dot:nth-child(2) { animation-delay: -0.16s; }

@keyframes thinking {
  0%, 80%, 100% { opacity: 0.25; transform: scale(0.78); }
  40% { opacity: 1; transform: scale(1); }
}

/* Composer */
.composer {
  display: flex;
  gap: 8px;
  /* 3g Track 1: the bottom padding clears the mobile home-indicator inset when
     the composer is the sticky bottom bar (Track 3). max() keeps the 24px
     resting padding everywhere env() resolves to 0 (desktop + headless VR), so
     it is pixel-neutral until a real safe-area inset exists. */
  padding: 16px 32px max(24px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--color-border);
  background: var(--color-bg);
  align-items: flex-end;
}

#input {
  flex: 1;
  resize: none;
  padding: 8px 14px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
  /* Resting height equals the send button and fits ~2 lines of the resized
     font; longer input scrolls vertically inside this fixed box rather than
     growing the composer (see autoResize() in app.js, now a no-op). */
  height: 54px;
  overflow-y: auto;
}

#input::placeholder { color: var(--color-text-faint); }

#input:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-accent) 18%, transparent);
}

#send {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 54px;
  padding: 0 16px;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-accent-fg);
  background: var(--color-accent-btn);
  border: 1px solid var(--color-accent-btn);
  border-radius: var(--radius-md);
  transition: filter var(--transition-fast), transform var(--transition-fast);
  box-shadow: var(--shadow-sm);
}

#send:hover:not(:disabled) { filter: brightness(1.08); }
#send:active:not(:disabled) { transform: translateY(1px); }

#send:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

#send kbd {
  font-family: var(--font-mono);
  font-size: 10.5px;
  padding: 2px 6px;
  /* Dark overlay over the button bg so white text clears 4.5:1 AA. */
  background: rgba(0, 0, 0, 0.35);
  color: #ffffff;
  border-radius: 3px;
  font-weight: 400;
}

/* ---------------- Trace pane ---------------- */
/* Backend trace panel only; the Agent Execution Graph is a sibling of
   body-row under <main>, not a child of trace-pane (v1.2.0 FR-028). */

.trace-pane {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  /* Phase 24: per-pane container; inline-size ONLY — never size (PITFALLS #5) */
  container-type: inline-size;
  container-name: trace-pane;
}

/* 3g phone-only controls: hidden by DEFAULT via the stylesheet (NOT inline
   style, so the responsive @media rules below can override them — an inline
   display:none cannot be beaten by a stylesheet rule). The graph peek toggle is
   revealed by @media(max-width:960px); the trace sheet trigger/close/handle by
   @media(max-width:640px). These base rules sit BEFORE those media blocks so the
   media rules (equal-or-higher specificity, later in source) win when active. */
.graph-peek-toggle-btn { display: none; }
.trace-sheet-trigger { display: none; }
#trace-sheet-close { display: none; }
#trace-sheet-handle { display: none; }

/* ----- Agent Execution Graph region ----- */

.graph-region {
  flex: 0 0 auto;            /* harmless under the new grid parent; kept for compat */
  min-height: clamp(280px, 35vh, 400px);
  display: flex;
  flex-direction: column;
  position: relative;        /* positioning context for the overlay */
  overflow: visible;         /* the overlay may extend within the region */
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
  container-type: inline-size;  /* inline-size ONLY — never size (PITFALLS #5) */
  container-name: graph-region;
}

/* 3g graph peek (OQ-G): at <=960 the graph hero collapses to a ~56px header
   peek by default (#graph-region[data-collapsed="true"] in the markup).
   #graph-peek-toggle flips data-collapsed to "false" (app.js wireGraphPeek) and
   the band expands to a bounded min(48vh,320px) with the existing horizontal-
   scroll canvas. Collapsed: the canvas + legend are visibility:hidden so their
   focusable graph nodes leave the tab order + a11y tree, and the region clips
   via max-height + overflow:hidden. Expanded: overflow returns to visible so the
   sub-check overlay is not clipped. Desktop (>=961) ignores data-collapsed and
   keeps the clamp(280px,35vh,400px) floor on .graph-region above (unchanged) --
   so the graph floor is now breakpoint-aware. This block is placed AFTER the
   .graph-region base so its <=960 min-height:0 + max-height win the cascade. */
@media (max-width: 960px) {
  .graph-region {
    min-height: 0;
    max-height: 58px;          /* graph-header (56px + 1px border) + region border */
    overflow: hidden;
  }
  .graph-region[data-collapsed="false"] {
    max-height: min(48vh, 320px);
    overflow: visible;
  }
  .graph-region[data-collapsed="true"] .graph-legend,
  .graph-region[data-collapsed="true"] .graph-canvas-wrap {
    visibility: hidden;
  }
  #graph-peek-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    flex: 0 0 auto;
  }
}

/* Motion gate (mirrors the graph motion gate): animate the peek height + the
   chevron only when motion is allowed; reduced-motion collapses/expands
   instantly with no height tween. */
@media (max-width: 960px) and (prefers-reduced-motion: no-preference) {
  .graph-region { transition: max-height var(--transition-base); }
  .graph-peek-chevron { transition: transform var(--transition-fast); }
  .graph-region[data-collapsed="false"] .graph-peek-chevron {
    transform: rotate(180deg);
  }
}

.graph-header {
  flex: 0 0 auto;
  height: 56px;             /* matched to .trace-header */
  padding: 0 24px;          /* matched to .trace-header */
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid var(--color-border-subtle);
}

.graph-header-text {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.graph-header h3 {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text);
}

.graph-header-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}

.graph-sub {
  font-size: 11.5px;
  /* Use --color-text-muted (not --color-text-faint) to clear WCAG 2 AA
     4.5:1 contrast on both dark (#141418) and light (#ffffff) surfaces. */
  color: var(--color-text-muted);
  margin-top: 2px;
}

.graph-canvas-wrap {
  flex: 1 1 auto;
  height: auto;
  width: 100%;
  max-width: var(--graph-canvas-max-width);
  margin: 0 auto;
  overflow: hidden;
  /* In a flex-column parent, cross-axis stretch is bounded by the
     child's min-content size. The SVG's intrinsic width comes from
     its viewBox attribute and can exceed the viewport at narrow
     widths, so without an explicit `width: 100%` the wrap would grow
     to match the SVG and overflow .graph-region instead of letting
     the @container 720px fallback below scroll the content
     horizontally inside a viewport-bound wrap. `width: 100%` plus
     `min-width: 0` pins the wrap to its parent's inline-size; the
     SVG then overflows within and `overflow-x: auto` exposes it as
     horizontal scroll. */
  min-width: 0;
}

/* The SVG scales responsively via viewBox + preserveAspectRatio="xMinYMid
   meet" + CSS 100%/100% dimensions. The logical SVG_H (220) is carried
   in the viewBox attribute set from JS; it no longer appears in CSS. */
.graph-canvas-wrap svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Below the SVG's natural width, fall back to v1.1.0 horizontal scroll.
   The 720px threshold is hardcoded — @container conditions cannot
   reference custom properties (CONTEXT.md §Deferred Ideas). */
@container graph-region (max-width: 720px) {
  .graph-canvas-wrap { overflow-x: auto; overflow-y: hidden; }
  .graph-canvas-wrap svg { width: auto; min-width: 100%; }
}

/* ----- Affordance legend (FR-034, ----- */
/* Touch-first default: hide hover group, show touch group. */
.graph-legend {
  flex: 0 0 auto;
  padding: 8px 24px;
  font-size: 0.75rem;
  color: var(--color-text-muted);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0;
}

.legend-sep {
  margin: 0 8px;
}

.legend-mode-group {
  display: inline-flex;
  align-items: center;
}

.graph-legend [data-group="hover"] {
  display: none;
}

/* Hover-capable devices: show hover group, hide touch group. */
@media (any-hover: hover) {
  .graph-legend [data-group="hover"] {
    display: inline-flex;
  }
  .graph-legend [data-group="touch"] {
    display: none;
  }
}

/* any-hover: hover covers hover-capable devices (mouse, trackpad).
   Fine-pointer-only devices without hover (stylus tablets) see the
   always-visible keyboard span instead -- showing hover copy to a
   device that cannot hover would be misleading. */


/* ----- Agent Execution Graph: SVG nodes, edges, states -----
   The node-state rules are written against the data-state attribute on
   each node <g> and against CSS custom properties, so swapping the
   theme swaps the property values and the SVG re-paints automatically;
   no graph CSS needs a [data-theme="light"] override (the only
   light-theme graph token, --color-escalation, is set in :root scope
   above). */

/* Edges: every edge is a faintly-drawn <path>; the straight-vs-arc
   distinction is geometry only and carries no semantics. */
.graph-edge {
  stroke: var(--color-graph-edge);
  stroke-width: 1.5;
  fill: none;
}

/* Terminal markers: start is a solid dot, end is a hollow ring. They
   never transition and must never appear to execute. */
.marker-start {
  fill: var(--color-graph-marker);
  stroke: none;
}
.marker-end {
  fill: none;
  stroke: var(--color-graph-marker);
  stroke-width: 2;
}

/* Node label typography: node labels are code identifiers, rendered in
   the mono face (SVG <text> does not inherit the page font reliably). */
.node-label {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
}
.node-duration {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  fill: var(--color-text-faint);
}
.node-state-label {
  font-family: var(--font-ui);
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  fill: var(--color-text-faint);
}

/* The five node states, driven by the data-state attribute on the
   node <g>. The pending baseline applies to every [data-node] group. */
[data-node] .node-label {
  fill: var(--color-node-pending-text);
}
[data-node] rect {
  fill: var(--color-node-pending-fill);
  stroke: var(--color-node-pending-stroke);
  stroke-width: 1.25;
}

[data-state="active"] rect {
  fill: var(--color-node-active-fill);
  stroke: var(--color-node-active-stroke);
}
[data-state="executed"] rect {
  fill: var(--color-node-executed-fill);
  stroke: var(--color-node-executed-stroke);
}
[data-state="paused"] rect {
  fill: var(--color-node-paused-fill);
  stroke: var(--color-node-paused-stroke);
}

/* skipped: a dashed stroke plus reduced group opacity, so it reads as
   "present but bypassed", visibly distinct from pending. */
[data-state="skipped"] rect {
  fill: transparent;
  stroke: var(--color-node-skipped-stroke);
  stroke-dasharray: 4 3;
}
[data-state="skipped"] {
  opacity: 0.55;
}
[data-state="skipped"] .node-label {
  fill: var(--color-node-skipped-text);
}

/* The duration and state-label text visibility is controlled by the
   fill-opacity presentation attribute, NOT display (display has no
   defined SVG layout value). The duration shows only when executed;
   the state word shows only when skipped or paused. */
.node-duration {
  fill-opacity: 0;
}
[data-state="executed"] .node-duration {
  fill-opacity: 1;
}
.node-state-label {
  fill-opacity: 0;
}
[data-state="skipped"] .node-state-label,
[data-state="paused"] .node-state-label {
  fill-opacity: 1;
}

/* The per-state shape glyph: the non-color state channel (WCAG SC
   1.4.1). active is a triangle, executed a check, pending a hollow dot,
   paused two bars; skipped has no glyph (its dashed stroke carries it). */
.glyph-active {
  fill: var(--color-node-active-stroke);
}
.glyph-executed {
  stroke: var(--color-node-executed-stroke);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}
.glyph-paused {
  fill: var(--color-node-paused-stroke);
}
.glyph-pending {
  stroke: var(--color-node-pending-stroke);
  stroke-width: 1.25;
  fill: none;
}

/* The expandable guardrail control: a native HTML <button> wrapped in
   an SVG <foreignObject> over the guardrail node rect. Transparent so
   the SVG node art shows through. */
.node-expand-btn {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: flex-start;
  justify-content: flex-start;
  padding: 3px 8px;
  background: transparent;
  border: 0;
  cursor: pointer;
  color: var(--color-text-faint);
}
.node-expand-btn:disabled {
  cursor: default;
  opacity: 0.5;
}
.node-expand-btn:not(:disabled):hover {
  color: var(--color-accent);
}
.node-expand-icon {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
}
/* A normal CSS outline on a real <button> renders reliably and matches
   the SPA's existing .control:focus-visible. */
.node-expand-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* CSS transitions and the motion layer, all gated on
   prefers-reduced-motion. When the user prefers reduced motion NO rule
   in this block applies: node-state and theme-flip color changes
   resolve instantly, the data-flow pulse does not animate, the
   duration-bar reveal is instant, the hover lift is instant, and the
   mini-timeline strip opens / collapses instantly. This single block
   is the one CSS motion gate; no graph motion rule sits outside it. */
@media (prefers-reduced-motion: no-preference) {
  /* The node rect transition covers all three animated properties:
     fill and stroke for node-state and theme-flip color changes, and
     stroke-width for the hover lift. */
  [data-node] rect {
    transition: fill var(--transition-base), stroke var(--transition-base),
      stroke-width var(--transition-fast);
  }
  [data-node] .node-duration,
  [data-node] .node-state-label {
    transition: fill-opacity var(--transition-base);
  }

  /* The per-node duration bar: only its OPACITY reveal is animated -
     the bar's WIDTH is never animated (the width encodes one static
     measured number; a width tween would be non-data-driven motion). */
  .duration-bar-track,
  .duration-bar-fill {
    transition: opacity var(--transition-base);
  }

  /* The mini-timeline strip open / inter-turn collapse: a height plus
     opacity transition. The strip element stays in the DOM; only its
     height and opacity move. */
  .graph-timeline-strip {
    transition: height var(--inter-turn-duration), opacity var(--inter-turn-duration);
  }

  /* The data-flow pulse: a soft-edged dot travelling one traversed
     edge once per real handoff, via CSS Motion Path. The whole pulse
     motion - the @keyframes, the animation shorthand, the offset-path
     rule - lives inside this no-preference block, so reduced motion
     has no pulse animation at all (the JS _startPulse also early-
     returns under reduced motion: the mandatory double gate). */
  @keyframes edge-pulse-travel {
    from {
      offset-distance: 0%;
    }
    to {
      offset-distance: 100%;
    }
  }
  .edge-pulse {
    offset-path: var(--edge-d);
    offset-rotate: 0deg;
    animation: edge-pulse-travel var(--pulse-duration) var(--pulse-easing) 1;
  }

  /* The sub-check overlay open / close transition (M7-T08, the lens-1 /
     lens-3 reconciliation; OQ13). The  .hidden { display: none }
     rule is kept; this adds, on top, a transition over opacity and a
     small transform, plus transition-behavior: allow-discrete on the
     display property so display flips at the START of the discrete
     transition. The element is therefore rendered (not display:none) for
     the whole transition, so _openOverlay's synchronous
     getBoundingClientRect measure and _overlay.focus() both run against a
     rendered, focusable element - the measure-position-focus
     sequence is preserved unchanged. @starting-style supplies the entry
     `from` values; only opacity and transform animate, neither of which
     affects the measured layout box. The whole declaration is inside
     this no-preference block, so reduced motion resolves it instantly;
     an engine lacking allow-discrete shows the overlay instantly (a
     correct progressive enhancement). */
  .subcheck-overlay {
    opacity: 1;
    transform: none;
    transition: opacity var(--transition-fast), transform var(--transition-fast),
      display var(--transition-fast);
    transition-behavior: allow-discrete;
  }
  @starting-style {
    .subcheck-overlay {
      opacity: 0;
      transform: translateY(-4px);
    }
  }
}

/* =====================================================================
   Agent Execution Graph: visual-impact layer
   Every rule below is functional - it renders an element driven by real
   execution telemetry. finalises the full theme-token contract.
   ===================================================================== */

/* ----- The data-flow pulse -----
   A soft-edged dot (a bright core via a drop-shadow halo) on one edge,
   travelling once per real handoff. cx/cy are 0 in the markup; CSS
   Motion Path drives the local origin. The @keyframes / animation /
   offset-path rules live inside the no-preference block above. */
.edge-pulse {
  fill: var(--color-pulse);
  /* A soft halo, not a flat hard disc; no motion trail, no smear. */
  filter: drop-shadow(0 0 2px var(--color-pulse));
}

/* ----- The per-node latency duration bar -----
   A track plus a fill rect in the duration strip, below the NNN ms
   figure. Visible only on an executed node, exactly as the duration
   text is gated, so a skipped node shows NO bar with no JS branching -
   a zero-length bar would falsely read as "ran in 0 ms". The bar's
   WIDTH is set in JS to a static measured value and is never animated;
   only the opacity reveal (in the no-preference block) is permitted. */
.duration-bar-track {
  fill: var(--color-duration-bar-track);
  opacity: 0;
}
.duration-bar-fill {
  fill: var(--color-duration-bar-fill);
  opacity: 0;
}
[data-state="executed"] .duration-bar-track,
[data-state="executed"] .duration-bar-fill {
  opacity: 1;
}

/* The live elapsed counter writes into the node-duration <text> during
   the active state;  hides that <text> with fill-opacity:0
   except when executed. This rule makes the counter visible during
   active - without it the entire counter renders invisibly. */
[data-state="active"] .node-duration {
  fill-opacity: 1;
}

/* ----- Node hover and the node-button focus ring -----
   Every executable node has a native <button> in a <foreignObject>
   sized to the node rect. The button is transparent so the SVG node
   art shows through; it carries the keyboard focus. */
.node-btn {
  width: 100%;
  height: 100%;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  touch-action: manipulation;
  /* The button has no visible chrome of its own; the SVG rect is the
     visible node. A pointer cursor signals it is interactive.
     touch-action: manipulation suppresses the 300ms double-tap delay
     on mobile browsers (FR-033). */
}
/* The node-button focus ring (M7-T08, SC 2.4.7 / SC 2.4.11).
   :focus-visible (NOT :focus) so a mouse click leaves no ring and a
   keyboard Tab does, matching the SPA house pattern. The ring is INSET
   (outline-offset: -2px), not the house +2px offset: the button fills an
   SVG <foreignObject> sized exactly to the node rect, and a foreignObject
   clips painting outside its bounds, so a positive-offset outline would
   be drawn at the foreignObject edge and clipped. An inset ring sits
   fully inside the node rect, is never clipped by the foreignObject or
   the SVG viewport, and stays clear of the H_GAP-separated neighbours and
   of the :focus-visible vs data-highlight-hover/pin distinction (the highlight is a
   stroke recolour; the focus ring is an inset outline - different
   channels, composable). border-radius matches the node rect corner. */
.node-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
  border-radius: var(--radius-sm);
}

/* Node hover: a uniform, hue-PRESERVING lift - the rect stroke gains a
   little width on every one of the five states (the same delta). Hover
   must NOT change the fill hue (that would read as a state change). The
   stroke-width transition is gated in the no-preference block. */
.graph-node:hover rect {
  stroke-width: 1.75;
}

/* ----- FR-026 two-channel highlight (M7-T07, -----
   The node highlight and the trace-row highlight are each perceivable
   without color and read as the SAME relationship (the accent family).
   Two channels: hover (transient) and pin (sticky) produce identical
   visual output via comma-grouped selectors. */
.graph-node[data-highlight-hover="true"] rect,
.graph-node[data-highlight-pin="true"] rect {
  stroke: var(--color-node-highlight);
  stroke-width: 1.5;
  fill: color-mix(in oklab, var(--color-node-highlight) 16%, transparent);
}

/* The trace-row highlight (both channels): a faint accent background
   plus an accent left-border - the same accent family as the node. */
.trace-decision[data-trace-highlight-hover="true"],
.trace-decision[data-trace-highlight-pin="true"] {
  background: var(--color-trace-highlight-bg);
  border-left: 2px solid var(--color-node-highlight);
}

/* ----- The mini-timeline strip -----
   A dedicated thin strip inside .graph-region, between the header and
   the canvas wrap. Collapsed (height 0) until turn_completed; it opens
   to a fixed ~26px then and the inter-turn reset collapses it again.
   Its appearing only after a real completed turn is itself honest. */
.graph-timeline-strip {
  flex: 0 0 auto;
  height: 0;
  opacity: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 24px;
  border-bottom: 1px solid var(--color-border-subtle);
}
.graph-timeline-strip[data-open="true"] {
  height: 26px;
  opacity: 1;
}
.timeline-bar {
  flex: 1;
  min-width: 0;
  display: flex;
  gap: 1px; /* a 1px gap so segment boundaries are perceivable (SC 1.4.11) */
  height: 8px;
  border-radius: 2px;
  overflow: hidden;
}
.timeline-segment {
  /* flex-grow is set per segment in JS to the raw duration_ms; flexbox
     normalises the proportions. A single neutral fill - every segment
     is an executed node, so a state hue would be uniform and pointless;
     the proportion is the information. */
  background: var(--color-timeline-segment);
  border-radius: 1px;
}
.timeline-label {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--color-text-muted);
  white-space: nowrap;
}

/* ----- The "expand all sub-checks" control -----
   A small bordered button matching the SPA .control language; it sits
   to the LEFT of the rich chip, slightly de-emphasized. */
.graph-expand-all {
  font-family: var(--font-ui);
  font-size: 10.5px;
  font-weight: 500;
  padding: 3px 9px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-muted);
  white-space: nowrap;
}
.graph-expand-all:not(:disabled):hover {
  background: var(--color-surface-hover);
  border-color: var(--color-text-faint);
  color: var(--color-text);
}
.graph-expand-all:disabled {
  opacity: 0.5;
  cursor: default;
}
.graph-expand-all:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

/* ----- The HITL pause visual beat -----
   When the graph enters paused the held review_response node is the
   brightest thing; the already-executed nodes drop to a marginally
   lower opacity (0.8, not as low as skipped's 0.55), pulling the eye to
   the one node that matters. A single one-shot opacity change; no
   looping pulse, no breathing animation. */
[data-paused="true"] .graph-node[data-state="executed"] {
  opacity: 0.8;
}

/* ----- The outcome accent on the graph -----
   On turn_completed a data-outcome attribute on the SVG root recolors
   the executed-node STROKE to the outcome hue for refused (amber) and
   escalated (rose). answered keeps the green executed stroke. This is
   a recolor of one existing state layer, not a decorative wash. */
[data-outcome="refused"] .graph-node[data-state="executed"] rect {
  stroke: var(--color-refusal);
}
[data-outcome="escalated"] .graph-node[data-state="executed"] rect {
  stroke: var(--color-escalation);
}

/* ----- The chat-bubble shared accent -----
   When generate_response goes active the in-flight assistant bubble
   gets data-generating; its avatar square (which already has a border
   and a background) picks up the accent, mirroring how the active node
   rect is accent-filled - the same hue on the avatar and the node is
   the "same work" tie. The accent is a static state; it must NOT
   animate in any way that suggests token-by-token text arrival. */
.message[data-generating="true"] .message-role {
  border-color: var(--color-accent);
  background: color-mix(in oklab, var(--color-accent) 22%, var(--color-surface));
  color: var(--color-accent);
}

/* ----- The turn-outcome chip (HTML, in graph-header-controls) -----
   The four outcomes are separated by WORDS, not hue; the color is a
   reinforcement. It reuses the bordered, background-tinted, rounded
   chip treatment of the trace-decision pills. */
.graph-outcome-chip {
  font-family: var(--font-ui);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  color: var(--color-text-muted);
  white-space: nowrap;
}

/* The rich chip: larger, sentence-case, two-part. The outcome
   word at ~13px weight 600; the metrics tail at ~12px weight 400. The
   global uppercase is dropped - "2.1 S" reads worse than "2.1 s". */
.graph-outcome-chip-rich {
  display: inline-flex;
  align-items: baseline;
  gap: 0;
  font-size: 13px;
  letter-spacing: 0;
  text-transform: none;
  padding: 4px 11px;
}
.graph-outcome-chip-word {
  font-weight: 600;
}
.graph-outcome-chip-tail {
  font-size: 12px;
  font-weight: 400;
  opacity: 0.85;
  margin-left: 5px;
}
.graph-outcome-chip[data-outcome="answered"] {
  color: var(--color-ok);
  border-color: color-mix(in oklab, var(--color-ok) 45%, transparent);
  background: color-mix(in oklab, var(--color-ok) 14%, transparent);
}
.graph-outcome-chip[data-outcome="refused"] {
  color: var(--color-refusal);
  border-color: color-mix(in oklab, var(--color-refusal) 45%, transparent);
  background: color-mix(in oklab, var(--color-refusal) 16%, transparent);
}
.graph-outcome-chip[data-outcome="escalated"] {
  color: var(--color-escalation);
  border-color: color-mix(in oklab, var(--color-escalation) 45%, transparent);
  background: color-mix(in oklab, var(--color-escalation) 16%, transparent);
}
.graph-outcome-chip[data-outcome="paused"] {
  color: var(--color-accent);
  border-color: color-mix(in oklab, var(--color-accent) 45%, transparent);
  background: color-mix(in oklab, var(--color-accent) 16%, transparent);
}

/* ----- The sub-check overlay (an HTML <div>, child of .graph-region) -----
   It escapes the SVG viewport entirely; it is layered above the canvas
   wrap and positioned by JS from the target node rect. */
.subcheck-overlay {
  position: absolute;
  z-index: 20;
  min-width: 232px;
  max-width: 300px;
  padding: 10px 12px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.subcheck-overlay.hidden {
  display: none;
}

.subcheck-overlay:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.subcheck-overlay-header {
  font-family: var(--font-ui);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.subcheck-list {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* The sub-check rows visually rhyme with the trace panel's
   .trace-decision rows so the two right-column regions read as one
   coherent surface. */
.subcheck-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  background: var(--color-bg);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
}

.subcheck-name {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--color-text);
}

/* The three honest sub-check states: pass, fail, not-evaluated; plus
   the pii-redaction transform annotation, which is never pass/fail. */
.subcheck-pill {
  flex: 0 0 auto;
  font-family: var(--font-ui);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 3px;
}
.subcheck-pill[data-subcheck="pass"] {
  color: var(--color-subcheck-pass);
  background: color-mix(in oklab, var(--color-subcheck-pass) 16%, transparent);
}
.subcheck-pill[data-subcheck="fail"] {
  color: var(--color-subcheck-fail);
  background: color-mix(in oklab, var(--color-subcheck-fail) 18%, transparent);
}
.subcheck-pill[data-subcheck="not-evaluated"] {
  color: var(--color-subcheck-na);
  background: color-mix(in oklab, var(--color-subcheck-na) 18%, transparent);
}
.subcheck-pill[data-subcheck="transform"] {
  color: var(--color-accent);
  background: color-mix(in oklab, var(--color-accent) 16%, transparent);
}

/* A not-evaluated row is dimmed: the guardrail short-circuited before
   this sub-check ran. */
.subcheck-row[data-subcheck="not-evaluated"] {
  opacity: 0.7;
}

/* The combined "expand all" panel uses COMPACT rows: name plus the
   state pill only, the per-row reason demoted to a title tooltip, so
   six rows plus two section headers fit a 280px region. */
.subcheck-row-compact {
  padding: 3px 8px;
}

/* The combined panel: at the smallest breakpoint even the compact
   panel may not fit, so it scrolls INSIDE its own box rather than
   overflowing the region. An internal scroll only at the smallest
   breakpoint is acceptable; a desktop scroll is not. */
.subcheck-overlay[data-mode="combined"] {
  max-height: calc(var(--graph-region-min-height) - 16px);
  overflow-y: auto;
}

.subcheck-detail {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--color-text-muted);
  /* Checkpoint-2 fix: wrap so the guardrail reason is fully readable in the
     overlay instead of clipped to one line. The overlay is a popover that
     grows downward; the smallest breakpoint already scrolls inside its box. */
  white-space: normal;
  overflow-wrap: anywhere;
}


.trace-region {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

/* ----- Screen-reader-only utility ----- */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ----- Skip-to-content link ----- */

/* Off-screen until focused so it does not disturb the visual layout, then
   pulled on-screen as a top-anchored button for keyboard users (WCAG 2.4.1). */
.skip-link {
  position: absolute;
  left: 8px;
  top: -48px;
  z-index: 1000;
  padding: 8px 14px;
  border-radius: 6px;
  background: var(--color-accent, #2563eb);
  color: #fff;
  font-weight: 600;
  text-decoration: none;
  transition: top 0.15s ease-in-out;
}

.skip-link:focus,
.skip-link:focus-visible {
  top: 8px;
  outline: 2px solid #fff;
  outline-offset: 2px;
}

.trace-header {
  flex-shrink: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  height: 56px;
  padding: 0 24px;
  border-bottom: 1px solid var(--color-border);
}

.trace-header-text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  flex: 1;
  order: 1;
}

#trace-json-copy-btn {
  order: 2;
}

.trace-header h3 {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text);
}

.trace-sub {
  font-size: 11.5px;
  /* Use --color-text-muted to clear WCAG 2 AA 4.5:1 on both themes. */
  color: var(--color-text-muted);
  margin-top: 2px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.trace-sub code {
  font-family: var(--font-mono);
  color: var(--color-text-muted);
}

.trace-body {
  flex: 1;
  min-width: 0;    /* Phase 24 D-08: prevents wide monospace content from forcing
                      the trace pane to grow beyond its container width */
  overflow-y: auto;
  padding: 20px 24px 32px;
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.65;
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
}

.trace-empty {
  font-family: var(--font-ui);
  font-size: 13px;
  line-height: 1.65;
  /* Use --color-text-muted to clear WCAG 2 AA 4.5:1 on both themes. */
  color: var(--color-text-muted);
}

.trace-empty p + p { margin-top: 10px; }

.trace-turn {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding-bottom: 22px;
  margin-bottom: 22px;
  border-bottom: 1px dashed var(--color-border);
  animation: fadeIn 280ms ease-out;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateX(4px); }
  to { opacity: 1; transform: translateX(0); }
}

.trace-turn:last-child {
  border-bottom: none;
  margin-bottom: 0;
}

.trace-turn-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-family: var(--font-ui);
}

.trace-turn-header strong {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text);
}

.trace-turn-header span {
  font-family: var(--font-mono);
  font-size: 11px;
  /* Use --color-text-muted to clear WCAG 2 AA 4.5:1 on both themes. */
  color: var(--color-text-muted);
}

.trace-section-label {
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  display: block;
  margin-bottom: 6px;
}

/* F1 (ADR-0006 amendment): the channel chip in the trace turn header. */
.trace-channel-chip {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  padding: 1px 8px;
  white-space: nowrap;
}

/* F1 (ADR-0006 amendment): the per-node wall-clock waterfall. */
.trace-timing {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-bottom: 10px;
}

.trace-timing-row {
  display: grid;
  grid-template-columns: 7.5em 1fr auto;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 11px;
}

.trace-timing-node {
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.trace-timing-bar-wrap {
  height: 8px;
  background: var(--color-border);
  border-radius: 4px;
  overflow: hidden;
}

.trace-timing-bar {
  height: 100%;
  min-width: 2px;
  background: var(--color-accent, var(--color-text-muted));
  border-radius: 4px;
}

.trace-timing-ms {
  color: var(--color-text-muted);
  white-space: nowrap;
  font-size: 10.5px;
}

.trace-stats {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 14px;
  font-family: var(--font-mono);
  font-size: 11.5px;
}

/* 22-E: the validated, PII-redacted input surfaced in the trace. */
.trace-input {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--color-text);
  background: var(--color-surface-subtle, rgba(127, 127, 127, 0.08));
  border-radius: 6px;
  padding: 6px 8px;
  margin-bottom: 10px;
  white-space: pre-wrap;
  word-break: break-word;
}

.trace-stats dt {
  /* Use --color-text-muted to clear WCAG 2 AA 4.5:1 on both themes. */
  color: var(--color-text-muted);
  text-transform: lowercase;
}

.trace-stats dd { color: var(--color-text); }

.trace-stats .faint {
  /* Use --color-text-muted to clear WCAG 2 AA 4.5:1 on both themes. */
  color: var(--color-text-muted);
}

.trace-decisions {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.trace-decision {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
}

/* FR-026 trace-row button reset. renders the newest
   turn's guardrail decision rows as native <button>s (Enter/Space, the
   role, the accessible name from the text, all free); cost-unit rows
   stay <div>s. A native <button> carries UA chrome - a button font, a
   centred text-align, an auto width - so the button-form rows are reset
   to render byte-for-byte like the <div> rows: the page font, full
   width, left-aligned, the inherited text colour, a pointer cursor.
   display:flex from the base rule already overrides the UA display. */
button.trace-decision {
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  margin: 0;
}
button.trace-decision:not([data-stale="true"]):hover {
  border-color: var(--color-node-highlight);
}

/* 3a: the per-turn all-as-paid cost breakdown {llm,rag,voice,compute} rendered
   under the trace cost section. The value is the prominent dollar figure; the
   breakdown is a muted monospace secondary row. Reuses the trace-decision
   surface so it reads as one with the rows above it (dark-first + light). */
.trace-cost-summary {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 7px 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
}
.trace-cost-value {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 600;
  color: var(--color-text);
}
.trace-cost-breakdown {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--color-text-muted);
}
/* SC 2.4.7 Focus Visible: :focus-visible (not :focus) so a mouse click
   leaves no ring and a keyboard Tab does, matching the SPA house pattern
   (a 2px accent outline, a 2px offset; the trace row is a normal HTML
   element so the house positive offset is safe here - unlike the
   foreignObject node button, which needs an inset ring). */
button.trace-decision:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}
/* A de-activated older turn's row (marks it data-stale and drops
   it to tabindex -1): it is no longer a Tab stop, so it gets the default
   cursor and no hover lift, signalling it is not keyboard-interactive.
   It stays DOM-present and screen-reader-readable. */
button.trace-decision[data-stale="true"] {
  cursor: default;
}

.trace-decision-name {
  flex: 0 0 130px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--color-text);
  /* Checkpoint-2 readability: wrap long names (e.g. model ids in the cost
     rows) within the column instead of clipping them to one line. */
  white-space: normal;
  overflow-wrap: anywhere;
}

.trace-decision-pill {
  flex-shrink: 0;
  font-family: var(--font-ui);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 3px;
}

.trace-decision-pill[data-status="ok"] {
  background: color-mix(in oklab, var(--color-ok) 16%, transparent);
  color: var(--color-ok);
}

.trace-decision-pill[data-status="refused"] {
  background: color-mix(in oklab, var(--color-refusal) 18%, transparent);
  color: var(--color-refusal);
}

.trace-decision-reason {
  flex: 1;
  min-width: 0;
  color: var(--color-text-muted);
  font-size: 11.5px;
  /* Checkpoint-2 readability: show the full decision reason (wrap) rather
     than clipping it to one line. */
  white-space: normal;
  overflow-wrap: anywhere;
}

.trace-citations ul {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.trace-citations li {
  background: var(--color-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: 4px;
  padding: 3px 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--color-text);
}

.trace-citations .none {
  font-style: italic;
  color: var(--color-text-faint);
}

.trace-error-box {
  background: var(--color-surface);
  border: 1px solid color-mix(in oklab, var(--color-error) 50%, var(--color-border));
  color: var(--color-error);
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-wrap: break-word;
}

/* ---------------- Footnote ---------------- */

.footnote {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 36px;
  padding: 0 24px;
  border-top: 1px solid var(--color-border);
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--color-text-muted);
  flex-shrink: 0;
  overflow-x: auto;
  white-space: nowrap;
}

.footnote a { color: var(--color-text); }
.footnote a:hover { color: var(--color-accent); }
.footnote .sep { color: var(--color-text-faint); }

/* Tablet: collapse the topbar controls into the overflow drawer so the row
   never overflows the viewport (the v1.2.0 "no scroll on tablet+" criterion).
   The body already stacks to a single column at this same 960px breakpoint, so
   the compact-topbar mode lines up with the compact-body mode. */
@media (max-width: 960px) {
  /* Phase 23 E: topbar overflow drawer */
  #topbar-overflow-btn { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; font-size: 18px; padding: 0; }
  #topbar-drawer {
    position: fixed; top: 56px; right: 0; width: 220px;
    background: var(--color-surface);
    border-left: 1px solid var(--color-border);
    border-bottom: 1px solid var(--color-border);
    border-radius: 0 0 0 var(--radius-md);
    box-shadow: var(--shadow-md);
    z-index: 100; padding: 8px 0;
    flex-direction: column;
  }
  #topbar-drawer:not(.hidden) { display: flex; }
  #topbar-drawer.hidden { display: none; }
  #topbar-drawer .control { width: 100%; height: 44px; padding: 0 16px; border-radius: 0; border: none; border-bottom: 1px solid var(--color-border-subtle); box-sizing: border-box; }
  /* Exclude locale-toggle, overflow-btn, and the drawer itself from hiding */
  .topbar-controls > *:not(#locale-toggle):not(#topbar-overflow-btn):not(#topbar-drawer) { display: none; }
  /* session-cost span inside drawer */
  #topbar-drawer #session-cost { display: flex; align-items: center; height: 44px; padding: 0 16px; border-bottom: 1px solid var(--color-border-subtle); }
  /* 3g drawer grouping (OQ-D): stack each group as a column with a tiny label;
     a stronger top border separates the three groups (Session / View / Data). */
  #topbar-drawer .drawer-group { display: flex; flex-direction: column; }
  #topbar-drawer .drawer-group + .drawer-group { border-top: 1px solid var(--color-border); }
  .drawer-group-label {
    display: block;
    padding: 8px 16px 4px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-text-faint);
  }
}

/* ---------------- Mobile ---------------- */

@media (max-width: 640px) {
  /* FR-038 / Truth 9 / GAP 3: The Playwright no-h-scroll probe measures
     document.documentElement (html), not body.  body already has overflow:
     hidden (app.css:175), but html had no horizontal-overflow constraint.
     At 390px, #regulatory-overlay-toggle and #voice-toggle inside
     .graph-header-controls overflow via overflow: visible on .graph-header,
     inflating html.scrollWidth > html.clientWidth.  Scoped to this 640px
     block only — desktop behavior (641-1280px) is unchanged (Plan 24-05). */
  html { overflow-x: hidden; }
  .topbar { padding: 0 14px; }
  .brand-tag { display: none; }
  .brand-name { font-size: 15px; }
  .topbar-controls .control { font-size: 12px; padding: 0 9px; }
  .messages { padding: 20px 16px 12px; } /* mirror: @container chat-pane <500px */
  .composer { padding: 12px 16px 16px; } /* mirror: @container chat-pane <500px */
  .empty-state { padding: 24px 20px; } /* mirror: @container chat-pane <500px */
  .empty-state h2 { font-size: 19px; } /* mirror: @container chat-pane <500px */
  .graph-header { padding: 0 16px; min-width: 0; } /* graph-region is a sibling of body-row, not a .trace-pane descendant — @media only; min-width: 0 prevents the flex-column item from expanding to its text min-content and causing horizontal scroll at 390px (D-08) */
  .trace-header { padding: 0 16px; } /* mirror: @container trace-pane <380px */
  .trace-body { padding: 16px 16px 24px; } /* mirror: @container trace-pane <380px */
  .footnote { padding: 0 16px; gap: 6px; } /* <footer> is after </main>, not a .trace-pane descendant — @media only */
  #send kbd { display: none; } /* mirror: @container chat-pane <500px */
  #send { padding: 0 14px; } /* mirror: @container chat-pane <500px */
  /* The canvas wrap and the SVG keep enough height for the node band
     and an open overlay; what tightens is the overlay's spare headroom
     and the sub-check row height (see M7-T05). The node rect height
     does not shrink; the canvas scrolls horizontally. 240px is the
     floor; never lower it. */
  :root { --graph-region-min-height: 240px; }
  /* Tighten the overlay so it fits the shorter region: a narrower min
     width and a shorter sub-check row. The node rect is untouched. */
  .subcheck-overlay { min-width: 200px; padding: 8px 10px; } /* inside .graph-region (sibling of body-row), not .trace-pane — @media only */
  .subcheck-row { padding: 5px 7px; } /* inside .graph-region (sibling of body-row), not .trace-pane — @media only */

  /* Rich-chip overflow guard (M7-T08, NFR-013). At 640px the 56px header
     is tight. The header must NOT wrap to two rows and the chip must NOT
     overflow it. The header text yields first (graph-header-text is
     shrinkable); the chip's metrics tail - the droppable part per the
     blueprint, "keep Answered, drop the node count" - is capped and
     clipped with an ellipsis, while the outcome word always stays whole.
     This shortens the chip rather than wrapping the header, with no
     media-query-driven JS rebranch. */
  .graph-header-text { min-width: 0; flex: 1 1 auto; } /* inside .graph-region (sibling of body-row), not .trace-pane — @media only */
  .graph-header-text h3,
  .graph-header-text .graph-sub {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap; /* inside .graph-region (sibling of body-row), not .trace-pane — @media only */
  }
  .graph-header-controls { min-width: 0; flex: 0 1 auto; } /* inside .graph-region (sibling of body-row), not .trace-pane — @media only */
  .graph-outcome-chip-rich { min-width: 0; flex-shrink: 1; } /* inside .graph-region (sibling of body-row), not .trace-pane — @media only */
  .graph-outcome-chip-tail {
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0; /* inside .graph-region (sibling of body-row), not .trace-pane — @media only */
  }
  .graph-outcome-chip-word { flex-shrink: 0; } /* inside .graph-region (sibling of body-row), not .trace-pane — @media only */

  /* 3g phone reorder (OQ-MOBILE-HERO): with the graph collapsed to a peek and
     the trace out of flow (bottom sheet, below), the body-row is a single
     column: a thin trace trigger bar (auto) above the chat (1fr). The chat
     therefore owns the body-row height (AC-P1). */
  .body-row {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(0, 1fr);
  }

  /* 3g sticky composer (OQ-MOBILE-HERO / premium #1): pin the composer to the
     bottom of the chat column; safe-area bottom padding is already on .composer. */
  .composer {
    position: sticky;
    bottom: 0;
    z-index: 2;
  }

  /* 3g trace trigger bar: full-width 44px tap target that opens the sheet. */
  .trace-sheet-trigger {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    height: 44px;
    padding: 0 16px;
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.02em;
    color: var(--color-text);
    background: var(--color-surface);
    border: none;
    border-bottom: 1px solid var(--color-border);
  }
  .trace-sheet-trigger:hover { background: var(--color-surface-hover); }
  .trace-sheet-trigger:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
  }

  /* 3g trace bottom-sheet (OQ-TR): the trace pane leaves the flow and becomes a
     fixed bottom sheet (z-index 1100, like the confirm dialog). Closed by
     default: translateY(100%) + visibility:hidden => 0 vertical cost AND out of
     the tab order / a11y tree. Opened by wireTraceSheet() which focus-traps it,
     scroll-locks the body, and sets inert on #graph-region + .chat-pane.
     The .trace-pane container-query (inline-size) keeps working as a full-width
     fixed element, so the 3a trace rendering is unaffected. */
  .trace-pane {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1100;
    height: min(85vh, 600px);
    border-top: 1px solid var(--color-border);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background: var(--color-surface);
    box-shadow: var(--shadow-md);
    transform: translateY(100%);
    visibility: hidden;
  }
  .trace-pane[data-sheet-open="true"] {
    transform: translateY(0);
    visibility: visible;
  }
  .trace-pane[data-sheet-open="true"] .trace-body {
    /* contain the scroll chain so a fling inside the sheet never scrolls the
       (already non-scrolling) shell behind it (premium #6). */
    overscroll-behavior: contain;
  }

  /* Sheet chrome (phone-only): a swipe-down drag handle (>=44px tall surface,
     JS owns the gesture via touch-action:none) and a 44px close button. */
  .trace-header {
    position: relative;
    height: auto;
    min-height: 56px;
    padding: 44px 16px 8px;
  }
  #trace-sheet-handle {
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 44px;
    cursor: grab;
    touch-action: none;
  }
  #trace-sheet-handle::after {
    content: "";
    position: absolute;
    top: 18px;
    left: 50%;
    transform: translateX(-50%);
    width: 40px;
    height: 4px;
    border-radius: 999px;
    background: var(--color-border);
  }
  #trace-sheet-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    order: 3;
    color: var(--color-text-muted);
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
  }
  #trace-sheet-close:hover { color: var(--color-text); }
  #trace-sheet-close:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
  }
}

/* Motion gate for the trace sheet: slide-in only when motion is allowed;
   reduced-motion shows/hides instantly (no transform tween). */
@media (max-width: 640px) and (prefers-reduced-motion: no-preference) {
  .trace-pane { transition: transform var(--transition-base); }
}

/* Phase 24: Per-pane container-query mirrors.
   Source order: @media first (above), @container after (here).
   Modern browsers: @container wins when both conditions are met (source order tiebreaker).
   Pre-Baseline browsers: only @media fires (graceful degradation, L-04).
   Thresholds: chat max-width: 500px, trace max-width: 380px (D-01/D-02; validated against live render D-03).
   Trace block intentionally contains ONLY .trace-header and .trace-body (genuine .trace-pane descendants).
   Graph-region and .footnote elements stay @media-only: .graph-region is a sibling of .body-row under <main>,
   and .footnote is a <footer> after </main> — neither is a .trace-pane descendant (Option B, NFR-019).
   No cqi/cqw/cqb/cqh units (PITFALLS A-3); no new transition: (PITFALLS A-7). */

/* mirror: @media (max-width:640px) chat-pane rules */
@container chat-pane (max-width: 500px) {
  .messages { padding: 20px 16px 12px; }
  .composer { padding: 12px 16px 16px; }
  .empty-state { padding: 24px 20px; }
  .empty-state h2 { font-size: 19px; }
  #send kbd { display: none; }
  #send { padding: 0 14px; }
}

/* mirror: @media (max-width:640px) trace-pane rules — only genuine .trace-pane descendants */
@container trace-pane (max-width: 380px) {
  .trace-header { padding: 0 16px; }
  .trace-body { padding: 16px 16px 24px; }
}

@media (max-width: 480px) {
  /* The long monorepo name wraps across three lines below 480 px. Hide
     it; the brand-mark + page <title> are sufficient identity at that
     scale. The "load example" dropdown also gets a shorter label
     because its option text is what conveys context. */
  .brand-name { display: none; }
  #example { max-width: 130px; }
}

/* ---------------- HITL review card (Phase C) ----------------
   Rendered in the assistant bubble when a turn pauses for human review.
   The card carries the flag reasons, the draft (rendered through the
   Markdown parser), and Approve / Edit / Reject controls. */

.review-card {
  border: 1px solid var(--color-refusal);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.review-card-header {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-refusal);
}

.review-card-note {
  margin: 0;
  font-size: 13px;
  color: var(--color-text-muted);
  line-height: 1.55;
}

.review-reasons {
  margin: 0;
  padding-left: 18px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.review-reasons li {
  font-size: 13px;
  color: var(--color-text);
}

.review-section-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-faint);
}

.review-draft {
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  padding: 10px 12px;
  font-size: 14px;
  line-height: 1.6;
}

.review-editor {
  width: 100%;
  box-sizing: border-box;
  font-family: var(--font-ui);
  font-size: 14px;
  line-height: 1.6;
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  resize: vertical;
}

.review-editor.hidden { display: none; }

.review-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.review-btn {
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 500;
  padding: 7px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface-hover);
  color: var(--color-text);
  cursor: pointer;
}

.review-btn:hover { filter: brightness(1.08); }
.review-btn:active { transform: translateY(1px); }

.review-btn-approve {
  background: linear-gradient(135deg, var(--color-accent), var(--color-accent-strong));
  border-color: var(--color-accent-strong);
  color: var(--color-accent-fg);
}

.review-btn-reject {
  border-color: var(--color-error);
  color: var(--color-error);
}

/* ===================================================================
   Voice UI ( — REQ-ext-b, REQ-ext-b6)
   =================================================================== */

:root {
  --color-voice-active: color-mix(in oklab, var(--color-accent) 22%, var(--color-surface));
  --color-mic-recording: #f87171;
}

/* Voice toggle button in graph-header-controls */

.voice-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.voice-toggle-btn:hover {
  background: var(--color-surface-hover);
  color: var(--color-text);
}

.voice-toggle-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.voice-toggle-btn[aria-pressed="true"] {
  background: var(--color-voice-active);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.voice-toggle-btn[aria-pressed="true"] .icon-mic-off {
  display: none;
}

.voice-toggle-btn[aria-pressed="true"] .icon-mic-on {
  display: inline-block;
}

.voice-toggle-btn[aria-pressed="false"] .icon-mic-off {
  display: inline-block;
}

.voice-toggle-btn[aria-pressed="false"] .icon-mic-on {
  display: none;
}

/* Regulatory overlay toggle button in graph-header-controls */

.regulatory-overlay-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.regulatory-overlay-btn:hover {
  background: var(--color-surface-hover);
  color: var(--color-text);
}

.regulatory-overlay-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.regulatory-overlay-btn[aria-pressed="true"] {
  background: var(--color-surface-hover);
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* Governance badge on mapped graph nodes. Hidden by default; visible when
   the regulatory overlay toggle is active. */

.gov-badge {
  visibility: hidden;
}

.regulatory-overlay-active .gov-badge {
  visibility: visible;
}

.regulatory-overlay-active {
  background: color-mix(in oklab, var(--color-accent) 4%, var(--color-surface));
}

#regulatory-overlay-toggle[aria-pressed="true"] {
  background: color-mix(in oklab, var(--color-accent) 14%, var(--color-surface));
  border-color: var(--color-accent);
}

/* Muted highlight border on mapped nodes when overlay is active. */
.regulatory-overlay-active .graph-node[data-node] rect {
  stroke: var(--color-accent);
  stroke-width: 2;
  stroke-opacity: 0.35;
}

/* Governance tooltip (lightweight HTML div, not the sub-check overlay). */

.gov-tooltip {
  display: none;
  position: absolute;
  z-index: 20;
  max-width: 260px;
  padding: 8px 10px;
  margin-top: -4px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  font-size: 12px;
  line-height: 1.4;
  color: var(--color-text);
}

.gov-tooltip.is-visible {
  display: block;
}

.gov-tooltip-title {
  font-weight: 600;
  margin-bottom: 4px;
}

.gov-tooltip-docs {
  list-style: none;
  padding: 0;
  margin: 0 0 4px 0;
}

.gov-tooltip-docs li {
  margin-bottom: 2px;
}

.gov-tooltip-docs a {
  color: var(--color-accent);
  text-decoration: underline;
}

.gov-tooltip-relevance {
  color: var(--color-text-muted);
  font-style: italic;
}

/* Microphone button in composer */

.mic-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  min-width: 44px;
  height: 54px;
  padding: 0 9px;
  gap: 5px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-muted);
  cursor: pointer;
  flex-shrink: 0;
  transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.mic-ptt-label {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: 1;
  color: inherit;
}

.mic-btn:hover {
  background: var(--color-surface-hover);
  color: var(--color-text);
}

.mic-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.mic-btn.recording {
  background: color-mix(in oklab, var(--color-mic-recording) 18%, var(--color-surface));
  border-color: var(--color-mic-recording);
  color: var(--color-mic-recording);
  animation: mic-pulse 1.2s ease-in-out infinite;
}

@keyframes mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--color-mic-recording) 30%, transparent); }
  50% { box-shadow: 0 0 0 6px color-mix(in oklab, var(--color-mic-recording) 10%, transparent); }
}

.mic-timer {
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 2px solid var(--color-mic-recording);
  pointer-events: none;
  /* The countdown ring is a conic-gradient that JS updates via CSS custom property. */
  background: conic-gradient(
    var(--color-mic-recording) calc(var(--mic-progress, 100) * 1%),
    transparent calc(var(--mic-progress, 100) * 1%)
  );
  mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
  opacity: 0;
  transition: opacity var(--transition-fast);
}

.mic-btn.recording .mic-timer {
  opacity: 1;
}

/* UX-03: "Listening…" aria-live pill shown while #mic-btn is held.
   Reuses pill geometry (3px 9px, mono 10-11px) with the recording-red border
   so it reads as live capture (status, not interactive accent). All color via
   var(--color-*); no raw hex. The pill is static (no transition) so it is
   inert under prefers-reduced-motion by construction. */
.voice-listening-region {
  display: inline-flex;
  align-items: center;
}

/* When the pill lives inside the chat box (during a voice hold, before the
   transcript renders) it reads as a left-aligned in-flow indicator, not a
   composer-adjacent chip that shifts the input row. */
#messages .voice-listening-region {
  display: flex;
  padding: 6px 2px;
}

.voice-listening-pill {
  display: inline-flex;
  align-items: center;
  padding: 3px 9px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-mic-recording);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

/* Voice message badge + replay control on a voice-originated user message.
   The badge reuses the .pill mono treatment; the replay button reuses the
   .copy-btn affordance with the :focus-visible accent ring. */
.voice-msg-affordances {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
}

.voice-input-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 9px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 400;
  line-height: 1;
  color: var(--color-text-muted);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

.voice-input-badge svg {
  flex-shrink: 0;
}

/* 3e-A: voice-input replay button -- an inline icon button in the affordances
   row. It no longer borrows .copy-btn (which pinned it absolute top-right at 40%
   opacity, overlapping the bubble text). */
.voice-replay-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background var(--transition-fast), color var(--transition-fast),
    border-color var(--transition-fast);
}

.voice-replay-btn:hover {
  background: var(--color-surface-hover);
  color: var(--color-text);
}

.voice-replay-btn.is-playing {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.voice-replay-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.voice-replay-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Response-audio on-demand play button (#5). The assistant TTS is no longer
   auto-played; this labeled control sits in the affordances row and fetches
   the one-time audio capability on first click. It deliberately does NOT
   borrow .copy-btn -- that rule pins copy buttons absolute in the bubble's
   top-right, where the assistant copy button already lives. */
.response-audio-affordances {
  margin-top: 8px;
}

.response-audio-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 24px;
  padding: 0 8px;
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: color var(--transition-fast), background var(--transition-fast);
}

.response-audio-btn:hover {
  color: var(--color-text);
  background: var(--color-surface);
}

.response-audio-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.response-audio-btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.response-audio-wave {
  flex-shrink: 0;
}

.response-audio-wave rect {
  transform-box: fill-box;
  transform-origin: center bottom;
}

.response-audio-btn.is-playing .response-audio-wave rect {
  animation: response-audio-bar 0.9s ease-in-out infinite;
}

.response-audio-btn.is-playing .response-audio-wave rect:nth-child(2) {
  animation-delay: 0.15s;
}

.response-audio-btn.is-playing .response-audio-wave rect:nth-child(3) {
  animation-delay: 0.3s;
}

.response-audio-btn.is-playing .response-audio-wave rect:nth-child(4) {
  animation-delay: 0.45s;
}

@keyframes response-audio-bar {
  0%, 100% { transform: scaleY(0.45); }
  50% { transform: scaleY(1); }
}

@media (prefers-reduced-motion: reduce) {
  .response-audio-btn.is-playing .response-audio-wave rect {
    animation: none;
  }
}

/* Voice disclosure modal */

.voice-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.voice-modal-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 28px 32px;
  max-width: 420px;
  width: 90%;
  box-shadow: var(--shadow-md);
}

.voice-modal-card h4 {
  margin: 0 0 12px;
  font-size: 16px;
  font-weight: 600;
  color: var(--color-text);
}

.voice-modal-card p {
  margin: 0 0 12px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--color-text-muted);
}

.voice-modal-retained {
  color: var(--color-accent) !important;
  font-size: 13px !important;
}

.voice-modal-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}

.voice-modal-confirm-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 36px;
  padding: 0 20px;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-accent-fg);
  background: var(--color-accent);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: filter var(--transition-fast);
}

.voice-modal-confirm-btn:hover {
  filter: brightness(1.08);
}

/* Footer voice notice */

.footer-voice-notice {
  font-size: 12px;
  /* Use --color-text-muted to clear WCAG 2 AA 4.5:1 on both themes. */
  color: var(--color-text-muted);
  font-weight: 500;
}

/* Voice trace row in the trace panel */

.voice-trace-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  font-size: 12px;
  color: var(--color-text-muted);
}

.voice-trace-row .voice-trace-label {
  font-weight: 500;
  color: var(--color-text);
}

.voice-trace-row .voice-trace-value {
  color: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: 11px;
}

.voice-trace-row .voice-trace-retained {
  color: var(--color-text-faint);
  font-size: 11px;
}

/* ===================================================================
    Plan 04: Data layer frontend surfaces
   Access flow, disclosure strip, transparency modal, cost display,
   session tracking, confirmation dialog
   =================================================================== */

/* ----- Access flow overlay ----- */

.access-flow {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
}

.access-screen {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  max-width: 480px;
  width: 90%;
  padding: 32px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

.access-screen h2 {
  font-size: 18px;
  font-weight: 600;
  color: var(--color-text);
  text-align: center;
}

.access-screen label {
  font-size: 13px;
  font-weight: 500;
  color: var(--color-text-muted);
  width: 100%;
}

/* Language selector buttons */
.access-lang-buttons {
  display: flex;
  gap: 12px;
  width: 100%;
  justify-content: center;
}

.access-lang-btn {
  min-width: 100px;
  min-height: 44px;
  font-size: 14px;
  font-weight: 500;
  text-align: center;
}

.access-lang-btn.active-lang {
  background: var(--color-accent);
  color: var(--color-accent-fg);
  border-color: var(--color-accent);
}

.access-continue-btn {
  min-height: 44px;
  padding: 0 24px;
  font-size: 14px;
  font-weight: 500;
}

/* Access decision cards */
.access-cards {
  display: flex;
  gap: 16px;
  width: 100%;
}

.access-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1;
  padding: 24px 16px;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: border-color var(--transition-fast), background var(--transition-fast);
  min-height: 44px;
}

.access-card:hover,
.access-card:focus-visible {
  border-color: var(--color-accent);
  background: var(--color-surface-hover);
}

.access-card:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.access-card strong {
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
}

.access-card span {
  font-size: 12px;
  color: var(--color-text-muted);
}

/* Key entry form */
.access-key-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
}

.access-input {
  width: 100%;
  padding: 10px 14px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--color-text);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  transition: border-color var(--transition-fast);
  min-height: 44px;
}

.access-input:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-accent) 18%, transparent);
}

.access-input::placeholder {
  color: var(--color-text-faint);
}

.access-activate-btn {
  min-height: 44px;
  padding: 0 24px;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-accent-fg);
  /* Use the dedicated interactive-button background (darker than --color-accent)
     so white --color-accent-fg text clears WCAG AA 4.5:1 in BOTH themes. The
     plain --color-accent (#8b8bff in dark) gave only 2.9:1 on this CTA. */
  background: var(--color-accent-btn);
  border: 1px solid var(--color-accent-btn);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: filter var(--transition-fast);
}

.access-activate-btn:hover {
  filter: brightness(1.08);
}

.access-decline-btn {
  min-height: 44px;
  padding: 0 24px;
  font-size: 14px;
  font-weight: 500;
  color: var(--color-text-muted);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.access-decline-btn:hover {
  background: var(--color-surface-hover);
}

.access-error {
  font-size: 13px;
  color: var(--color-error);
  min-height: 0;
}

.access-error:empty {
  display: none;
}

/* Key-activation modal: float its result/error message just BELOW the card,
   horizontally centered, so showing it never grows the card (no layout shift) --
   the user sees it as a result of their click, not a jump. Scoped to the key
   modal (a short card with room below); the taller request/NDA screens keep
   their in-flow errors to avoid pushing the message off-viewport. The
   .access-error:empty { display:none } above still hides it when there is no
   message. */
#access-screen-key {
  position: relative;
}

#access-screen-key #key-error {
  position: absolute;
  top: calc(100% + 12px);
  left: 0;
  right: 0;
  text-align: center;
}

/* ---- Phase 52 funnel surfaces (C1 teaser / C2 verify / C3 key-reveal / C8
   Turnstile). var(--color-*) only; no raw hex; no --color-focus (the 6-digit
   input reuses the .access-input color-mix focus ring). 44px touch targets. */

/* C1 value teaser: a compact static sample cited turn. */
.teaser-sample {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
}

.teaser-turn {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 14px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
}

.teaser-turn-agent {
  border-color: color-mix(in oklab, var(--color-accent) 22%, var(--color-border));
}

.teaser-role {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.teaser-text {
  font-size: 14px;
  line-height: 1.55;
  color: var(--color-text);
  margin: 0;
}

.teaser-cite {
  align-self: flex-start;
}

/* C2 verify: the 6-digit code input group. Mono, large, 44px, accent ring. */
.verify-instruction {
  font-size: 14px;
  line-height: 1.55;
  color: var(--color-text-muted);
  text-align: center;
  margin: 0;
}

.verify-code-group {
  display: flex;
  width: 100%;
  justify-content: center;
}

.verify-code-input {
  font-family: var(--font-mono);
  font-size: 20px;
  letter-spacing: 0.5em;
  text-align: center;
  min-height: 48px;
  max-width: 260px;
}

.verify-resend-btn {
  min-height: 44px;
  padding: 0 16px;
  font-size: 13px;
  font-weight: 500;
  color: var(--color-text-muted);
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.verify-resend-btn:hover:not(:disabled) {
  background: var(--color-surface-hover);
}

.verify-resend-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* C3 key reveal: verified tick, readonly mono key field + copy, pending line. */
.key-reveal-tick {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  color: var(--color-ok);
  border: 2px solid color-mix(in oklab, var(--color-ok) 45%, transparent);
}

.key-reveal-have-key {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
}

.key-reveal-field-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
}

.key-reveal-input {
  font-family: var(--font-mono);
  font-size: 13px;
  flex: 1 1 auto;
}

.key-reveal-copy-btn {
  flex: 0 0 auto;
  min-width: 44px;
  min-height: 44px;
}

.key-reveal-pending {
  font-size: 14px;
  line-height: 1.55;
  color: var(--color-text-muted);
  text-align: center;
  margin: 0;
}

/* C8 Turnstile container: spacing + surface tokens. The widget itself is
   third-party-rendered (exempt from the var(--color-*)-only rule); only its
   container uses our tokens. */
.turnstile-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 12px 0;
}

/* ACT-1: the Turnstile widget lives at the top of <body> (a sibling of the
   access-flow overlay), so during the gated flow the opaque .access-flow overlay
   (position:fixed; inset:0; z-index:900) paints OVER it -- an interactive
   challenge would be invisible + un-clickable exactly on the key-entry screen
   where activation needs the token. When on the key screen, lift the widget into
   a fixed, above-overlay slot (z-index > 900) so any interactive challenge is
   visible and solvable before Activate. Toggled by showAccessScreen(n===3); a
   no-op while the widget is unconfigured (the container stays display:none). */
.turnstile-container.turnstile-access-visible {
  position: fixed;
  left: 50%;
  bottom: 32px;
  transform: translateX(-50%);
  z-index: 950;
}

/* UI-2: after auth the widget must stay mounted (each /chat turn needs a fresh
   token) but it should NOT print at the top of <body> flow (which reads as "in
   the header"). Dock it to the graph-region bottom-center. Kept BODY-LEVEL and
   NOT re-parented into #graph-region -- that panel's inline-size containment
   makes it the containing block for position:fixed descendants and would break
   the ACT-1 key-screen lift. `left`/`top` are set each sync by
   _dockTurnstileToGraph() from #graph-region getBoundingClientRect (viewport
   coords, so position:fixed matches). Low z so an opened graph overlay layers
   over it; the anchor point is the graph's bottom edge and the translate lifts
   the box ~36px above it so it clears the text rendered at the graph's bottom
   edge. Compact padding so it reads as a small status chip. */
.turnstile-container.turnstile-chat-docked {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 1;
  margin: 0;
  padding: 0;
  transform: translate(-50%, calc(-100% - 36px));
  pointer-events: auto;
}

/* Request form */
.access-request-form {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
}

/* Notice-at-collection copy above the request submit (GOVR-05). */
.request-notice {
  margin: 0;
  font-size: 12px;
  line-height: 1.65;
  color: var(--color-text-muted);
}

/* Required privacy-acceptance gate row on the /request form. Mirrors the C2
   consent row geometry (>=24px hit area, 18px accent box) but is a single
   required control rather than a group. */
.request-privacy-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  min-height: 24px;
  font-size: 13px;
  line-height: 1.65;
  color: var(--color-text);
}

.request-privacy-checkbox {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin: 2px 0 0 0;
  accent-color: var(--color-accent);
  cursor: pointer;
}

.request-privacy-checkbox:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-accent) 18%, transparent);
}

/* Honeypot field (anti-spam) */
.hp-field {
  position: absolute;
  left: -9999px;
  opacity: 0;
  height: 0;
  width: 0;
  overflow: hidden;
  pointer-events: none;
}

/* CAPTCHA row */
.captcha-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.captcha-row span {
  font-size: 14px;
  color: var(--color-text);
  white-space: nowrap;
}

.captcha-field {
  width: 80px;
  min-width: 60px;
  text-align: center;
}

/* NDA consent area */
.nda-version-label {
  font-size: 12px;
  color: var(--color-text-muted);
  font-family: var(--font-mono);
}

.nda-content-area {
  max-height: 240px;
  overflow-y: auto;
  padding: 16px;
  background: var(--color-bg);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  font-size: 13px;
  line-height: 1.65;
  color: var(--color-text-muted);
  width: 100%;
}

.access-nda-actions {
  display: flex;
  gap: 12px;
  width: 100%;
}

.access-nda-actions .access-activate-btn {
  flex: 1;
}

.access-nda-actions .access-decline-btn {
  flex: 0 0 auto;
}

/* Disabled primary CTA: the consent gate keeps #nda-agree-btn disabled until
   terms load AND all required consent boxes are checked (C1 fail-closed). */
.access-activate-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.access-activate-btn:disabled:hover {
  filter: none;
}

/* In-button progress spinner (e.g. the Activate-key /chat probe). Uses
   currentColor so it inherits the button's foreground; the JS swap that adds it
   also sets disabled + aria-busy. Under reduced-motion it slows rather than
   stopping, so it still signals activity alongside the "Activating…" label. */
.btn-spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  margin-right: 8px;
  vertical-align: -2px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: btn-spin 0.6s linear infinite;
}

@keyframes btn-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn-spinner {
    animation-duration: 1.5s;
  }
}

/* C2: granular consent group (the only net-new component this phase, built
   entirely from existing tokens so it holds in both light and dark themes). */
.nda-consent-group {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 16px;
  background: var(--color-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}

.nda-consent-required {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.nda-consent-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  min-height: 24px;
  font-size: 13px;
  line-height: 1.65;
  color: var(--color-text);
  cursor: pointer;
}

.nda-consent-box {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin: 2px 0 0 0;
  accent-color: var(--color-accent);
  cursor: pointer;
}

.nda-consent-box:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-accent) 18%, transparent);
}

.nda-consent-note {
  margin: 4px 0 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--color-text-muted);
}

/* C2-MKT: the marketing opt-in is visually + semantically separated from the
   required group. No accent emphasis; the "optional" framing reads muted. */
.nda-consent-marketing {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--color-border-subtle);
}

.nda-consent-marketing .nda-consent-row {
  color: var(--color-text-muted);
}

/* ----- Disclosure strip ----- */

.disclosure-strip {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 32px;
  padding: 0 24px;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border-subtle);
  font-size: 11px;
  color: var(--color-text-muted);
  flex-shrink: 0;
}

/* 3b: keep the strip a single FIXED-height row (no growth -> no layout shift
   that would push a pane into an a11y-flagged scroll region). The long notice
   truncates to one line; its full text stays in the modal via "Learn more" and
   remains in the DOM. The links never break mid-phrase (white-space:nowrap). */
#disclosure-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.disclosure-strip a {
  color: var(--color-accent);
  font-size: 11px;
  white-space: nowrap;
  /* Underline ensures links are distinguishable by non-color cue
     (WCAG 2 AA link-in-text-block). */
  text-decoration: underline;
}

.disclosure-strip a:hover {
  color: var(--color-accent-strong);
}

.disclosure-strip a:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.disclosure-strip.opted-out #disclosure-text {
  display: none;
}

.disclosure-strip.opted-out #disclosure-opt-out {
  display: none;
}

.disclosure-strip.opted-out #disclosure-opted-out-text {
  display: inline;
}

#disclosure-opted-out-text {
  display: none;
  font-size: 12px;
  color: var(--color-text-muted);
}

/* ----- Always-on emergency safety banner (SAFE-02) -----
   Mirrors the .disclosure-strip flex layout but DROPS every opt-out artifact
   (it is non-dismissable and always-on). No fixed height: vertical padding +
   min-height let the safety copy wrap to two lines at 390px without clipping,
   and flex-wrap keeps it on-screen with no horizontal scroll. An amber/caution
   tone (#7c2d12 on #fef3c7 = 8.42:1, well above WCAG AA 4.5:1) carries the
   warning; the text itself conveys the meaning (no information by color alone).
   Explicit colors (not theme vars) keep the high-visibility tone identical in
   light and dark themes. z-index 100 sits above content, below the modals. */
.emergency-banner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  min-height: 40px;
  padding: 12px 24px;
  background: #fef3c7;
  color: #7c2d12;
  border-bottom: 1px solid #d97706;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
  z-index: 100;
  flex-shrink: 0;
}

/* ----- Privacy link in topbar ----- */

.privacy-link {
  font-size: 12px;
  font-weight: 500;
}

/* ----- Session cost in topbar ----- */

.session-cost {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.session-cost:empty {
  display: none;
}

/* 3g UI fix: the session cost moved into the emergency-banner row, pushed to the
   right edge. Inherit the banner's dark-on-amber color (#7c2d12) for contrast --
   the topbar muted gray would fail on the #fef3c7 banner. Still hidden while
   empty via .session-cost:empty above, so the banner is unchanged until a cost
   exists. */
.emergency-banner .session-cost {
  margin-left: auto;
  color: #7c2d12;
}

/* ----- C4.1 in-session TTL countdown (FNL-03) ----- */
/* Text-only: a label + the remaining-time number. The progress bar was removed
   (the "time left NNh NNm" text suffices). At <=2min the number turns amber
   (var(--color-refusal)). */
.session-ttl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--color-text-muted);
  white-space: nowrap;
}

.session-ttl-label {
  font-family: var(--font-ui);
  font-size: 11px;
  color: var(--color-text-muted);
}

.session-ttl-num {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--color-text);
}

/* ----- C4.2 manual terminate control (FNL-03) ----- */
/* Error-bordered to read destructive, matching #confirm-yes-btn. */
.session-end-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-error);
  border-color: color-mix(in oklab, var(--color-error) 50%, var(--color-border));
}

/* ----- C5 concurrency-queue modal (FNL-04) ----- */
.queue-modal-heading {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  color: var(--color-text);
}

.queue-modal-status {
  margin: 0;
  font-size: 13px;
  color: var(--color-text-muted);
}

.queue-modal-status:empty {
  display: none;
}

.queue-leave-btn {
  color: var(--color-error);
  border-color: color-mix(in oklab, var(--color-error) 50%, var(--color-border));
}

/* ----- Trace cost value ----- */

.trace-cost-value {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--color-accent);
}

/* ----- Transparency modal ----- */

.transparency-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.transparency-modal-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 28px 32px;
  max-width: 520px;
  width: 90%;
  max-height: 80vh;
  /* Clip to the rounded corners; the scroll lives on the inner panels area
     (below) so the scrollbar never crosses the card's radius. */
  overflow: hidden;
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.transparency-modal-header,
.transparency-modal-footer {
  flex-shrink: 0;
}

.transparency-modal-header {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.transparency-modal-header h4 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: var(--color-text);
}

/* SSE live indicator */
.sse-live-indicator {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--color-accent);
}

.sse-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-accent);
  animation: sse-dot-pulse 2s ease-in-out infinite;
}

@keyframes sse-dot-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}

/* Transparency panels */
.transparency-panels {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* The card clips to its rounded corners; this inner region carries the
     scroll, so the unified thin scrollbar sits inside the card edges and
     never breaks the radius. */
  overflow-y: auto;
  min-height: 0;
  flex: 1 1 auto;
  /* a little inset so the scrollbar doesn't hug the panel borders */
  padding-right: 4px;
}

.transparency-panel {
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.transparency-panel summary {
  padding: 12px 14px;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-text);
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  transition: background var(--transition-fast);
}

.transparency-panel summary::-webkit-details-marker {
  display: none;
}

.transparency-panel summary::after {
  content: "\25B6";
  font-size: 10px;
  color: var(--color-text-faint);
  transition: transform var(--transition-fast);
}

.transparency-panel[open] summary::after {
  transform: rotate(90deg);
}

.transparency-panel summary:hover {
  background: var(--color-surface-hover);
}

.transparency-panel summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

.transparency-panel p,
.transparency-panel .transparency-rights-actions {
  padding: 12px 14px;
  font-size: 13px;
  line-height: 1.65;
  color: var(--color-text-muted);
  margin: 0;
}

.transparency-rights-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.transparency-action-btn {
  min-height: 36px;
  font-size: 13px;
}

.transparency-action-destructive {
  border-color: color-mix(in oklab, var(--color-error) 50%, var(--color-border));
  color: var(--color-error);
}

/* C3 DSR result region: success-with-count, error, and the muted
   "nothing to download" state. The success/error color is set inline by
   app.js (var(--color-ok) / var(--color-error)); the default is muted. */
.dsr-status {
  margin-top: 8px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--color-text-muted);
}

.dsr-status:empty {
  display: none;
}

.transparency-modal-footer {
  display: flex;
  justify-content: flex-end;
}

.transparency-modal-footer .control {
  min-height: 36px;
  padding: 0 16px;
}

/* ----- Confirmation dialog ----- */

.confirm-dialog-overlay {
  position: fixed;
  inset: 0;
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

.confirm-dialog-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 24px 28px;
  max-width: 380px;
  width: 90%;
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.confirm-dialog-card p {
  font-size: 14px;
  line-height: 1.6;
  color: var(--color-text);
  margin: 0;
}

.confirm-dialog-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

.confirm-dialog-actions .control {
  min-height: 36px;
  padding: 0 16px;
  font-size: 13px;
}

#confirm-yes-btn {
  color: var(--color-error);
  border-color: color-mix(in oklab, var(--color-error) 50%, var(--color-border));
}

/* ----- Mobile adjustments for access flow ----- */

@media (max-width: 640px) {
  .access-screen {
    padding: 24px 20px;
  }

  .access-cards {
    flex-direction: column;
    gap: 12px;
  }

  /* 3g (OQ-G2): keep the disclosure a single truncated line on phone too
     (#disclosure-text already has flex:1 + text-overflow:ellipsis), dropping the
     prior flex-wrap / height:auto that let it grow to two rows. */
  .disclosure-strip {
    padding: 0 14px;
    gap: 8px;
    flex-wrap: nowrap;
    height: 32px;
    min-height: 0;
  }

  .transparency-modal-card {
    padding: 20px 18px;
    max-width: 100%;
    width: 95%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sse-dot {
    animation: none;
    opacity: 1;
  }
}

/* Access-disabled modal: blocking surface used when the server reports
 * mode=disabled (no DEMO_KEY env var and no Supabase). The user cannot
 * proceed; only the contact email is offered. The body-level
 * `.access-blocked` class hides scrollbars so the blur layer covers
 * everything underneath. */
.access-disabled-modal {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(8, 10, 18, 0.55);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  padding: 24px;
}
.access-disabled-modal.hidden {
  display: none;
}
.access-disabled-card {
  max-width: 480px;
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: 12px;
  padding: 28px 28px 24px 28px;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
  text-align: left;
  line-height: 1.55;
}
.access-disabled-card h2 {
  margin: 0 0 12px 0;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--color-text);
}
.access-disabled-card p {
  margin: 0 0 10px 0;
  color: var(--color-text-muted);
  font-size: 14px;
}
.access-disabled-card a {
  color: var(--color-accent, var(--color-text));
  text-decoration: underline;
  word-break: break-all;
}
body.access-blocked {
  overflow: hidden;
}

/* 3g trace bottom-sheet scroll-lock (OQ-TR): mirrors body.access-blocked so the
   background shell cannot scroll while the sheet is open. The shell already sets
   body{overflow:hidden}; this keeps the idiom explicit + symmetric with the
   sheet open/close lifecycle in wireTraceSheet(). */
body.trace-sheet-locked {
  overflow: hidden;
}

/* =====================================================================
   Phase 52 C6/C7: session-end screen + no-PII report (FNL-05/06)
   var(--color-*) only; the report numeric fields in --font-mono.
   ===================================================================== */

.session-end-flow {
  overflow-y: auto;
  padding: 24px 0;
}

.session-end-screen {
  max-width: 560px;
  gap: 20px;
}

.session-end-skip {
  align-self: flex-end;
  font-size: 12px;
}

.session-end-block {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  padding-top: 16px;
  border-top: 1px solid var(--color-border-subtle);
}

.session-end-block-heading {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: var(--color-text);
}

.cta-inquiry,
.nps-text {
  width: 100%;
  resize: vertical;
  font-family: var(--font-ui);
}

.cta-status:empty,
.nps-status:empty {
  display: none;
}

/* NPS 0-10 row: neutral .control chips, >=44px, wraps on mobile. The selected
   chip takes var(--color-accent) (the only accent in NPS). */
.nps-scale {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.nps-score-btn {
  min-width: 44px;
  min-height: 44px;
  font-family: var(--font-mono);
}

.nps-score-selected {
  background: var(--color-accent);
  color: var(--color-accent-fg);
  border-color: var(--color-accent);
}

/* The on-screen report card (built from live SPA state). */
.session-report {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 16px;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.session-report-title {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  color: var(--color-text);
}

.session-report-list {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px 16px;
}

.session-report-label {
  font-size: 13px;
  color: var(--color-text-muted);
}

.session-report-value {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--color-text);
  text-align: right;
}

.session-report-footer {
  margin: 0;
  font-size: 11px;
  color: var(--color-text-muted);
}

/* =====================================================================
   C7: @media print -- the NEW print path (absent before Phase 52). Hide
   everything but #session-report; force a light-on-white (toner-friendly)
   rendering; single-page A4/Letter-safe. window.print() is the only print
   mechanism (no PDF lib, $0).
   ===================================================================== */
@media print {
  /* Hide every top-level surface (chat, graph, trace, topbar, overlays). */
  body > * {
    display: none !important;
  }
  /* Re-show the session-end flow chrome only enough to reach the report. */
  #session-end-flow {
    display: block !important;
    position: static !important;
    background: #fff !important;
  }
  #session-end-flow .session-end-screen > *:not(.session-end-report) {
    display: none !important;
  }
  #session-end-flow .session-end-screen {
    display: block !important;
    box-shadow: none !important;
    border: none !important;
    background: #fff !important;
    max-width: 100% !important;
    width: 100% !important;
    padding: 0 !important;
  }
  .session-end-report,
  #session-report {
    display: block !important;
  }
  /* The download button is screen-only. */
  .report-download {
    display: none !important;
  }
  /* Force light-on-white for legibility + toner economy on paper. */
  #session-report {
    color: #000 !important;
    background: #fff !important;
    border: 1px solid #000 !important;
    border-radius: 0 !important;
  }
  #session-report .session-report-title,
  #session-report .session-report-label,
  #session-report .session-report-value,
  #session-report .session-report-footer {
    color: #000 !important;
  }
  #session-report .session-report-value {
    font-family: var(--font-mono);
  }
  @page {
    margin: 1.5cm;
  }
}
