/* ==========================================================================
   AI Document Assistant
   Built on the app's existing tokens (--brand, --surface-*, --line, --ink) so
   it reads as part of SewerSite in both light and dark.
   Desktop: right-side drawer. Mobile: near-fullscreen sheet.
   ========================================================================== */

.ai-topbar-button svg { width: 20px; height: 20px; }
.ai-answer-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; margin-top: 12px; }
.ai-answer-actions .ai-link { min-height: 40px; padding: 6px 4px; text-align: left; }
.ai-answer-actions [aria-pressed="true"] { color: var(--brand); text-decoration: underline; text-underline-offset: 4px; }
.ai-feedback-status { color: var(--muted); font-size: 12px; margin: 4px 0 0; }
.ai-feedback-status:empty { display: none; }
.ai-feedback-reason { width: 100%; margin-top: 8px; min-height: 44px; color: var(--ink); background: var(--surface-container-low); border: 1px solid var(--line); }
.ai-original { margin-top: 12px; }
.ai-original summary { cursor: pointer; min-height: 40px; padding: 8px 0; color: var(--muted); }
.ai-original .ai-quote { margin: 4px 0; white-space: pre-wrap; overflow-wrap: anywhere; }

/* Shared robot artwork for the launcher, assistant identity and empty state. */
.ai-chatbot-robot {
  display: block;
  overflow: hidden;
  border-radius: 50%;
  pointer-events: none;
  user-select: none;
  flex: none;
}
.ai-chatbot-robot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}
.ai-fab .ai-chatbot-robot { width: 48px; height: 48px; }
.ai-head-icon .ai-chatbot-robot { width: 30px; height: 30px; }
.ai-empty-icon .ai-chatbot-robot { width: 48px; height: 48px; }

/* The sheet is stretched between the viewport edges rather than given a height.
   Viewport units are the wrong tool here: vh ignores the URL bar and dvh ignores
   the keyboard, and both leave the sheet taller than what is actually visible. */
.ai-overlay {
  position: fixed; z-index: 120;
  /* inset:0 — NOT a height.
     Measured on an iPhone 13 (iOS 18.7): with the keyboard open the layout
     viewport correctly shrank from 797 to 394, but the sheet stayed 797 tall
     and its header was driven to -403. The cause was `height: 100dvh`: the
     dynamic viewport unit is defined against the retracted browser UI and
     deliberately ignores the keyboard, so it stayed 797. A sheet taller than
     the viewport puts the composer below the fold, and iOS then scrolls to
     reveal it and takes the header with it.
     Stretching between top and bottom instead makes the sheet track the layout
     viewport exactly, so nothing needs to scroll and the header cannot move.
     No height is declared at all, because any viewport unit reintroduces the
     same bug. */
  inset: 0;
  display: flex; justify-content: flex-end;
  overflow: hidden;
  overscroll-behavior: none;
  /* Nothing in the panel is draggable. Without this, a touch that starts on the
     composer or the header drags the whole sheet around, because the browser
     treats it as a pan on the nearest scrollable ancestor. Scrollable children
     opt back in individually below. */
  touch-action: none;
}
.ai-overlay[hidden] { display: none; }
.ai-scrim { position: absolute; inset: 0; background: rgba(6, 12, 18, .46); backdrop-filter: blur(2px); animation: aiFade .18s ease both; }

/* Scroll lock. overflow:hidden alone is ignored by iOS Safari and by the
   WKWebView shell, which is why the page kept scrolling behind the open panel.
   position:fixed does hold it; ai-ui.js preserves and restores the scroll
   offset so closing the panel does not jump the user back to the top. */
body.ai-open { overflow: hidden; }
/* position:fixed is REQUIRED here: it pins the page so the opaque overlay keeps
   covering the whole screen. Without it iOS scrolls the document, the fixed
   overlay is carried up, and the app bleeds through at the bottom. The keyboard
   is kept off the header by pre-lifting the composer on focus (ai-ui.js), which
   stops iOS needing to scroll the viewport in the first place. */
body.ai-scroll-locked {
  position: fixed;
  left: 0; right: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}
/* The page behind must not be reachable by touch at all while the sheet is
   open. Only the message list re-enables panning. */
body.ai-scroll-locked > *:not(.ai-overlay) { touch-action: none; }

.ai-panel {
  position: relative; z-index: 1;
  display: flex; flex-direction: column;
  width: min(460px, 100%); height: 100%;
  background: var(--bg);
  border-left: 1px solid var(--line);
  box-shadow: -24px 0 60px -30px rgba(0, 0, 0, .55);
  animation: aiSlideIn .26s cubic-bezier(.16, 1, .3, 1) both;
}
@keyframes aiFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes aiSlideIn { from { transform: translateX(24px); opacity: .4; } to { transform: translateX(0); opacity: 1; } }

.ai-head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--line); background: var(--surface-container-lowest); }
.ai-head-id { display: flex; align-items: center; gap: 11px; min-width: 0; }
.ai-head-icon { display: grid; place-items: center; width: 36px; height: 36px; border-radius: 50%; background: transparent; color: #fff; flex: none; }
body.dark .ai-head-icon { color: #08211a; }
.ai-head-icon svg { width: 19px; height: 19px; }
.ai-head-id strong { display: block; color: var(--ink); font-size: 15px; letter-spacing: -.01em; }
.ai-head-id small { display: block; color: var(--muted); font-size: 11.5px; }
.ai-head-actions { display: flex; align-items: center; gap: 6px; flex: none; }
.ai-ghost { background: transparent; border: 1px solid var(--line); border-radius: 10px; padding: 7px 10px; color: var(--muted); font-size: 12px; font-weight: 600; cursor: pointer; }
.ai-ghost:hover { color: var(--ink); }
.ai-icon-btn { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid var(--line); border-radius: 10px; background: transparent; color: var(--ink); cursor: pointer; }
.ai-icon-btn svg { width: 17px; height: 17px; }

.ai-tabs { flex: none; display: flex; gap: 4px; padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--surface-container-lowest); }
.ai-tab { flex: 1; min-height: 36px; border: 0; border-radius: 10px; background: transparent; color: var(--muted); font-size: 12.5px; font-weight: 700; cursor: pointer; }
.ai-tab.active { background: var(--surface-container); color: var(--ink); }

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

/* ------------------------------------------------------------------ chat */
.ai-chat { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.ai-filter { display: flex; align-items: center; gap: 8px; padding: 10px 14px 0; }
.ai-filter label { color: var(--muted); font-size: 11.5px; font-weight: 700; }
.ai-filter select { flex: 1; min-height: 34px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-container-lowest); color: var(--ink); font-size: 12.5px; padding: 0 8px; }

/* overscroll-behavior: contain is the fix for the "two or three scrollbars"
   feeling — without it, flicking past the last message hands the gesture to the
   page underneath and the background starts moving instead. */
.ai-messages {
  flex: 1; min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
  padding: 14px;
  display: flex; flex-direction: column; gap: 12px;
}
.ai-msg { display: flex; }
.ai-msg-user { justify-content: flex-end; }
.ai-bubble { max-width: 88%; padding: 11px 13px; border-radius: 16px; font-size: 14px; line-height: 1.5; word-wrap: break-word; overflow-wrap: anywhere; }
.ai-msg-user .ai-bubble { background: var(--brand); color: #fff; border-bottom-right-radius: 5px; }
body.dark .ai-msg-user .ai-bubble { color: #08211a; }
.ai-msg-assistant .ai-bubble { background: var(--surface-container-lowest); border: 1px solid var(--line); color: var(--ink); border-bottom-left-radius: 5px; }
.ai-answer-warn { border-color: color-mix(in srgb, var(--amber) 45%, var(--line)) !important; }
.ai-answer-conflict { border-color: color-mix(in srgb, var(--red) 40%, var(--line)) !important; }
.ai-answer-text strong { font-weight: 800; }

/* The tonal fill already separates the document's own words from the
   assistant's phrasing, so no accent bar is needed to say "this is a quote". */
.ai-quote { margin: 10px 0 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-container); color: var(--ink); font-size: 13.5px; line-height: 1.55; white-space: pre-wrap; }

.ai-sources { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 11px; padding-top: 10px; border-top: 1px dashed var(--line); }
.ai-sources-label { color: var(--muted); font-size: 10.5px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }
.ai-source { border: 1px solid var(--line); border-radius: 999px; padding: 5px 10px; background: var(--surface-container); color: var(--brand-2); font-size: 11.5px; font-weight: 700; cursor: pointer; }
.ai-source:hover { border-color: var(--brand-2); }

.ai-typing { display: inline-flex; gap: 4px; }
/* A smooth opacity pulse rather than a hopping dot: it still reads as "thinking"
   without the dated bounce, and it stays calm on a phone held at arm's length. */
.ai-typing i { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); animation: aiPulse 1.2s infinite cubic-bezier(.16, 1, .3, 1); }
.ai-typing i:nth-child(2) { animation-delay: .16s; }
.ai-typing i:nth-child(3) { animation-delay: .32s; }
@keyframes aiPulse { 0%, 70%, 100% { opacity: .28; } 35% { opacity: 1; } }

/* The thinking line. Naming the current step ("reading the documents") is what
   makes a pause feel like work rather than a hang — the dots alone say only
   "wait". Each stage crossfades in so the label never snaps. */
.ai-think { display: flex; align-items: center; gap: 9px; }
.ai-think-label {
  color: var(--muted);
  font-size: 12.5px;
  line-height: 1.3;
  animation: aiThinkIn .28s ease both;
}
@keyframes aiThinkIn { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }

.ai-empty { text-align: center; padding: 26px 16px; }
.ai-empty .ai-disclaimer { margin: 10px auto 0; max-width: 34ch; font-size: 12px; line-height: 1.4; opacity: .8; }
/* No plate behind the robot. The artwork is already a finished circular badge,
   so the 52px rounded square this used to paint read as a second, mismatched
   button sitting behind it — clearly in dark mode, where the fill is lighter
   than the page. */
.ai-empty-icon { display: grid; place-items: center; width: 52px; height: 52px; margin: 0 auto 12px; border-radius: 50%; background: transparent; color: var(--brand-2); }
.ai-empty-icon svg { width: 26px; height: 26px; }
.ai-empty h3 { margin: 0 0 6px; color: var(--ink); font-size: 17px; letter-spacing: -.02em; }
.ai-empty p { margin: 0 auto 16px; max-width: 34ch; color: var(--muted); font-size: 13px; line-height: 1.5; }
.ai-chips { display: grid; gap: 7px; }
.ai-chip { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: var(--surface-container-lowest); color: var(--ink); font-size: 12.5px; text-align: left; cursor: pointer; }
.ai-chip:hover { border-color: var(--brand-2); }

.ai-composer { flex: none; display: flex; align-items: flex-end; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--line); background: var(--surface-container-lowest); }
/* The composer must not scroll. Panning inside it dragged the sheet and the
   message list with it, so the box grows to fit instead (ai-ui.js sizes it)
   and only contains its own overscroll once it reaches the cap. */
.ai-composer textarea { touch-action: pan-y; overscroll-behavior: contain; overflow-y: hidden; flex: 1; min-height: 42px; max-height: 120px; resize: none; border: 1px solid var(--line); border-radius: 10px; padding: 11px 12px; background: var(--bg); color: var(--ink); font: inherit; font-size: 14px; line-height: 1.4; }
.ai-send { display: grid; place-items: center; width: 42px; height: 42px; flex: none; border: 0; border-radius: 10px; background: var(--brand); color: #fff; cursor: pointer; transition: transform .16s ease; }
body.dark .ai-send { color: #08211a; }
.ai-send:active { transform: scale(.94); }
.ai-send svg { width: 18px; height: 18px; }
.ai-privacy { flex: none; margin: 0; padding: 0 14px 12px; color: var(--muted); font-size: 10.5px; line-height: 1.45; background: var(--surface-container-lowest); }

/* ---------------------------------------------------------- search + KB */
.ai-search-tab, .ai-kb {
  flex: 1; min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
  padding: 14px;
}
.ai-search-form { display: flex; gap: 8px; margin-bottom: 12px; }
.ai-search-form input { flex: 1; min-height: 42px; border: 1px solid var(--line); border-radius: 10px; padding: 0 12px; background: var(--surface-container-lowest); color: var(--ink); font-size: 14px; }
.ai-btn { min-height: 42px; padding: 0 16px; border: 1px solid transparent; border-radius: 10px; background: var(--brand); color: #fff; font-size: 13.5px; font-weight: 700; cursor: pointer; }
body.dark .ai-btn:not(.secondary) { color: #08211a; }
.ai-btn.secondary { background: var(--surface-container); color: var(--ink); border-color: var(--line); }
.ai-link { border: 0; background: transparent; color: var(--brand-2); font-size: 12.5px; font-weight: 700; cursor: pointer; padding: 4px 0; }
.ai-link.danger { color: var(--red); }

/* Interaction: the assistant shares the app's press language so it feels native,
   not like a widget bolted on. .ai-send already presses; these add the rest. */
.ai-btn, .ai-tab, .ai-chip, .ai-source, .ai-history-item { transition: transform var(--dur-instant, 90ms) var(--ease-out), border-color var(--dur-fast, 140ms) var(--ease-out); touch-action: manipulation; }
.ai-btn:active, .ai-chip:active, .ai-tab:active, .ai-source:active, .ai-history-item:active { transform: scale(var(--press, .97)); }
.ai-icon-btn { transition: transform var(--dur-instant, 90ms) var(--ease-out), background var(--dur-fast, 140ms) var(--ease-out); touch-action: manipulation; }
.ai-icon-btn:active { transform: scale(var(--press-strong, .94)); }
@media (prefers-reduced-motion: reduce) {
  .ai-btn:active, .ai-chip:active, .ai-tab:active, .ai-source:active, .ai-history-item:active, .ai-icon-btn:active { transform: none; }
}

.ai-result { padding: 12px; border: 1px solid var(--line); border-radius: 16px; background: var(--surface-container-lowest); margin-bottom: 9px; }
.ai-result-head { color: var(--brand-2); font-size: 11.5px; font-weight: 800; margin-bottom: 5px; }
.ai-result p { margin: 0 0 6px; color: var(--ink); font-size: 13px; line-height: 1.5; }

.ai-kb-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(88px, 1fr)); gap: 8px; margin-bottom: 14px; }
.ai-kb-card { padding: 12px; border: 1px solid var(--line); border-radius: 16px; background: var(--surface-container-lowest); text-align: center; }
.ai-kb-card strong { display: block; color: var(--ink); font-size: 21px; letter-spacing: -.03em; }
.ai-kb-card span { color: var(--muted); font-size: 10.5px; font-weight: 600; }
.ai-kb-actions { display: flex; gap: 8px; margin-bottom: 14px; }
.ai-kb-actions .ai-btn { flex: 1; }
.ai-kb-empty { color: var(--muted); font-size: 13px; line-height: 1.5; text-align: center; padding: 20px; border: 1px dashed var(--line); border-radius: 16px; }

.ai-doc { display: grid; grid-template-columns: 1fr auto; gap: 6px 10px; padding: 12px; border: 1px solid var(--line); border-radius: 16px; background: var(--surface-container-lowest); margin-bottom: 9px; }
.ai-doc.is-disabled { opacity: .58; }
.ai-doc-main strong { display: block; color: var(--ink); font-size: 14px; letter-spacing: -.01em; }
.ai-doc-main small { display: block; color: var(--muted); font-size: 11.5px; margin-top: 2px; }
.ai-doc-note { margin: 6px 0 0; color: color-mix(in srgb, var(--amber) 62%, var(--ink)); font-size: 11.5px; line-height: 1.4; }
.ai-doc-status { align-self: start; padding: 4px 8px; border-radius: 999px; font-size: 10px; font-weight: 800; letter-spacing: .04em; }
.ai-doc-status.ok { background: color-mix(in srgb, var(--green) 20%, transparent); color: var(--green); }
.ai-doc-status.warn { background: color-mix(in srgb, var(--amber) 22%, transparent); color: color-mix(in srgb, var(--amber) 70%, var(--ink)); }
.ai-doc-status.bad { background: color-mix(in srgb, var(--red) 18%, transparent); color: var(--red); }
.ai-doc-status.busy, .ai-doc-status.muted { background: var(--surface-container); color: var(--muted); }
.ai-doc-actions { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 12px; padding-top: 8px; border-top: 1px dashed var(--line); }

/* ---------------------------------------------------------- source drawer */
.ai-source-drawer { position: fixed; inset: 0; z-index: 140; display: flex; align-items: flex-end; justify-content: center; }
.ai-source-sheet { position: relative; z-index: 1; width: min(560px, 100%); max-height: 78vh; display: flex; flex-direction: column; background: var(--bg); border: 1px solid var(--line); border-radius: 16px 16px 0 0; animation: aiSheetUp .26s cubic-bezier(.16,1,.3,1) both; }
@keyframes aiSheetUp { from { transform: translateY(26px); opacity: .5; } to { transform: translateY(0); opacity: 1; } }
.ai-source-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 16px; border-bottom: 1px solid var(--line); }
.ai-source-head strong { display: block; color: var(--ink); font-size: 15px; }
.ai-source-head small { display: block; color: var(--muted); font-size: 11.5px; margin-top: 2px; }
.ai-source-body { padding: 16px; overflow-y: auto; }
.ai-source-passage { margin: 0 0 14px; color: var(--ink); font-size: 14px; line-height: 1.6; white-space: pre-wrap; }
.ai-source-actions { display: flex; gap: 8px; }

/* --------------------------------------------------------------- dialogs */
.ai-dialog-back { position: fixed; inset: 0; z-index: 160; display: grid; place-items: center; padding: 16px; background: rgba(6, 12, 18, .5); }
.ai-dialog { width: min(480px, 100%); max-height: 86vh; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; padding: 18px; border-radius: 16px; background: var(--bg); border: 1px solid var(--line); }
.ai-dialog h3 { margin: 0; color: var(--ink); font-size: 17px; letter-spacing: -.02em; }
.ai-dialog-body { display: grid; gap: 11px; }
.ai-field { display: grid; gap: 5px; }
.ai-field span { color: var(--muted); font-size: 12px; font-weight: 600; }
.ai-field input, .ai-field select, .ai-field textarea { width: 100%; min-height: 42px; border: 1px solid var(--line); border-radius: 10px; padding: 9px 11px; background: var(--surface-container-lowest); color: var(--ink); font: inherit; font-size: 14px; }
.ai-field textarea { min-height: 110px; resize: vertical; }
.ai-dialog-foot { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
.ai-dialog-foot .ai-btn { width: 100%; }
.ai-dialog-error { margin: 0; padding: 10px 12px; border-radius: 10px; background: color-mix(in srgb, var(--red) 12%, transparent); color: var(--red); font-size: 12.5px; }
.ai-progress { margin: 0; color: var(--brand-2); font-size: 12px; font-weight: 600; min-height: 16px; }
.ai-preview-summary { margin: 0; color: var(--muted); font-size: 12px; }
.ai-preview-headings { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.ai-preview-headings strong { width: 100%; color: var(--ink); font-size: 12px; }
.ai-preview-headings span { padding: 4px 8px; border-radius: 999px; background: var(--surface-container); color: var(--muted); font-size: 11px; }
.ai-preview-text { max-height: 40vh; overflow: auto; margin: 0; padding: 12px; border-radius: 10px; background: var(--surface-container); color: var(--ink); font-size: 12px; line-height: 1.5; white-space: pre-wrap; }

.ai-debug { margin-top: 10px; }
.ai-debug summary { color: var(--muted); font-size: 11px; cursor: pointer; }
.ai-debug pre { max-height: 220px; overflow: auto; margin: 6px 0 0; padding: 8px; border-radius: 8px; background: var(--surface-container); font-size: 10.5px; }

/* ---------------------------------------------------------------
   The assistant's mark: a sewer network reaching a central manhole.
   pipes connect -> junctions light -> flow arrives -> the cover locks and
   wakes -> haloes -> reset, over 4.8s.
   --------------------------------------------------------------- */
.sewer-chatbot-icon {
  --sewer-orange: #e85d20;
  /* The cover, the junction bodies and the pipe inner detail are filled or
     knocked-out shapes, so they have to be the surface they sit on. Painted
     white, as the brief has them, they punch white holes in a dark tile. */
  --chatbot-icon-background: var(--surface-container-lowest, #fff);
  position: relative;
  display: grid;
  place-items: center;
  pointer-events: none;   /* every tap has to reach the control underneath */
  user-select: none;
}
.sewer-chatbot-icon svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* Each host sizes it, and each size was chosen by rendering the mark in it.
   The illustration carries six branches and three junctions, so it is given
   every pixel its container allows rather than a comfortable inset. */
.ai-fab .sewer-chatbot-icon { width: 46px; height: 46px; }
.ai-empty-icon .sewer-chatbot-icon {
  width: 48px; height: 48px;
  --chatbot-icon-background: var(--surface-container);
}
.ai-head-icon .sewer-chatbot-icon {
  width: 30px; height: 30px;
  /* The identity chip is white on brand navy and keeps that language rather
     than importing the button's orange onto a navy tile. */
  --sewer-orange: currentColor;
  --chatbot-icon-background: var(--brand);
}

/* The resting values of .pipe and .core-dot are their HIDDEN ones — the pipes
   start at a full dash offset, and the animation is what reveals them. A cut
   that never animates has to be given the revealed state, or it renders as an
   empty ring. */
.sewer-chatbot-icon.is-static .pipe { stroke-dashoffset: 0; }
.sewer-chatbot-icon.is-static .core-dot { opacity: 1; }

.sewer-chatbot-icon .ghost {
  fill: none; stroke: color-mix(in srgb, var(--sewer-orange) 15%, transparent);
  stroke-width: 12; stroke-linecap: round; stroke-linejoin: round;
}
.sewer-chatbot-icon .pipe {
  fill: none; stroke: var(--sewer-orange); stroke-width: 7;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 120; stroke-dashoffset: 120;
}
.sewer-chatbot-icon .inner {
  fill: none; stroke: var(--chatbot-icon-background); stroke-width: 2.3;
  stroke-linecap: round; opacity: .8;
}
.sewer-chatbot-icon .junction {
  fill: var(--chatbot-icon-background); stroke: var(--sewer-orange); stroke-width: 3.2;
}
.sewer-chatbot-icon .junction-core { fill: var(--sewer-orange); opacity: .22; }
.sewer-chatbot-icon .flow { fill: var(--sewer-orange); opacity: 0; }

.sewer-chatbot-icon .mh-wrap { transform-box: fill-box; transform-origin: center; }
.sewer-chatbot-icon .mh-outer {
  fill: var(--chatbot-icon-background); stroke: var(--sewer-orange); stroke-width: 5;
}
.sewer-chatbot-icon .mh-mid { fill: none; stroke: var(--sewer-orange); stroke-width: 2.5; opacity: .38; }
.sewer-chatbot-icon .mh-grid {
  fill: none; stroke: var(--sewer-orange); stroke-width: 1.8; opacity: .22;
}
.sewer-chatbot-icon .mh-lock {
  fill: none; stroke: var(--sewer-orange); stroke-width: 3; stroke-linecap: round;
  stroke-dasharray: 10 9; opacity: .38;
  transform-box: fill-box; transform-origin: center;
}
.sewer-chatbot-icon .tick {
  fill: none; stroke: var(--sewer-orange); stroke-width: 2.5; stroke-linecap: round; opacity: .25;
}
.sewer-chatbot-icon .halo {
  fill: none; stroke: var(--sewer-orange); stroke-width: 2.3; opacity: 0;
  transform-box: fill-box; transform-origin: center;
}
.sewer-chatbot-icon .signal {
  fill: none; stroke: var(--sewer-orange); stroke-width: 2.2; stroke-linecap: round; opacity: 0;
}

/* The sequence hangs off .active, so it can be tied to a real generating state
   later without touching the markup. It ships on: body.ai-page hides the
   button while the assistant is open, so an animation gated on "generating"
   would never be on screen to be seen. */
.sewer-chatbot-icon.active .pa { animation: sewerDrawPipe 4.8s ease-in-out infinite 0s; }
.sewer-chatbot-icon.active .pb { animation: sewerDrawPipe 4.8s ease-in-out infinite .16s; }
.sewer-chatbot-icon.active .pc { animation: sewerDrawPipe 4.8s ease-in-out infinite .32s; }
.sewer-chatbot-icon.active .pd { animation: sewerDrawPipe 4.8s ease-in-out infinite .48s; }
.sewer-chatbot-icon.active .pe { animation: sewerDrawPipe 4.8s ease-in-out infinite .64s; }
.sewer-chatbot-icon.active .pf { animation: sewerDrawPipe 4.8s ease-in-out infinite .8s; }
@keyframes sewerDrawPipe {
  0%, 7%, 100% { stroke-dashoffset: 120; }
  24%, 80%     { stroke-dashoffset: 0; }
}

/* transform-box:fill-box gives each junction its own centre, so they scale in
   place instead of swinging around the middle of the icon. */
.sewer-chatbot-icon .j1,
.sewer-chatbot-icon .j2,
.sewer-chatbot-icon .j3 { transform-box: fill-box; transform-origin: center; }
.sewer-chatbot-icon.active .j1 { animation: sewerJunctionPop 4.8s ease-in-out infinite .65s; }
.sewer-chatbot-icon.active .j2 { animation: sewerJunctionPop 4.8s ease-in-out infinite .82s; }
.sewer-chatbot-icon.active .j3 { animation: sewerJunctionPop 4.8s ease-in-out infinite .99s; }
@keyframes sewerJunctionPop {
  0%, 18%, 100% { transform: scale(.6); opacity: .3; }
  30%, 74%      { transform: scale(1);  opacity: 1; }
}

/* Each particle follows its own branch, so each needs its own path. */
.sewer-chatbot-icon.active .f1 { animation: sewerFlowA 4.8s ease-in-out infinite 1s; }
.sewer-chatbot-icon.active .f2 { animation: sewerFlowB 4.8s ease-in-out infinite 1.15s; }
.sewer-chatbot-icon.active .f3 { animation: sewerFlowC 4.8s ease-in-out infinite 1.3s; }
.sewer-chatbot-icon.active .f4 { animation: sewerFlowD 4.8s ease-in-out infinite 1.45s; }
@keyframes sewerFlowA {
  0%, 28%, 100% { opacity: 0; transform: translate(0, 0); }
  36% { opacity: 1; }
  60% { opacity: 1; transform: translate(47px, 27px); }
  68% { opacity: 0; transform: translate(51px, 30px); }
}
@keyframes sewerFlowB {
  0%, 31%, 100% { opacity: 0; transform: translate(0, 0); }
  39% { opacity: 1; }
  63% { opacity: 1; transform: translate(-43px, 30px); }
  71% { opacity: 0; transform: translate(-48px, 33px); }
}
@keyframes sewerFlowC {
  0%, 34%, 100% { opacity: 0; transform: translate(0, 0); }
  42% { opacity: 1; }
  66% { opacity: 1; transform: translate(0, 47px); }
  74% { opacity: 0; transform: translate(0, 51px); }
}
@keyframes sewerFlowD {
  0%, 37%, 100% { opacity: 0; transform: translate(0, 0); }
  45% { opacity: 1; }
  69% { opacity: 1; transform: translate(-2px, -46px); }
  77% { opacity: 0; transform: translate(-2px, -51px); }
}

.sewer-chatbot-icon.active .mh-wrap { animation: sewerCoreLock 4.8s ease-in-out infinite; }
.sewer-chatbot-icon.active .mh-lock { animation: sewerLockSpin 4.8s cubic-bezier(.45, 0, .2, 1) infinite; }
.sewer-chatbot-icon.active .h1 { animation: sewerHaloOne 4.8s ease-out infinite; }
.sewer-chatbot-icon.active .h2 { animation: sewerHaloTwo 4.8s ease-out infinite; }
.sewer-chatbot-icon.active .signal { animation: sewerSignalSweep 4.8s ease-in-out infinite; }
@keyframes sewerCoreLock {
  0%, 48%, 100% { transform: scale(1); }
  59% { transform: scale(1.07); }
  68% { transform: scale(.99); }
  76% { transform: scale(1.03); }
  84% { transform: scale(1); }
}
@keyframes sewerLockSpin {
  0%, 48% { transform: rotate(0deg); opacity: .25; }
  62%     { opacity: .8; }
  82%     { transform: rotate(90deg); opacity: .55; }
  100%    { transform: rotate(90deg); opacity: .25; }
}
@keyframes sewerHaloOne {
  0%, 54%, 100% { opacity: 0; transform: scale(.75); }
  63% { opacity: .72; }
  82% { opacity: 0; transform: scale(1.55); }
}
@keyframes sewerHaloTwo {
  0%, 61%, 100% { opacity: 0; transform: scale(.8); }
  70% { opacity: .4; }
  90% { opacity: 0; transform: scale(1.85); }
}
@keyframes sewerSignalSweep {
  0%, 58%, 100% { opacity: 0; stroke-dashoffset: 0; }
  67% { opacity: .7; }
  86% { opacity: .7; stroke-dashoffset: -42; }
}

@media (prefers-reduced-motion: reduce) {
  .sewer-chatbot-icon .pipe,
  .sewer-chatbot-icon .flow,
  .sewer-chatbot-icon .halo,
  .sewer-chatbot-icon .signal,
  .sewer-chatbot-icon .mh-wrap,
  .sewer-chatbot-icon .mh-lock,
  .sewer-chatbot-icon .j1,
  .sewer-chatbot-icon .j2,
  .sewer-chatbot-icon .j3 { animation: none !important; }
  .sewer-chatbot-icon .pipe { stroke-dashoffset: 0; }
}

/* ---------------------------------------------------------------- mobile */
@media (max-width: 860px) {
  .ai-panel {
    /* Glued to the visible viewport by ai-ui.js when the keyboard is open
       (transform:translateY(offsetTop) + height=visualViewport.height), so the
       header stays at the top of what the user sees and the composer sits on the
       keyboard. No transition here — it must track iOS frame-by-frame. The
       opaque .ai-overlay behind never moves, so nothing bleeds through. */
    position: absolute;
    inset: 0;
    width: auto;
    height: auto;
    /* Smooths the composer rising when the sheet is pre-shrunk on focus. Only
       height is animated; `top` (used only if iOS still scrolls) is not, so
       tracking stays crisp. */
    transition: height .2s ease;
    border-left: 0;
    border-radius: 0;
    animation: aiSheetUp .26s cubic-bezier(.16,1,.3,1) both;
  }

  /* The safe-area inset belongs to the header and the footer, NOT to the panel.
     Padding the panel left its own --bg showing above the header and below the
     privacy note, which is the empty band at the top and bottom of the screen
     on an iPhone. Putting the inset inside those bars instead lets their
     surface colour run edge to edge under the notch and the home indicator. */
  .ai-head { padding-top: calc(14px + env(safe-area-inset-top, 0px)); }
  /* No home-indicator safe-area band at the bottom — the user wants the sheet
     to sit flush to the edge like a normal web page. Just a small fixed cushion
     so the text is not jammed against the very edge; env() is deliberately NOT
     reserved here, so there is no empty band under the composer. */
  .ai-privacy { padding-bottom: 8px; }
  .ai-search-tab, .ai-kb { padding-bottom: 12px; }

  /* Belt and braces for the bands. env() resolves differently across iOS
     versions and changes when the app returns from the background, so relying
     on padding alone left a strip of a different colour at the notch or the
     home indicator. Painting the overlay itself in the bar colour means any
     strip that does appear is already the right colour — there is nothing left
     to look like a gap. The scrim is desktop-only anyway, since the sheet is
     full-screen here. */
  .ai-overlay { background: var(--surface-container-lowest); }
  .ai-scrim { display: none; }
  /* The panel carries the bar colour too, so a strip left by env() at either
     edge is the same colour as the header and the composer and simply cannot
     read as a gap. The chat area paints its own --bg back over the middle. */
  .ai-panel { background: var(--surface-container-lowest); }
  .ai-body { background: var(--bg); }

  /* With the keyboard up the home indicator is not drawn, so drop even the
     small cushion and sit the composer right on the keyboard. */
  body.ai-keyboard .ai-privacy { padding-bottom: 8px; }
  body.ai-keyboard .ai-search-tab,
  body.ai-keyboard .ai-kb { padding-bottom: 12px; }
  .ai-bubble { max-width: 92%; font-size: 15px; }
  .ai-composer textarea { font-size: 16px; }  /* stops iOS zoom-on-focus */
  .ai-doc { grid-template-columns: 1fr; }
  .ai-doc-status { justify-self: start; }
  .ai-source-sheet { max-height: 86vh; }

  /* ===================== PAGE MODE (phones) =====================
     A strict app shell, exactly as specified:
       1. the header never moves,
       2. the composer sits directly on the keyboard,
       3. the ONLY scrollable thing is the message list,
       4. no filler text under the composer.
     The container is fixed, JS sizes it to visualViewport.height and moves it
     to visualViewport.offsetTop, so it covers exactly what the phone is showing
     however WebKit has slid that window. Nothing here is sticky: the header is
     simply a non-scrolling flex row of a full-height column. */
  /* html and body are the LAYOUT viewport and nothing sizes them.
     They briefly took --app-height (1.0.146) so the document would end where
     the sheet does. Measured on the phone, that fed a loop: writing the height
     changed the document, WebKit re-clamped the visual viewport, that fired
     another resize, and the sheet converged downwards -- 844 to 80px in a fifth
     of a second, then crawling back to 380 over seven seconds while the usable
     area was 450. Only the sheet is sized now, and it is out of flow, so the
     document never changes and there is nothing to feed back. */
  html.ai-page-lock { height: 100%; overflow: hidden; }
  /* The background is the sheet's, not the app's. While WebKit slides the
     window the sheet cannot be exactly where the window is (see the backdrop
     note below), and whatever shows through in those frames must not be a
     different colour. */
  body.ai-page { height: 100%; overflow: hidden; background: var(--surface-container-lowest); }
  body.ai-page #root { display: none !important; }      /* the page replaces the app */
  body.ai-page > .ai-overlay {
    /* The overlay is the backdrop and covers the WHOLE layout viewport, in the
       sheet's own colour. It is not sized to the keyboard -- the panel is.

       That separation is what makes the keyboard animation survivable. WebKit
       publishes the destination of its reveal-slide at once but moves the
       window there over about a fifth of a second, and the true position in
       between is not observable from script: visualViewport.offsetTop already
       reads as the destination. So for those few frames the sheet and the
       window disagree no matter which of them is followed -- measured as a
       270px band of page background above the header on the way in, and a
       clipped header on the way out.

       Nothing can close that gap, so nothing is left in it to see. The backdrop
       already fills the whole layout viewport in the sheet's colour, so the
       header simply slides into place over its own background instead of over
       the app's page wash. */
    position: fixed;
    inset: 0;
    width: 100%;
    display: block;
    overflow: hidden;               /* the shell never scrolls */
    background: var(--surface-container-lowest);
    touch-action: auto;
  }
  /* Three rows, and the middle one is minmax(0, 1fr) rather than 1fr: a bare
     1fr takes its minimum from the content, so a long message list would push
     the composer off the bottom. */
  /* Three rows, and the middle one is minmax(0, 1fr) rather than 1fr: a bare
     1fr takes its minimum from the content, so a long message list would push
     the composer off the bottom. */
  body.ai-page .ai-panel {
    /* Anchored to the TOP, and --app-offset carries WebKit's slide.
       Bottom-anchoring was tried and is exactly equivalent at rest and during
       the animation alike: the offset WebKit reports IS the slack, so a panel
       on the bottom edge and a panel translated by the offset land in the same
       place, frame for frame. Measured, on the phone: the header dived 388px
       and slid back over a quarter second either way.

       What removes that dive is upstream of both -- the composer is put above
       the keyboard line before the keyboard is summoned (ai-ui.js), so WebKit
       has nothing to reveal and never slides. Top-anchoring is what lets that
       work: the height is applied by moving the panel's BOTTOM edge up, which
       is the composer, and leaves the header exactly where it is. */
    position: absolute;
    left: 0; right: 0; top: 0; bottom: auto;
    width: 100%;
    height: var(--app-height, 100%);
    min-height: 0;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    transform: translateY(var(--app-offset, 0px));
    animation: none;
    box-shadow: none;
    border: 0;
    transition: none;
  }
  /* 1. Header: one compact bar the same height as the app's own topbar (64px),
     holding only menu / title / close. The project details, the tabs and Clear
     chat live in the left drawer, so the chat starts right under the bar
     instead of under a block of text. */
  body.ai-page .ai-head {
    position: static;
    min-height: 64px;
    gap: 12px;
    padding: 10px 12px;
    padding-top: calc(10px + env(safe-area-inset-top, 0px));
  }
  body.ai-page .ai-head-id { flex: 1; min-width: 0; }
  body.ai-page .ai-head-id strong { font-size: 16px; letter-spacing: -.01em; }
  body.ai-page .ai-menu-btn {
    flex: none;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 12px;
    background: transparent;
    color: var(--ink);
    cursor: pointer;
  }
  body.ai-page .ai-menu-btn svg { width: 22px; height: 22px; }
  /* Match the app's topbar button exactly: 44x44 tap target around the shared
     .hamburger-icon, whose phone metrics core.css already sets. */
  body.ai-page .ai-menu-btn { width: 44px; height: 44px; }
  body.ai-page .ai-menu-btn:active { transform: scale(var(--press-strong, .94)); }

  /* Left drawer: everything that used to crowd the header. */
  body.ai-page .ai-drawer {
    position: absolute;
    z-index: 8;
    top: 0;
    bottom: 0;
    left: 0;
    width: min(300px, 84%);
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: calc(14px + env(safe-area-inset-top, 0px)) 14px 16px;
    background: var(--surface-container-lowest);
    border-right: 1px solid var(--line);
    box-shadow: 18px 0 46px -28px rgba(0, 0, 0, .6);
    overflow-y: auto;
    overscroll-behavior: contain;
    animation: aiDrawerIn .22s cubic-bezier(.16, 1, .3, 1) both;
  }
  body.ai-page .ai-drawer[hidden] { display: none; }
  @keyframes aiDrawerIn { from { transform: translateX(-100%); } to { transform: none; } }
  body.ai-page .ai-drawer-scrim {
    position: absolute;
    inset: 0;
    z-index: 7;
    background: rgba(6, 12, 18, .5);
  }
  body.ai-page .ai-drawer-scrim[hidden] { display: none; }
  body.ai-page .ai-drawer-id { display: flex; align-items: center; gap: 11px; padding: 2px 2px 12px; border-bottom: 1px solid var(--line); }
  body.ai-page .ai-drawer-id strong { display: block; color: var(--ink); font-size: 15px; }
  body.ai-page .ai-drawer-id small { display: block; color: var(--muted); font-size: 11.5px; line-height: 1.4; }
  /* Tabs stack vertically inside the drawer. */
  body.ai-page .ai-tabs {
    flex: none;
    display: grid;
    gap: 4px;
    padding: 0;
    border-bottom: 1px solid var(--line);
    padding-bottom: 10px;
    background: transparent;
  }
  body.ai-page .ai-tab { min-height: 44px; text-align: left; padding: 0 12px; }
  body.ai-page .ai-drawer-clear { margin-top: auto; min-height: 44px; }
  body.ai-page .ai-body { min-height: 0; overflow: hidden; display: grid; grid-template-rows: minmax(0, 1fr); }
  body.ai-page .ai-chat {
    min-height: 0;
    overflow: hidden;
    display: grid;
    grid-template-rows: minmax(0, 1fr) auto;   /* messages, then composer */
  }
  body.ai-page .ai-filter { flex: none; }
  /* 3. The one and only scroll container. */
  body.ai-page .ai-messages {
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    touch-action: pan-y;
  }
  /* 2. Composer is the last fixed row, so it rests on top of the keyboard once
     the shell is sized to the visible height. */
  body.ai-page .ai-composer {
    /* The home indicator's inset belongs to the composer, the last row, and
       only when the keyboard is down — with the keyboard up the keyboard itself
       occupies that band. */
    padding-bottom: max(8px, env(safe-area-inset-bottom, 0px));
  }
  body.ai-keyboard.ai-page .ai-composer { padding-bottom: 8px; }
  /* The lift (ai-ui.js liftComposer): while the keyboard is on its way the
     textarea is moved up out of the keyboard's path and painted invisibly, and
     a still copy holds its slot, so the screen does not jump up and back.
     Only paint is hidden -- not colour, not the caret -- and !important because
     dark mode forces textarea background and border with !important. */
  body.ai-page .ai-composer { position: relative; }
  body.ai-page .ai-standin { display: none; color: var(--ink); }
  body.ai-page .ai-composer.ai-lifting .ai-standin {
    display: block;
    position: absolute;
    margin: 0;
    border: 1px solid;
    overflow: hidden;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    pointer-events: none;
  }
  body.ai-page .ai-standin.is-placeholder { color: color-mix(in srgb, var(--muted) 72%, transparent); }
  body.dark.ai-page .ai-standin.is-placeholder { color: #7f8792; }
  body.ai-page .ai-composer.ai-lifting textarea {
    transform: translateY(var(--ai-lift, 0px));
    -webkit-text-fill-color: transparent !important;
    background: transparent !important;
    border-color: transparent !important;
    box-shadow: none !important;
    outline: none !important;
    transition: none !important;
    pointer-events: none;
  }
  body.ai-page .ai-composer.ai-lifting textarea::placeholder {
    color: transparent !important;
    -webkit-text-fill-color: transparent !important;
  }
  /* 4. Drop the privacy note on phones — it ate a block of space right under the
     composer. It stays on desktop, and the same assurance is in the empty state. */
  body.ai-page .ai-privacy { display: none; }
  /* The other tabs scroll inside themselves too, never the page. */
  body.ai-page .ai-search-tab,
  body.ai-page .ai-kb,
  body.ai-page .ai-history { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
}

@media (prefers-reduced-motion: reduce) {
  .ai-panel, .ai-scrim, .ai-source-sheet { animation: none; }
  .ai-typing i { animation: none; }
}

/* --------------------------------------------------- project context + history */
/* The project name sits directly under the assistant title so the scope of every
   answer is visible before the question is typed. */
.ai-head-project { display: block; color: var(--brand-2); font-size: 12px; font-weight: 800; letter-spacing: -.01em; }
.ai-head-id small.ai-head-project + small { color: var(--muted); }

.ai-history { flex: 1; min-height: 0; overflow-y: auto; padding: 14px; }
.ai-history-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.ai-history-list { display: grid; gap: 8px; }
.ai-history-item { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 16px; background: var(--surface-container-lowest); }
.ai-history-open { flex: 1; min-width: 0; display: grid; gap: 3px; text-align: left; border: 0; background: transparent; cursor: pointer; padding: 0; }
.ai-history-open strong { color: var(--ink); font-size: 14px; letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-history-open small { color: var(--muted); font-size: 11.5px; }
.ai-history-item:hover { border-color: var(--brand-2); }

/* The #ai-debug readout. Never rendered unless the URL asks for it by name;
   ai-ui.js creates the element only then. Deliberately ugly and on top of
   everything: it exists to be photographed, not to be lived with. */
.ai-debug {
  position: absolute; z-index: 40; top: 0; right: 0;
  margin: 0; padding: 6px 8px;
  max-width: 78%;
  font: 10px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre;
  color: #0f0;
  background: rgba(0, 0, 0, .82);
  border-bottom-left-radius: 8px;
  pointer-events: none;
}

/* Whatever the assistant offers to do is offered inside the reply, never done
   silently. Buttons wrap: an offer can carry a second, quieter choice. */
.ai-msg-actions { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 8px; }
.ai-msg-action { min-height: 38px; padding: 0 16px; font-size: 13px; }

/* Opt-in local model (Gemma 4). Lives in the drawer only -- never in the
   composer or the message list, which carry the keyboard/viewport work. */
.ai-local { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface-container-low, transparent); }
.ai-local[hidden] { display: none; }
.ai-local strong { color: var(--ink); font-size: 14px; }
.ai-local p { margin: 0; color: var(--muted); font-size: 12.5px; line-height: 1.45; }
.ai-local .ai-local-warn { color: var(--amber, #b9770e); }
.ai-local progress { width: 100%; height: 10px; }
.ai-local .ai-btn { min-height: 44px; }
.ai-engine-note { margin: 8px 0 0; color: var(--muted); font-size: 11.5px; line-height: 1.4; }
