/* ═══════════════════════════════════════════════════════════════════════════
   Chat page components.
   ───────────────────────────────────────────────────────────────────────────
   Loaded only by index.html. Every colour here is a token from app.css — this
   file adds no palette of its own, which is the difference from the ~350-line
   private token block the old index.html carried. That block is why the Ask page
   looked like a different product from the Reports page.
   ═══════════════════════════════════════════════════════════════════════════ */

/* `.main__bare` — the shared bare-page content slot — is defined in app.css,
   beside the rest of the is-bare height chain. It used to live here, which
   meant the two OTHER bare pages (dashboard, upload) never loaded it at all.
   The chat's OWN rows live below. */
.chat{flex:1; min-height:0; display:grid; grid-template-rows:1fr auto;}

.chat__scroll{
  overflow-y:auto; overscroll-behavior:contain; padding:22px 20px 8px;
  scroll-behavior:smooth;
}
/* `min-height:100%` is what lets the empty state fill the scrollport instead
   of hanging off its top — see the note above `.welcome`. It resolves because
   `.chat__scroll` is the definite `1fr` row of `.chat{grid-template-rows:1fr
   auto}`; if that ever becomes `auto`, the empty state silently collapses
   back to a short block at the top with the void beneath it. It costs a
   loaded transcript nothing: a column of turns is already taller than this. */
.chat__col{max-width:var(--page-max); margin:0 auto; display:flex;
  flex-direction:column; gap:22px; min-height:100%;}

/* ── messages ───────────────────────────────────────────────────────────── */
.turn{display:flex; flex-direction:column; gap:12px;}

.ask{display:flex; gap:11px; align-items:flex-start; justify-content:flex-end;}
.ask__text{
  background:var(--bubble-user-bg); color:var(--bubble-user-fg);
  border:1px solid var(--bubble-user-border); border-radius:var(--radius-lg);
  padding:10px 14px; font-size:14px; line-height:1.55; max-width:min(46rem,78%);
  white-space:pre-wrap; overflow-wrap:anywhere;
}
.avatar{
  width:28px; height:28px; border-radius:50%; flex:none; display:grid;
  place-items:center; font-size:11.5px; font-weight:700;
}
.avatar--user{background:var(--bg-surface-alt); color:var(--text-secondary);
  border:1px solid var(--border-color);}
.avatar--bot{background:var(--color-accent); color:var(--color-accent-on);}
.avatar--bot svg{width:15px; height:15px;}

.reply{display:flex; gap:11px; align-items:flex-start;}
.reply__body{min-width:0; flex:1; display:flex; flex-direction:column; gap:12px;}

/* ── the reasoning trail ────────────────────────────────────────────────── */
.trail{
  display:flex; flex-direction:column; gap:2px;
  border-left:2px solid var(--border-color); padding:2px 0 2px 12px;
}
.trail__step{
  display:flex; align-items:center; gap:8px; font-size:12.5px;
  color:var(--text-muted); padding:2px 0;
}
.trail__step.is-active{color:var(--text-primary); font-weight:550;}
.trail__step.is-bad{color:var(--color-danger);}
.trail__icon{width:14px; height:14px; flex:none; display:grid; place-items:center;}
.trail__icon svg{width:13px; height:13px;}
.trail__spin{
  width:11px; height:11px; border-radius:50%;
  border:2px solid var(--color-accent-soft-strong); border-top-color:var(--color-accent);
  animation:spin .8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg);}}
.trail__el{font-variant-numeric:tabular-nums; color:var(--text-faint); font-size:11.5px;}

/* Collapsed once the answer starts arriving: the detail stays reachable but
   stops competing with the answer for attention. */
.trail-done{margin:0;}
.trail-done>summary{
  cursor:pointer; font-size:12px; color:var(--text-muted); list-style:none;
  display:inline-flex; align-items:center; gap:6px; padding:3px 8px;
  border:1px solid var(--border-color); border-radius:var(--radius-pill);
  background:var(--bg-surface-alt);
}
.trail-done>summary::-webkit-details-marker{display:none;}
.trail-done>summary:hover{color:var(--text-primary);}
.trail-done[open]>summary{margin-bottom:8px;}

/* ── the answer ─────────────────────────────────────────────────────────── */
.prose--answer{font-size:14.5px; line-height:1.62; max-width:52rem;}
.prose--answer>*:first-child{margin-top:0;}
.prose--answer>*:last-child{margin-bottom:0;}

.provrow{display:flex; align-items:center; gap:8px; flex-wrap:wrap;}

.answer__foot{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  flex-wrap:wrap; padding-top:11px; border-top:1px solid var(--border-color);
}
.answer__by{font-size:12px; color:var(--text-muted); min-width:0;}
.answer__by.is-bad{color:var(--color-danger);}
.answer__by.is-warn{color:var(--color-warning);}
.tools{display:flex; align-items:center; gap:3px; flex-wrap:wrap;}
.tool{
  display:inline-flex; align-items:center; gap:6px; padding:5px 9px;
  border:1px solid transparent; border-radius:var(--radius-sm); background:none;
  font:inherit; font-size:12.5px; color:var(--text-muted); cursor:pointer;
  transition:background var(--transition-fast), color var(--transition-fast);
}
.tool:hover{background:var(--bg-hover); color:var(--text-primary);}
.tool svg{width:14px; height:14px;}
.tool--accent{color:var(--color-accent); font-weight:600;}
.tool--accent:hover{background:var(--color-accent-soft); color:var(--color-accent);}

/* feedback loop (dev-cycle only) — the "Flag issue" toggle-reveal panel,
   appended after .answer__foot exactly like the "View SQL" box. */
.fb{
  margin-top:8px; display:flex; flex-direction:column; gap:8px;
  padding:10px 12px; border:1px solid var(--border-color);
  border-radius:var(--radius-md); background:var(--bg-surface-alt);
}
.fb__actions{display:flex; justify-content:flex-end;}

/* ── the result panel ───────────────────────────────────────────────────── */
.result{display:flex; flex-direction:column; gap:13px;}
.result__grid{display:grid; grid-template-columns:1.25fr .75fr; gap:14px; align-items:stretch;}
.result__grid.is-one{grid-template-columns:1fr;}
@media (max-width:1000px){.result__grid{grid-template-columns:1fr;}}

/* A turn reopened from history: its rows are fetched when it scrolls into view.
   Deliberately quiet — this is a placeholder for a table, not a warning, and it
   sits exactly where the table will appear. `.result--offloaded` carries the
   collapsed panel's measured min-height, so a panel evicted by the LRU above the
   viewport leaves the scroll position where it was. */
.result--offloaded{justify-content:flex-start;}
.offload{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:10px 12px; border:1px dashed var(--border-color);
  border-radius:var(--radius-md); background:var(--bg-panel);
}
.offload__label{font-size:12.5px; color:var(--text-muted);}
.offload__action{display:flex; align-items:center; gap:8px; min-height:22px;}
.offload__spin{
  width:11px; height:11px; border-radius:50%;
  border:2px solid var(--color-accent-soft-strong); border-top-color:var(--color-accent);
  animation:spin .8s linear infinite;
}
.panel{
  background:var(--bg-card); border:1px solid var(--border-color);
  border-radius:var(--radius-md); display:flex; flex-direction:column; min-width:0;
  overflow:hidden;
}
/* One centre line for everything in it, and the TITLE is the only element
   allowed to shrink — the controls keep their size, which is the opposite of
   what happened before (a long title squashed the filter box to 45px). */
.panel__head{
  display:flex; align-items:center; gap:var(--sp-2);
  padding:6px 10px; min-height:38px;
  border-bottom:1px solid var(--border-color); background:var(--bg-surface-alt);
}
.panel__title{
  flex:1 1 auto; min-width:0;
  font-size:var(--fs-chrome); font-weight:650; color:var(--text-secondary);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.panel__body{padding:0; min-width:0; flex:1;}
.panel__body--pad{padding:12px;}
/* The height belongs to `.tablescroll`, not to `.tablewrap` — see the note in
   dash.css. With it on the wrap, the toolbar and the sort caption scrolled away
   with the rows, which is the whole thing splitting the scrollport out was for.
   Putting a bounded height here is also what finally makes the sticky header
   do something: it had a scrolling ancestor but never a short one. */
.panel .tablewrap{border:0; border-radius:0;}
.panel .tablescroll{max-height:340px;}
.panel--modal .tablescroll{max-height:none;}


/* The chart-type picker. This IS a segmented control — a choice between views
   of one thing — so it looks like one: an inset track with a raised chip for
   the current option, the same component language as .seg. It was three
   independent pills at 21px that agreed with nothing.  */
.chartpills{
  display:flex; align-items:center; gap:2px; margin-left:auto; flex:none;
  height:var(--ctl-h-sm); padding:2px;
  background:var(--bg-surface-alt); border:1px solid var(--border-color);
  border-radius:var(--radius-sm);
}
.chartpill{
  display:inline-flex; align-items:center; justify-content:center;
  height:calc(var(--ctl-h-sm) - 6px); padding:0 9px;
  border:0; border-radius:3px; background:none;
  font:inherit; font-size:11.5px; font-weight:600; line-height:1;
  color:var(--text-muted); cursor:pointer; white-space:nowrap;
  transition:background var(--transition-fast), color var(--transition-fast);
}
.chartpill:hover{color:var(--text-primary);}
.chartpill[aria-pressed="true"]{
  background:var(--bg-card); color:var(--text-primary);
  box-shadow:var(--shadow-xs);
}
.chartpill:focus-visible{outline:2px solid var(--color-accent);
  outline-offset:-1px;}
.chartwrap{height:290px; padding:10px;}

/* ── the "more views" disclosure ──────────────────────────────────────────
   A full-width .btn read as an empty grey slab across the answer — a big
   nothing between the charts and the follow-ups. This is a disclosure, not an
   action: a faded accent wash that deepens on hover, a hairline that firms up,
   and a chevron that turns when the panels come down. */
.morebar{
  display:flex; align-items:center; justify-content:center; gap:8px;
  width:100%; height:var(--ctl-h); padding:0 14px; margin-top:2px;
  border:1px solid transparent; border-radius:var(--radius-md);
  background:var(--color-accent-soft);
  font:inherit; font-size:12px; font-weight:600; letter-spacing:.01em;
  color:var(--color-accent); cursor:pointer;
  transition:background var(--transition-fast), border-color var(--transition-fast),
             box-shadow var(--transition-fast);
}
.morebar:hover{
  background:var(--color-accent-soft-strong);
  border-color:color-mix(in srgb, var(--color-accent) 26%, transparent);
}
.morebar:focus-visible{outline:2px solid var(--color-accent); outline-offset:2px;}
.morebar__chev{
  width:13px; height:13px; flex:none;
  transition:transform var(--transition-base);
}
.morebar[aria-expanded="true"] .morebar__chev{transform:rotate(180deg);}
/* The panels themselves arrive rather than appear. `hidden` is display:none, so
   this runs on reveal — one pass, no layout thrash.
   NO fill mode, deliberately: the `to` keyframe IS the panel's resting state, so
   there is nothing to hold, and a filled animation would keep a transform on the
   element for the life of the page — promoting a layer and out-ranking every
   later declaration in the cascade. That combination is what made hovering one
   card blink all of them; see the note in controls.test.js. */
.chart-extra{animation:chartIn var(--transition-base) ease-out;}
@keyframes chartIn{
  from{opacity:0; transform:translateY(-6px);}
  to{opacity:1; transform:none;}
}
@media (prefers-reduced-motion:reduce){
  .chart-extra{animation:none;}
  .morebar,.morebar__chev{transition:none;}
}

/* ── what a plot is actually showing ──────────────────────────────────────
   A windowed chart must say so, and say it in a way that reads as a sentence
   rather than a debug line: the COUNT is the fact, the rest is context. */
.plotnote{
  display:flex; align-items:baseline; gap:5px; flex-wrap:wrap;
  padding:0 10px 2px; font-size:11.5px; color:var(--text-muted);
}
.plotnote__n{color:var(--text-primary); font-weight:650;
  font-variant-numeric:tabular-nums;}
.plotnote__span{margin-left:auto; color:var(--text-faint);}
.plotcaveat{
  display:flex; gap:6px; padding:0 10px 8px;
  font-size:11.5px; line-height:1.45; color:var(--color-warning);
}
.plotrow{display:flex; align-items:center; gap:9px; padding:0 10px 10px;}

/* ── the range slider ────────────────────────────────────────────────────
   One track, two handles. It was two stacked sliders labelled From and To,
   which is two rows of chrome to express one interval — and never shows you
   the interval itself, only its ends. Here the selected span is drawn.

   A native range input cannot have two thumbs, so this is two of them laid
   over one rail with their own tracks made transparent. The inputs ignore
   pointer events and only their THUMBS take them back: otherwise the one on
   top swallows every click meant for the other. */
.rangeslider{
  position:relative; flex:1 1 auto; min-width:90px; height:var(--ctl-h);
  cursor:pointer;                 /* the whole rail is clickable — see page-ask */
}
/* A range thumb's CENTRE travels from half-a-thumb in to half-a-thumb from the
   end, never the full width — so a rail drawn edge to edge, and a fill measured
   as a plain percentage of it, drift from the handles by up to half a thumb
   (11px at the right-hand end, which is where the default window sits). Inset
   the rail by exactly that, and the drawn track becomes the travel range. */
.rangeslider__rail{
  position:absolute; top:50%; transform:translateY(-50%);
  left:calc(var(--slider-thumb) / 2); right:calc(var(--slider-thumb) / 2);
  height:4px; border-radius:2px; pointer-events:none;
  background:var(--border-strong);
}
/* Nested INSIDE the rail, so its percentages resolve against the travel range
   rather than the full width — the alignment is then geometric, not a constant
   someone has to keep in step. */
.rangeslider__fill{
  position:absolute; top:0; height:100%;
  border-radius:2px; pointer-events:none; background:var(--color-accent);
}
.rangeslider .slider{
  position:absolute; left:0; top:0; width:100%; height:100%;
  margin:0; pointer-events:none; background:transparent;
}
.rangeslider .slider::-webkit-slider-runnable-track{background:transparent;}
.rangeslider .slider::-moz-range-track{background:transparent;}
.rangeslider .slider::-webkit-slider-thumb{pointer-events:auto;}
.rangeslider .slider::-moz-range-thumb{pointer-events:auto;}
/* The end handle sits above, so the two are still separable when they meet. */
.rangeslider .slider:last-of-type{z-index:1;}

/* A horizontal bar draws EVERY category label (autoSkip is off — an unnamed bar
   is not a bar chart), so the canvas has to be tall enough for them and the
   frame scrolls. At a fixed height 37 dates overlapped into an unreadable
   smear down the y axis. */
.chartsizer{height:100%; min-height:100%;}
.chartwrap--scroll{overflow-y:auto; overscroll-behavior:contain;}
.chartwrap--scroll .chartsizer{min-height:0;}

/* ── composer ───────────────────────────────────────────────────────────── */
/* ── the composer ────────────────────────────────────────────────────────────
   The single most-used control in the product, and the one a user judges the
   whole thing by: it is what they touch before every answer.

   The bar it sits in is a PANEL — one step behind the canvas — with the box a
   white sheet raised off it. That is the same surface logic as a visual on the
   report canvas, applied to the one control that matters most, and it is why
   the composer reads as an object you type into rather than a border at the
   bottom of the page.

   Six things make the difference between a styled textarea and a finished one:

     1. THE WHOLE BOX IS THE TARGET. `cursor:text` on the container and a click
        handler is not needed — the textarea fills it. Clicking the padding used
        to do nothing, which on a 56px-tall control is most of its area.

     2. FOCUS IS A LIFT, NOT A RING ALONE. Accent border, a soft accent glow, a
        deeper shadow and 1px of rise. It is 200ms and reads as the control
        coming forward to meet you.

     3. THE SEND BUTTON IS A SQUARE WITH A RADIUS, NOT A CIRCLE. A circle beside
        a 10px-radius box is two different languages; matching the box's inner
        radius makes them one object. It is also DISABLED until there is
        something to send, so the primary action is never a lie.

     4. THE TEXTAREA GROWS, AND THE BOX GROWS WITH IT. Up to 8 lines, then the
        textarea scrolls inside a box that has stopped growing — so the page
        never loses its answer to a long question.

     5. STREAMING IS A STATE OF THE BAR. While an answer streams, the send
        button becomes Stop and the bar carries a moving accent hairline along
        its top edge — the progress signal is on the control you are waiting at,
        not somewhere else on the page.

     6. THE HINT IS ONE LINE AND IT RECEDES. It matters on the first visit and
        never again, so it is 11px, faint, and drops out entirely on a narrow
        screen where it would wrap to two lines.                              */
/* The bar stops being a chrome band. `--bg-panel` + a top border made it a
   separate strip of furniture pinned under the page — the composer sat ON
   something rather than IN the page. On the canvas colour with no rule, the
   composer box is the only object there, which is the correct reading: it is
   the instrument, the bar is just where it lives. The box keeps its own
   border and shadow, so it is still clearly a surface you type into. */
.composerbar{
  flex:none; position:relative;
  background:var(--bg-app);
  padding:var(--sp-3) var(--sp-5) var(--sp-4);
}
.composerbar__inner{max-width:var(--page-max); margin:0 auto; min-width:0;}

/* While an answer streams: a 2px accent line travelling along the top edge of
   the bar. On the control the user is waiting at — not a spinner elsewhere. */
.composerbar.is-streaming::before{
  content:''; position:absolute; left:0; right:0; top:-1px; height:2px;
  background:linear-gradient(90deg,
    transparent 0%, var(--color-accent) 42%, var(--color-accent) 58%,
    transparent 100%);
  background-size:220% 100%;
  animation:composerflow 1.6s linear infinite;
}
@keyframes composerflow{
  from{background-position:120% 0;} to{background-position:-120% 0;}
}

.composerbox{
  display:flex; align-items:flex-end; gap:var(--sp-2);
  padding:7px 7px 7px var(--sp-4);
  background:var(--bg-card);
  border:1px solid var(--border-strong);
  border-radius:var(--radius-lg);
  box-shadow:var(--shadow-sm);
  /* The whole box is the text target, not just the textarea's own rectangle. */
  cursor:text;
  transition:border-color var(--transition-base) var(--ease-out),
             box-shadow var(--transition-base) var(--ease-out),
             transform var(--transition-base) var(--ease-out);
}
.composerbox:hover:not(:focus-within){border-color:var(--text-faint);}
.composerbox:focus-within{
  border-color:var(--color-accent);
  box-shadow:var(--shadow-md), 0 0 0 4px var(--color-accent-soft);
  transform:translateY(-1px);
}
.composerbox textarea{
  flex:1 1 auto; min-width:0;
  border:0; outline:none; background:none; resize:none;
  font:inherit; font-size:var(--fs-lede); line-height:1.5;
  color:var(--text-primary);
  /* 8 lines, then the textarea scrolls and the BOX stops growing — so a long
     question never pushes the answer off the screen. */
  max-height:calc(1.5em * 8); min-height:calc(1.5em * 1);
  padding:8px 0; margin:0;
  font-variant-numeric:normal;   /* prose, not a column of figures */
}
.composerbox textarea::placeholder{color:var(--text-faint);}
/* The textarea's own scrollbar, once it has one: thin and only over the box. */
.composerbox textarea{scrollbar-width:thin;}

/* ── the send / stop button ──────────────────────────────────────────────────
   A square with the box's inner radius, not a circle: a circle beside a 10px
   box is two languages in one object. Same press and focus behaviour as every
   other button in the product. */
/* 36px from --ctl-h-lg, not a hand-set 34: DESIGN.md #4 requires every
   interactive control to claim its height from a token, and 34 was off the
   scale by two pixels for no reason.

   The gloss is gone — an inset white highlight plus a coloured drop shadow is
   a 2010 button, and against the flat surfaces everything else in this
   product uses it was the one object pretending to be three-dimensional. It
   is still a filled accent button, because it IS the primary action; it just
   stops advertising. */
.iconbtn{
  flex:none; width:var(--ctl-h-lg); height:var(--ctl-h-lg);
  display:grid; place-items:center;
  border:1px solid var(--color-accent); border-radius:var(--radius-sm);
  background:var(--color-accent); color:var(--color-accent-on);
  cursor:pointer;
  transition:background var(--transition-fast), border-color var(--transition-fast),
             box-shadow var(--transition-fast), transform 60ms var(--ease-out),
             opacity var(--transition-fast);
}
.iconbtn:hover:not([disabled]){
  background:var(--color-accent-dark); border-color:var(--color-accent-dark);
}
.iconbtn:active:not([disabled]){
  transform:translateY(1px); box-shadow:inset 0 1px 2px rgba(0,0,0,.18);
}
.iconbtn:focus-visible{outline:2px solid var(--color-accent); outline-offset:2px;}
/* Disabled until there is something to send. A primary action that is always
   available but does nothing is a lie about the state of the form. */
.iconbtn[disabled]{
  background:var(--bg-surface-alt); border-color:var(--border-color);
  color:var(--text-faint); box-shadow:none; cursor:not-allowed;
}
.iconbtn svg{width:16px; height:16px;}
/* Stop swaps the accent for danger and nothing else — it occupies the same
   slot as Send, so it must be the same object in a different state, not a
   second button design. It lost the same gloss for the same reason. */
.iconbtn--stop{
  background:var(--color-danger); border-color:var(--color-danger); color:#fff;
}
.iconbtn--stop:hover:not([disabled]){
  background:var(--color-danger-dark, var(--color-danger));
  border-color:var(--color-danger-dark, var(--color-danger));
}

/* A secondary control inside the box (a model picker, an attach) — ghost, so it
   does not compete with Send for the eye. */
.composerbox__tool{
  flex:none; width:30px; height:30px; margin-bottom:2px;
  display:grid; place-items:center;
  border:0; border-radius:var(--radius-sm); background:none;
  color:var(--text-faint); cursor:pointer;
  transition:background var(--transition-fast), color var(--transition-fast);
}
.composerbox__tool:hover{background:var(--bg-hover); color:var(--color-accent);}
.composerbox__tool:focus-visible{outline:2px solid var(--color-accent);
  outline-offset:-2px;}
.composerbox__tool svg{width:16px; height:16px;}

/* The hint. Matters on the first visit and never again — and on the first
   visit the WELCOME is showing one, in its own colophon. Two copies of the
   same three shortcuts, ten pixels apart, is the page telling you twice.
   This copy stands down while the empty state is up and takes over once
   there is a transcript, which is the only time it is the only copy.
   (The markup stays in index.html either way: controls.test.js greps it for
   the <kbd> literals, and the element is the one that survives.) */
.chat.is-empty .composerbar__hint{display:none;}
.composerbar__hint{
  display:flex; align-items:center; justify-content:center; gap:10px;
  font-size:11px; color:var(--text-faint); margin-top:7px;
  min-height:1.2em;
}
.welcome__foot kbd,
.composerbar__hint kbd{
  font-family:var(--font-mono); font-size:10px;
  padding:1px 4px; border-radius:3px;
  background:var(--bg-surface-alt); border:1px solid var(--border-color);
  color:var(--text-muted);
}
/* On a narrow screen the hint wraps to two lines and costs more than it teaches. */
@media (max-width:720px){
  .composerbar__hint{display:none;}
  .composerbar{padding-left:var(--sp-3); padding-right:var(--sp-3);}
}

@media (prefers-reduced-motion:reduce){
  .composerbox,.iconbtn{transition:border-color var(--transition-fast);}
  .composerbox:focus-within{transform:none;}
  .iconbtn:active{transform:none;}
  .composerbar.is-streaming::before{animation:none; background:var(--color-accent);}
}


.scrollbtn{
  position:absolute; right:26px; bottom:110px; z-index:20;
  width:34px; height:34px; border-radius:50%; display:grid; place-items:center;
  background:var(--bg-card); border:1px solid var(--border-color);
  box-shadow:var(--shadow-md); color:var(--text-secondary); cursor:pointer;
}
.scrollbtn svg{width:16px; height:16px;}

/* ── welcome ──────────────────────────────────────────────────────────────
   THE DEAD SPACE, and why it was structural rather than a padding value.

   `.welcome` hung off the TOP of the scrollport (`margin:5vh auto 0`) while
   the composer is pinned to the bottom by `.chat{grid-template-rows:1fr auto}`.
   Everything between them was one pooled void — on a 900px window that is
   roughly 500px of nothing, and no amount of tuning the margin fixes it,
   because the slack all collects in a single place.

   The fix is to let the empty state OWN the height and distribute the slack
   as gaps instead: `.chat__col{min-height:100%}` (it resolves because
   `.chat__scroll` is the definite `1fr` row) and `.welcome` then fills that
   column and centres its own bands. The head sits above the optical centre,
   the starters at it, the hint at the foot — three measured gaps rather than
   one hole.

   Two things went with it, both flagged by the design language rather than by
   taste:
     * `.welcome__mark` — a 46x46 SOLID --color-accent square at an off-scale
       13px radius. DESIGN.md #1: the accent marks state as an EDGE, and there
       is never "an accent background larger than a chip". It was also the
       loudest object on a page whose subject is the question you are about to
       type.
     * the centred column. Every other page in the product opens with a
       left-aligned eyebrow -> title -> lede; this one alone was centre-set,
       which is what made it read as a splash screen rather than a page. */
.welcome{
  flex:1 1 auto; min-height:0;
  display:flex; flex-direction:column; justify-content:center;
  gap:var(--sp-6); max-width:none; margin:0; text-align:left;
}
.welcome__eyebrow{
  font-size:var(--fs-micro); font-weight:700; letter-spacing:.09em;
  text-transform:uppercase; color:var(--text-faint); margin:0 0 var(--sp-2);
}
.welcome h1{
  font-size:var(--fs-title); font-weight:670; letter-spacing:-.02em;
  margin:0 0 var(--sp-2); text-wrap:balance;
}
/* The lede takes a reading measure of its own rather than the full column —
   a 1280px-wide sentence is a line the eye has to track back across. */
.welcome p{
  color:var(--text-muted); margin:0; font-size:var(--fs-lede);
  line-height:1.55; max-width:58ch;
}

/* The starters, as lanes. `auto-fit` + `minmax` means the module groups sit
   side by side on a wide window and stack on a narrow one without a
   breakpoint per count. */
.welcome__deck{
  display:grid; grid-template-columns:repeat(auto-fit, minmax(19rem, 1fr));
  gap:var(--sp-5) var(--sp-6);
}
.welcome__group{min-width:0;}
.welcome__label{
  font-size:var(--fs-micro); font-weight:700; letter-spacing:.09em;
  text-transform:uppercase; color:var(--text-faint);
  padding-bottom:var(--sp-2); border-bottom:1px solid var(--border-color);
  margin-bottom:var(--sp-1);
}
.welcome__chips{display:flex; flex-direction:column;}

/* THE CHIP IS GONE. It was `border + --bg-card` — a bordered-and-filled box,
   which made the starters a THIRD such family on a screen DESIGN.md #6 allows
   exactly one of (the result panel is the one that earns it, because it is
   the substance). As hairline rows they read as a list of questions, which is
   what they are, and ten of them stop looking like ten buttons.

   Also the reason follow-ups improve for free: `.suggest--inline` builds on
   this same base, so the answer's "Next:" block becomes a list too. */
.suggest{
  display:flex; align-items:center; gap:var(--sp-3);
  width:100%; text-align:left; padding:9px 0 9px 2px;
  border:0; border-bottom:1px solid var(--border-color);
  border-radius:0; background:none; font:inherit;
  font-size:var(--fs-body); line-height:1.4; color:var(--text-secondary);
  cursor:pointer;
  transition:color var(--transition-fast) var(--ease-out),
             padding-left var(--transition-fast) var(--ease-out),
             border-color var(--transition-fast) var(--ease-out);
}
.suggest:last-child{border-bottom:0;}
/* A row reveals rather than grows: it takes the ink it was missing and steps
   toward the reader by 3px. The same gesture the conversation rows and the
   report rows already use, so a hover means one thing across the product. */
.suggest:hover{
  color:var(--text-primary); padding-left:5px;
  border-color:var(--border-strong);
}
.suggest:focus-visible{
  outline:2px solid var(--color-accent); outline-offset:2px; border-radius:3px;
}
/* The arrow is the affordance, and it only exists on approach — a static
   chevron on every one of ten rows is ten marks competing with the text. */
.suggest__go{
  margin-left:auto; flex:none; width:13px; height:13px; color:var(--text-faint);
  opacity:0; transform:translateX(-3px);
  transition:opacity var(--transition-fast) var(--ease-out),
             transform var(--transition-fast) var(--ease-out);
}
.suggest:hover .suggest__go,.suggest:focus-visible .suggest__go{
  opacity:1; transform:none; color:var(--color-accent);
}
@media (hover:none){
  /* No hover to reveal it, so it is simply always there. */
  .suggest__go{opacity:1; transform:none;}
}

/* The keyboard hint belongs to the empty state, where it is read once, rather
   than under the composer forever. */
.welcome__foot{
  display:flex; align-items:center; gap:var(--sp-4); flex-wrap:wrap;
  padding-top:var(--sp-4); border-top:1px solid var(--border-color);
  font-size:var(--fs-micro); color:var(--text-faint);
}

/* ── follow-ups ─────────────────────────────────────────────────────────────
   The model ends its answer with a "Next:" block. Those lines used to render
   TWICE — as inert markdown bullets inside the prose, and again as chips below
   the attribution footer, where nobody scrolls. The block is now cut out of the
   prose and rendered once, here, as real buttons directly under the answer. */
.followups{
  display:flex; flex-direction:column; gap:7px;
  padding-top:2px;
}
.followups__label{
  font-size:9.5px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  color:var(--text-faint);
}
.followups__row{display:flex; flex-direction:column; gap:6px; align-items:stretch;}
/* One per line rather than wrapped chips: these are whole sentences, and a row
   of wrapped sentence-length chips is unreadable. Left-aligned with the icon in
   a fixed gutter so the text starts at the same x on every row. */
.suggest--inline{
  display:flex; align-items:flex-start; gap:9px;
  padding:8px 12px; font-size:13px; line-height:1.45;
  max-width:52rem;   /* the prose measure, so they read as part of the answer */
}
.suggest--inline svg{
  width:13px; height:13px; flex:none; margin-top:3px; color:var(--text-faint);
  transition:color var(--transition-fast);
}
.suggest--inline:hover svg{color:var(--color-accent);}
.suggest--inline span{min-width:0;}

/* ── the conversations rail ────────────────────────────────────────────────
   A chat history is a primary object here, so the rail is built like one:
   a primary action, a search field, recency-grouped rows, and a footer that
   stays reachable. The rows themselves used to be styled `.thread` — the same
   class app.css used for the message LIST — which is why every one of them
   rendered as a centred column with its title clipped at both ends. See the
   note on `.convo` below.                                                   */
/* `.sidebar__extra` — the rail's page slot — is in app.css: reports.html uses
   the same slot and does not load this file. */

/* The rail's primary action — but NOT the page's. It was a full-width solid
   accent slab with a shadow, which made the loudest, highest-contrast object
   on the entire screen a button you press once at the start of a session,
   sitting above the conversation you are actually reading. DESIGN.md #1 is
   explicit that the accent is an edge and never a fill this size, and the
   product's own hierarchy allows one filled accent action per view — which on
   this page is Send, the thing you press constantly.

   So it de-escalates to surface contrast: a --bg-card row on the panel, with
   the accent kept on the glyph. It still reads as the primary thing in the
   rail (nothing else there has a border and a fill) without competing with
   the composer for the eye. Left-aligned, because it now sits above a list of
   left-aligned rows and a centred label in a full-width control was the odd
   one out. */
.newchat{
  display:flex; align-items:center; justify-content:flex-start; gap:8px;
  width:100%; flex:none; height:var(--ctl-h); padding:0 10px;
  border:1px solid var(--border-color); border-radius:var(--radius-md);
  background:var(--bg-card); font:inherit; font-size:var(--fs-chrome);
  font-weight:600; color:var(--text-primary); cursor:pointer;
  transition:border-color var(--transition-fast) var(--ease-out),
             background var(--transition-fast) var(--ease-out);
}
.newchat:hover{border-color:var(--color-accent); background:var(--bg-hover);}
.newchat:focus-visible{
  outline:2px solid var(--color-accent); outline-offset:2px;
}
.newchat:active{background:var(--bg-surface-alt);}
.newchat svg{width:14px; height:14px; flex:none; color:var(--color-accent);}

/* ── heading ── */
.convohead{
  display:flex; align-items:baseline; gap:7px; flex:none; padding:2px 2px 0;
}
.convohead__label{
  font-size:10px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  color:var(--text-faint);
}
/* The count, so a long history reads as a quantity rather than just a long
   scroll. Tabular so it does not jiggle as it changes. */
.convohead__n{
  margin-left:auto; font-size:10.5px; color:var(--text-faint);
  font-variant-numeric:tabular-nums;
}

/* ── search ── */
.convosearch{
  display:flex; align-items:center; gap:6px; flex:none;
  padding:0 8px; min-height:30px;
  border:1px solid var(--border-color); border-radius:var(--radius-md);
  background:var(--bg-surface-alt);
  transition:border-color var(--transition-fast), background var(--transition-fast),
             box-shadow var(--transition-fast);
}
.convosearch:focus-within{
  border-color:var(--color-accent); background:var(--bg-card);
  box-shadow:0 0 0 3px var(--color-accent-soft);
}
.convosearch>svg{width:13px; height:13px; flex:none; color:var(--text-faint);}
.convosearch__input{
  flex:1 1 auto; min-width:0; border:0; background:none; outline:none;
  font:inherit; font-size:12px; color:var(--text-primary); padding:5px 0;
}
.convosearch__input::placeholder{color:var(--text-faint);}
/* Hidden until there is something to clear — a permanently visible × on an
   empty field is a control that does nothing. */
.convosearch__x{
  flex:none; border:0; background:none; padding:2px; border-radius:4px;
  color:var(--text-faint); cursor:pointer; display:grid; place-items:center;
}
.convosearch__x:hover{background:var(--bg-hover); color:var(--text-primary);}
.convosearch__x svg{width:11px; height:11px;}

/* ── the list ── */
/* Scrolls on its own so the footer stays reachable at the foot of the rail
   instead of being pushed off the bottom by a long history. The negative
   margin lets a row's hover background reach the rail's edges while the
   scrollbar stays inside. */
.convolist{
  flex:1 1 auto; min-height:0; overflow-y:auto; overflow-x:hidden;
  margin:0 -4px; padding:0 4px 2px;
  display:flex; flex-direction:column; gap:1px;
}
/* The recency divider. Shown only when there is more than one group — with a
   single bucket the label is a heading over the whole list, which is noise. */
.convogroup{
  flex:none; padding:10px 10px 4px;
  font-size:9.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--text-faint);
}
.convogroup:first-child{padding-top:2px;}

.convoquiet{
  padding:14px 10px; border:1px dashed var(--border-color);
  border-radius:var(--radius-md); background:var(--bg-surface-alt);
}
.convoquiet__t{font-size:12px; font-weight:600; color:var(--text-secondary);}
.convoquiet__s{font-size:11px; color:var(--text-muted); margin-top:3px;
  line-height:1.45;}

/* ── one row ────────────────────────────────────────────────────────────────
   One line, never reflowing. The row is a DIV holding a button, not a button
   itself: nesting the rename/delete buttons inside a button is invalid HTML and
   browsers drop or reparent the inner ones.

   The action buttons are ABSOLUTELY POSITIONED over the timestamp rather than
   appended as flex siblings — as siblings they added width on hover, the row
   wrapped to three lines and the title spilled out of the rail. Nothing here
   changes size when the pointer arrives; only what is painted.               */
.convo{
  position:relative;
  /* flex-direction and justify-content are stated explicitly, not left to the
     default. This component was previously called `.thread`, the same name
     app.css used for the MESSAGE LIST — a flex column. app.css set
     flex-direction:column and this rule did not override it, so every row in
     the rail became a centred stack: title on one line, timestamp beneath, and
     a long title clipped at both ends. The name is fixed; declaring the axis is
     what makes the rule immune to it happening again. */
  display:flex; flex-direction:row; justify-content:flex-start;
  align-items:stretch; flex-wrap:nowrap; flex:none;
  width:100%; max-width:100%; overflow:hidden;
  border-radius:var(--radius-sm);
  transition:background var(--transition-fast), color var(--transition-fast);
}
.convo__open{
  flex:1 1 auto; min-width:0;
  display:flex; flex-direction:row; align-items:center; gap:8px; flex-wrap:nowrap;
  padding:7px 10px; border:0; background:none; font:inherit;
  font-size:12.5px; line-height:1.35; color:var(--text-secondary);
  cursor:pointer; text-align:left; min-height:32px;
}
.convo:hover{background:var(--bg-hover);}
.convo:hover .convo__open{color:var(--text-primary);}
.convo.is-active{background:var(--bg-active);}
.convo.is-active .convo__open{color:var(--text-primary); font-weight:600;}
.convo__open:focus-visible{outline:2px solid var(--color-accent);
  outline-offset:-2px; border-radius:var(--radius-sm);}
/* A 2px accent rule marks the open conversation without shouting — the same
   device the sidebar nav and the dashboard page tabs use. */
.convo.is-active::before{
  content:''; position:absolute; left:0; top:6px; bottom:6px; width:2px;
  border-radius:0 2px 2px 0; background:var(--color-accent); z-index:1;
}
.convo__title{flex:1 1 auto; min-width:0; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap;}
.convo__when{
  flex:none; font-size:10.5px; color:var(--text-faint); white-space:nowrap;
  font-variant-numeric:tabular-nums;
}
/* The action buttons sit over the end of the title and mask it as they fade in.
   That mask has to be OPAQUE where the buttons are, and this is where it is easy
   to get wrong: `--bg-hover` and `--bg-active` are translucent overlays (4.5%
   and 9–17% alpha), so a gradient ending in one of them ends in something you
   can still read through — the icons paint straight over the words.

   So the mask is TWO stacked gradients: an opaque `--convo-bg` underneath and
   the row's own translucent tint on top, both fading from transparent at the
   same rate. At the left edge both are clear, so the fade is real; under the
   buttons the pair composites to exactly the colour of the row behind them.

   `--convo-bg` is a local variable rather than `--bg-sidebar` directly, so the
   rail can be moved out of the sidebar without this rule going wrong silently. */
.convolist{--convo-bg:var(--bg-sidebar);}
.convo__acts{
  position:absolute; right:4px; top:50%; transform:translateY(-50%);
  display:none; gap:1px; align-items:center; padding-left:20px;
  background:
    linear-gradient(90deg, transparent, var(--bg-hover) 45%),
    linear-gradient(90deg, transparent, var(--convo-bg) 45%);
}
.convo:hover .convo__acts,.convo:focus-within .convo__acts{display:flex;}
.convo:hover .convo__when,.convo:focus-within .convo__when{visibility:hidden;}
.convo.is-active .convo__acts{
  background:
    linear-gradient(90deg, transparent, var(--bg-active) 45%),
    linear-gradient(90deg, transparent, var(--convo-bg) 45%);
}
/* Hover on an active row stacks both tints, matching the row underneath. */
.convo.is-active:hover .convo__acts{
  background:
    linear-gradient(90deg, transparent, var(--bg-hover) 45%),
    linear-gradient(90deg, transparent, var(--bg-active) 45%),
    linear-gradient(90deg, transparent, var(--convo-bg) 45%);
}
.convo__acts button{
  border:0; background:none; padding:4px; border-radius:5px; cursor:pointer;
  color:var(--text-faint); display:grid; place-items:center;
}
.convo__acts button:hover{background:var(--bg-card); color:var(--text-primary);}
.convo__acts button svg{width:12px; height:12px;}

/* Rename in the row. Sized to the row so the list does not jump when it opens. */
.convo__edit{
  width:100%; margin:1px 0; padding:6px 9px;
  border:1px solid var(--color-accent); border-radius:var(--radius-sm);
  background:var(--bg-card); color:var(--text-primary);
  font:inherit; font-size:12.5px; font-weight:600; outline:none;
  box-shadow:0 0 0 3px var(--color-accent-soft);
}

/* ── footer ── */
.convoclear{flex:none; padding-top:8px; border-top:1px solid var(--border-color);}
.convoclear .tool{width:100%; justify-content:flex-start;}
.convoclear__ask{
  font-size:11.5px; color:var(--text-secondary); line-height:1.4;
  padding:2px 2px 7px;
}
.convoclear__row{display:flex; gap:6px;}
.convoclear__row .btn{flex:1 1 auto; justify-content:center;}

/* ── login gate ─────────────────────────────────────────────────────────── */
.gate{
  position:fixed; inset:0; z-index:100; display:grid; place-items:center;
  background:var(--bg-page); padding:24px;
}
.gate__card{
  width:min(400px,100%); background:var(--bg-card);
  border:1px solid var(--border-color); border-radius:var(--radius-lg);
  box-shadow:var(--shadow-lg); padding:28px;
}
.gate__mark{
  width:44px; height:44px; border-radius:12px; margin:0 auto 16px;
  background:var(--color-accent); color:var(--color-accent-on);
  display:grid; place-items:center;
}
.gate__mark svg{width:23px; height:23px;}

/* ── pin dialog ─────────────────────────────────────────────────────────── */
.pin{display:grid; grid-template-columns:1fr 1fr; gap:18px;}
@media (max-width:820px){.pin{grid-template-columns:1fr;}}
.pin__preview{
  background:var(--bg-surface-alt); border:1px solid var(--border-color);
  border-radius:var(--radius-md); padding:13px; min-height:230px;
  display:flex; flex-direction:column; gap:9px;
}
/* The preview hosts a real visual, so it needs the same shape a tile body has:
   a bounded, flexible box the chart can measure itself against. */
.pin__frame{flex:1; min-height:0; display:flex; flex-direction:column; gap:7px;}
.pin__vizbody{
  flex:1; min-height:170px; display:flex; flex-direction:column; gap:7px;
  overflow:auto;
}
.pin__why{
  font-size:12px; color:var(--text-muted); background:var(--color-info-soft);
  border-radius:var(--radius-sm); padding:7px 10px;
}
.pin__form{display:flex; flex-direction:column; gap:11px; min-width:0;}
.pin__row{display:grid; grid-template-columns:1fr 1fr; gap:9px;}
.pin__form .label{margin-bottom:3px;}
.pin__form .input,.pin__form .select{padding:6px 9px; font-size:13px;}

.usage__grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:10px;}

@media print{
  .composerbar,.scrollbtn,.trail,.tools{display:none !important;}
  .chat__scroll{overflow:visible;}
}
