/* ===========================================================================
   The whole interface, in one file, in CASCADE ORDER. Read the order rules
   before moving anything; the section list is at the foot of this block.

   ---- why it is one file ----

   About 640 rules; the length is commentary, not CSS. Splitting it by component
   was refused because the cascade is load-bearing — some fifty selectors are
   written twice at EQUAL specificity with nothing but file order deciding, and
   there is no build step to keep two files in step. stylecheck.mjs also reads
   one path per surface (SURFACES[].css) and would stop sweeping what moved out.

   ---- the order rules ----

   1. EVERY max-width block sits after the rules it overrides. This is the one
      that bites: media queries add no specificity, so a base rule written
      BELOW its own override simply wins at every width. `.turn-current
      .turn-translation` did exactly that for as long as it was filed under the
      gate section — see the note beside it in HISTORY. No check here can see a
      pair like that.
   2. The rules for one thing live TOGETHER. That is rule 1's only real defence.
   3. `dialogs on a small screen` is LAST and must stay last — it overrides
      id-scoped dialog geometry and wins on file order, not specificity.
   4. There is a SECOND prefers-reduced-motion block near the foot, forced there
      because what it turns off lives inside a max-width query.
   5. `.layout.no-sidebar` is written after `.layout.sidebar-hidden` so it wins
      on the frame both are present.

   ---- a tint is a token, not a recipe ----

   `.banner`, `.hints`, `.tailor-row` and `.wizard-alert` each write the
   color-mix ONCE against `--tint`, so a variant is `--tint: var(--good)` and
   nothing else. Do not add one that restates the mixing.

   ---- the sections, in order ----

   LABEL VOICE      the mono/uppercase treatment every micro-label takes
   HEADER · MARK    the wordmark — a verbatim copy of landing/landing.css
   LAYOUT           the page grid, the sidebar and its rail
   INPUTS           fields, the ghost/solid button pair, pills
   MODAL            the dialog shell, its variants, the conversation list
   CONTROLS         the sidebar's set-up block, meters, fields
   INTERVIEW MODE   document rows, help dots, the switch
   ALERTS           busy/alert dialogs, tinted document rows, categories, bulb
   HISTORY          the transcript, the answers box and its strip, Advice menu
   GATE             the sign-in card
   SETTINGS         the account dialog's forms and category rows
   HOME · CHARTS    the home screen, its tiles and canvases, then its 520px block
   LANE DOTS … MODE MENU   the sidebar's remaining blocks
   CAPTURE BUTTON   one control for start, stop, elapsed
   VISUAL ASSISTANT the camera panel and the New dialog
   SIDE PANEL · AUTO ANSWERS · BOOKMARK
   WIZARD           one scaffold, its parts shown per step
   THE HANDSET      the narrow header and set-up dialog (max-width: 700px)
   (min-width: 701px)  the home screen fits the viewport
   PLANS            the pricing dialog
   DIALOGS ON A SMALL SCREEN   full-screen dialogs — LAST, see rule 3
   =========================================================================== */

/* The palette is the landing page's: neon teal into violet on near-black
   charcoal. Teal is you — and the AI, which works on your side of the glass;
   violet is the far end. The neutrals, --me and --them are landing.css's own
   values and `stylecheck.mjs` asserts they stay level with it; everything else
   here is the app's, because the landing has no modal, no meter and no
   document row to colour.

   It was a bluish charcoal with a blue/amber speaker pair. Blue and amber
   separate further round the wheel than teal and violet do, which genuinely
   mattered in the 7px meter bars, so the lanes lean on LIGHTNESS as well as
   hue to keep them apart at that size rather than on hue alone. */
:root {
  /* ---- surfaces, in the order they stack ---- */
  --sunken: #06080a;  /* below the page: inputs, meter and progress tracks */
  --bg: #090b0d;
  --panel: #141a1c;   /* cards, modals, side blocks */
  --raise: #1b2224;   /* chrome that sits on top: buttons, row hovers */
  /* Hairlines, as alpha rather than a mixed colour, which is what the landing
     uses and what makes an edge read as a fold in the surface instead of a
     drawn box. The app stacks page → panel → modal, so an alpha line resolves
     a little brighter at each depth — that is the point, not a flaw: a border
     inside a modal SHOULD be slightly stronger than the same border on the
     page behind it, because the surface under it is lighter. */
  --line: rgba(255, 255, 255, 0.09);
  --line-strong: rgba(255, 255, 255, 0.17);  /* hovered, or switched off */

  /* ---- ink ---- */
  --text: #eef2f3;
  --mute: #c2cccd;    /* body of a settled transcript line: below --text,
                         well above --dim, which would be too quiet to read */
  --dim: #98a4a8;
  /* The quietest ink that is still TEXT, and it has to stay readable, because
     what it sets is not decoration: the ME/THEM tag on every transcript line,
     the original under a translated one, the timestamp, and the sentence under
     each card in the wizard that is what makes the choice answerable.

     It was #5e696d, which measured 3.11:1 on --panel and 3.49:1 on --bg —
     under the 4.5:1 WCAG AA floor for text below 18.66px, which is every one
     of the things above. Same hue and saturation, lifted in value until it
     clears: 4.61:1 on --panel, 5.17:1 on --bg. Shared with landing.css and
     asserted by stylecheck.mjs, so it moves in both files or in neither. */
  --faint: #77858a;

  /* ---- the three capture lanes ----
     One per source, because a turn is labelled by the source it arrived on and
     one transcript can hold all three. `room` is phone mode, where both voices
     share a single microphone, so it is neither of the other two and must not
     look like either — amber puts it about as far from both as the wheel
     allows. It used to borrow --tailored, which is now the wrong colour twice
     over: that token has become blue, and before this it held exactly the
     violet --them now uses, so the far end and the room would have been the
     same colour in the same transcript. */
  --me: #38d9be;
  --them: #a78bfa;
  --room: #ffb454;

  /* ---- status ---- */
  --good: #3ddc84;
  --bad: #ff6b6b;
  /* The amber that means ATTENTION rather than a speaker: a lane gone quiet, a
     flagged turn, talking too much, a capture running, the middle light of the
     fresh/ageing/stale stamp.

     It exists because eighteen of these were riding --them, which was amber
     only by coincidence, and the palette swap turned every one of them violet
     — a live capture button and a `.state-yellow` dot in the far end's colour,
     beside a transcript full of violet Them labels. `.wizard-alert` had already
     worked around it with a literal orange and a comment saying why. **--them
     is for the far end's lane and nothing else**; if what you want is amber,
     it is this. */
  --warn: #e0a33e;

  /* What belongs to the ORGANISATION rather than to the person signed in.
     Two tabs in the Settings dialog carry it, and nothing else may: every
     other tab in that strip is about the account, and the two that are not
     had to say so on the strip itself rather than only once inside.

     Its own token rather than a borrowed one, for the reason --warn exists.
     Every hue in this file already means something — a lane, a capture, a
     refusal, a verdict — and "a different scope" is a genuinely new meaning,
     so it gets a name instead of quietly riding on somebody else's. Slate
     blue: the one region of the wheel neither speaker lane, neither traffic
     light and neither state colour has a claim on. */
  --org: #7f9cc4;

  /* ---- dark ink for a bright fill ----
     Each is its own partner's hue taken almost to black, not one neutral
     near-black: they read as the same colour turned down rather than as a hole
     punched in it. They have to move whenever their partner moves, which is
     what makes them tokens instead of five literals. */
  --on-me: #04211c;
  --on-them: #1b0f38;
  --on-good: #062117;
  --on-bad: #2b0c0c;
  --on-warn: #1a1400;

  /* A CV rewritten for this job. Blue, which --me vacated on the way to teal;
     it marks a DERIVED document and has nothing to do with a speaker, which is
     the other half of what this token used to be quietly doing. */
  --tailored: #4a9eff;

  /* The face the landing page is set in, adopted here so the wordmark is the
     same shape in both places — it is drawn in the system's own face rather
     than a loaded one, so the stack IS the mark. Held as a token because
     stylecheck.mjs can then assert the two files still agree about it. */
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;

  /* The landing's second face, and the single biggest reason the two looked
     like different products even after the palette matched. Every small label
     over there is mono, uppercase and widely tracked — the section heads, the
     nav, the buttons, the speaker tags on the transcript card, the fineprint.
     This app had no mono at all and set every label in the same sans as the
     prose, which is what made it read as a dense admin panel beside an
     editorial page. See the LABEL VOICE section below for where it goes. */
  --mono: ui-monospace, "SF Mono", "Cascadia Code", Consolas, Menlo, monospace;

  /* The BRAND's pair — the two colours in the mark and the favicon, teal into
     violet, matching landing.css's --you/--them exactly. Kept level by
     stylecheck.mjs, which is the only thing that can: the two stylesheets
     cannot import each other.

     The same two values as --me/--them above, and still their own tokens. They
     answer a different question: this pair is the product's identity, that pair
     is which side of a call a line came from. If the lanes are ever recoloured
     the mark must not follow, and having both names is what buys that.
     Nothing but the mark and the favicon may use these. */
  --brand-you: #38d9be;
  --brand-them: #a78bfa;
}

* { box-sizing: border-box; }

/* Every scrollbar in the app — the sidebar, the conversation list, a tall
   modal, the transcript — in the page's own dark palette rather than
   whatever the OS theme happens to draw by default, which on most Windows
   setups is a pale grey slab that looks like it belongs to a different
   application. `scrollbar-color`/`scrollbar-width` is the standard property
   and covers Chrome 121+ on its own; the `::-webkit-scrollbar` rules beside
   it are what the capture APIs' Chrome/Edge requirement actually needs
   supported today. Applied on * rather than named per scrollable element, so
   a container that gains overflow later is never the one exception that
   still shows the OS default. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}
*::-webkit-scrollbar { width: 11px; height: 11px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background-color: var(--line);
  border-radius: 20px;
  border: 3px solid var(--bg);
  background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover { background-color: var(--dim); }
/* Inside a modal the track is the panel colour, not the page's, or the
   3px border above would show as a thin ring of the wrong shade against it. */
.modal-card *::-webkit-scrollbar-thumb { border-color: var(--panel); }

body {
  margin: 0;
  padding: 14px 20px 20px;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.55 var(--sans);
}

/* The landing's two blooms, pinned to the viewport behind everything. They are
   most of why that page does not read as flat black, and they cost one
   composited layer that never repaints.

   Dialled well down from the landing's 0.10/0.07: that page is mostly empty and
   the light is the subject, while this one is dense and every one of these
   pixels ends up behind a transcript somebody is reading off a wall. Placed to
   the sides rather than centred for the same reason — the middle of this page
   is where the words are.

   z-index -1 puts it behind every box but still above the canvas, where --bg is
   painted. Give html a background and this layer disappears under it. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(900px 600px at 78% 12%, color-mix(in srgb, var(--me) 10%, transparent), transparent 70%),
    radial-gradient(820px 620px at 15% 88%, color-mix(in srgb, var(--them) 8%, transparent), transparent 70%);
}

/* The landing's editorial side rules, at the width this app's content actually
   uses (.layout and .topbar are both capped at 1320px). Wide screens only: on a
   narrow one they would sit on top of the content instead of beside it. */
@media (min-width: 1420px) {
  body::after {
    content: "";
    position: fixed;
    inset: 0;
    margin-inline: auto;
    width: 1360px;
    border-inline: 1px solid rgba(255, 255, 255, 0.035);
    pointer-events: none;
    z-index: -1;
  }
}

a { color: inherit; }
.hidden { display: none !important; }
.dim { color: var(--dim); font-size: 12px; }
.empty { color: var(--dim); }

/* ------------------------------------------------------------ label voice */

/* The one treatment every micro-label in the app takes, gathered here rather
   than restated per component: mono, uppercase, tracked out. It is the
   landing's `.kicker`/`.sec-head`/`.t-tag` voice, which is a single voice over
   there doing all three jobs.

   Listed as one selector list rather than a class, because these labels are
   emitted by a dozen render functions in six modules and a class would mean
   touching every one of them — and, worse, would let a new label be written
   without it. Anything that names a field, a lane, a section or a state is a
   label; the prose beside it is not.

   Every name in the list must be a selector that MATCHES something here. It is
   a list of names with nothing checking them, which is how `.wizard-card` got
   in without existing; `.settings-heading` and `.t-tag` were the next two —
   the first a class that had been removed, the second one of the landing's own,
   which this stylesheet can never see. stylecheck.mjs now walks every selector
   in this file against the templates, so a fourth cannot be added quietly.

   Tracked at 0.13em, which is where the landing sits. Mono at the same nominal
   size is WIDER than sans, so these run about 15% longer than what they
   replaced — the sidebar is 270px and the mobile bar is tighter still, so the
   sizes below are a notch down from the sans ones they replace rather than
   equal to them. */
.field > span,
.field-label,
.home-charts-heading,
.chart-card-head h3,
.usage-meter-head,
.usage-plan,
.section-head,
.panel-block > summary > span:first-child,
.turn-who {
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}

/* A heading over a block of the page — the landing's `.sec-head`, including the
   short rule that runs off the end of it. Used for "The last 30 days" and the
   chart card titles, which were plain bold sans and read as body copy that
   happened to be heavier. */
.section-head {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--dim);
}

.section-head::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--line);
}

/* ------------------------------------------------------------------ header */

.topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  max-width: 1320px;
  margin: 0 auto 14px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}

/* ------------------------------------------------------------------- mark */

/* The wordmark, the same one landing.css draws, ported rather than reinvented.
   What was here before was the Bamya Leads logo box with the second word
   changed — bordered lockup, inverted chip, Arial Black — and the landing
   abandoned that shape deliberately ("no bordered lockup, no inverted chip,
   nothing to load"). Two marks for one product meant that pressing Sign in on
   the landing page crossed into what looked like a different application.

   The rules from `.mark` down to the end of @keyframes mark-split are a
   VERBATIM copy of landing/landing.css, so the two can be diffed by eye; the
   app's own additions are gathered at the foot of this section, after the
   keyframes, rather than folded in. The one edit inside the copy is that the
   tile's bars take --brand-you/--brand-them instead of --you/--them, because
   both those names mean something else here — see the palette at the top.

   Set in the system's own face at its heaviest weight. Which face that is
   differs per platform and that is the point: it is the same voice as the rest
   of the page, since --sans is what every other word here is set in. The
   tracking is pulled in because at this weight the default spacing reads as
   three words rather than one. */
.mark {
  font-family: var(--sans);
  font-weight: 900;
  font-size: 2.03rem;
  letter-spacing: -0.045em;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
  line-height: 1;

  /* How long the mark reads "bamya" before it becomes the product — see the
     arrival, below. Four animations are keyed off it and they have to agree
     to the millisecond, since the swap is timed to land inside the first
     smear of the perpetual one; it is written here, once, and inherited. */
  --mark-hold: 2s;
}

/* The favicon's waveform tile, inline before the wordmark. Sized in em so a
   smaller mark scales it for free. The tile is pure black with no border, both
   on request; the bars take the brand pair, which are the same two colours the
   favicon hard-codes. */
.mark-icon {
  width: 0.98em;
  height: 0.98em;
  margin-right: 0.26em;
  vertical-align: -0.14em;
}

/* The letters are white and only the colon is coloured — until it swings, when
   the same split the landing's section titles carry runs through the words. It
   is built out of two dots rather than typed. It has to be built: rotating a
   real ":" would swing it around the middle of its line box, and that is
   nowhere near the middle of its two dots — they would orbit off-centre.

   An empty inline-block takes its baseline from its bottom margin edge, so the
   lower dot sits on the text baseline exactly where a typeset colon does, with
   no nudging. The side margins are what the dots need when they swing through
   horizontal: they reach 0.16em past the box either side, and the mark's
   negative tracking eats 0.045em of whatever is given here. */
.mark-colon {
  display: inline-block;
  position: relative;
  vertical-align: baseline;
  width: 0.2em;
  height: 0.52em;
  margin: 0 0.28em;
  animation: mark-turn 7s infinite;
  /* After the shorthand, which would otherwise reset these to `ease` and 0s.
     The delay is what makes the dots' FIRST sweep the arrival rather than an
     idle swing two seconds after the reader has already read the word. */
  animation-timing-function: var(--mark-spring);
  animation-delay: var(--mark-hold);
}

.mark-colon::before,
.mark-colon::after {
  content: "";
  position: absolute;
  left: 0;
  width: 0.2em;
  height: 0.2em;
  border-radius: 50%;
}

/* Violet over mint, the pair in the order the page reads it — though half a
   turn later it is the other way up, which is the point. */
.mark-colon::before { top: 0; background: var(--brand-them); }
.mark-colon::after { bottom: 0; background: var(--brand-you); }

/* Not a constant spin — a swing and a rest, twice a cycle. Each sweep is half a
   turn, which is what puts the other colour on top, so the beat is: swap,
   hold, swap back, hold.

   The swing is a damped spring, sampled — the step response of an actual
   second-order system at a damping ratio of 0.68, which is what gives it the
   one clear overshoot to 190 degrees and the small settle back that a bezier
   cannot express at all. A bezier has two control points and can only ever
   cross its target once; anything that goes past and comes back the way a
   thing with weight does needs the curve stated as samples. linear() is
   exactly the tool for that: a measured curve, written down.

   It sits on the element rather than in the keyframes, so it is written once
   and both sweeps take it. The rests override it with `linear` — a segment
   with no timing function of its own inherits this one, and the rests must not
   creep. */
.mark-colon {
  --mark-spring: linear(
    0, 0.084, 0.2686, 0.4793, 0.6719, 0.8254, 0.9346, 1.0033, 1.0398, 1.0533,
    1.0525, 1.044, 1.0328, 1.0218, 1.0125, 1.0056, 1.0011, 0.9985, 0.9973,
    0.9971, 1);
}

@keyframes mark-turn {
  0%   { transform: rotate(0deg); }
  18%  { transform: rotate(180deg); animation-timing-function: linear; }
  50%  { transform: rotate(180deg); }
  68%  { transform: rotate(360deg); animation-timing-function: linear; }
  100% { transform: rotate(360deg); }
}

/* The landing titles' split, run through the wordmark on the other axis and
   keyed to the colon: nothing at all while the dots stand vertical, widest as
   they pass through horizontal. The two words take it in OPPOSITE directions,
   so violet ends up on the outside of the mark and mint hugs the colon, and the
   whole thing pulls apart and closes again with each swing rather than shaking.

   --abx is registered, which is what makes it interpolate — an unregistered
   custom property animates in steps, not smoothly. Where @property is missing
   the var() fallback holds the mark at zero and it simply never splits. */
@property --abx {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

.mark-b {
  text-shadow: calc(var(--abx, 0) * -1em) 0 0 var(--brand-them),
               calc(var(--abx, 0) *  1em) 0 0 var(--brand-you);
}

/* "amya" takes the tail's direction rather than one of its own, so the smear
   runs on unbroken through the swap: what tears apart is what comes back. */
.mark-call,
.mark-amya {
  text-shadow: calc(var(--abx, 0) *  1em) 0 0 var(--brand-them),
               calc(var(--abx, 0) * -1em) 0 0 var(--brand-you);
}

/* Same 7s clock as the swing, started in the same style pass, so the two stay
   in step without either one knowing about the other.

   The stops are |sin(angle)| SAMPLED off the same spring the dots turn on, not
   a guessed rise and fall — they are two readings of one simulation, which is
   the only way the peak can be trusted to land on the horizontal. An
   evenly-spaced arch would peak around the middle of the sweep, by which time
   the spring has already carried the dots to 150 degrees and is on its way
   back. The late ripple is that overshoot: the dots pass 180, swing to 190 and
   settle, and the words breathe once more, faintly, as they do. linear joins
   the samples — the shape is in the numbers, so easing between them would bend
   it twice. */
.mark { animation: mark-split 7s linear infinite; }
/* After the shorthand, for the reason the colon's carries one: the arrival is
   the first beat of this clock, not a separate effect laid over it. Every mark
   on the page takes it so they stay in step. */
.mark { animation-delay: var(--mark-hold); }

@keyframes mark-split {
  0%,    50%    { --abx: 0; }
  1.5%,  51.5%  { --abx: 0.053; }   /*  36deg */
  2.2%,  52.2%  { --abx: 0.082; }   /*  65deg */
  3%,    53%    { --abx: 0.089; }   /*  98deg — dots horizontal, widest */
  3.8%,  53.8%  { --abx: 0.071; }   /* 128deg */
  4.5%,  54.5%  { --abx: 0.047; }   /* 149deg */
  6%,    56%    { --abx: 0.004; }   /* 177deg */
  7.5%,  57.5%  { --abx: 0.013; }   /* 188deg — overshoot, the second breath */
  9%,    59%    { --abx: 0.015; }   /* 189deg */
  11%,   61%    { --abx: 0.009; }   /* 185deg */
  13%,   63%    { --abx: 0.003; }   /* 182deg */
  15%,   65%    { --abx: 0; }
  18%,   68%, 100% { --abx: 0; }
}

/* The mark arrives as the company and becomes the product: "bamya" for two
   seconds, then the tail tears apart and b:call resolves out of it.

   The b is the anchor and does not move a pixel through any of it, which is
   what the absolute positioning here is for. "amya" is taken out of the flow
   entirely, so .mark-tail is ":call" wide from the first frame and the mark
   never changes width — nothing in the header reflows, and the b is held in
   place by the tile in front of it, which never moves either. Everything that
   changes is to the b's right. Animating a width instead would have shifted
   whatever sits beside it, which in the topbar is the conversation's title.

   The swap is still keyed to the perpetual animation, held back by
   --mark-hold: the dots turn in on their spring, the whole mark smears on the
   sampled arch it always smears on, and the identity changes at 0.089em — the
   peak of that arch, where there is least to see. What the two words do on top
   of that is their own, and much louder: the arch is an ambient breath at a
   twentieth of an em and this is a tear, so it is stated where it happens
   rather than by pushing the shared curve somewhere it must not go for the
   other 6.8 seconds of every cycle.

   It is a chained filter rather than a text-shadow, and that is the only
   mechanism that could carry it: the colon is two coloured BOXES, not glyphs,
   so a text-shadow reaches "call" and leaves the dots undistorted. Chained
   drop-shadows also compound — each one shadows the result of the last, so
   two functions draw four ghosts and the smear reads as a streak rather than
   two copies. It travels LEFT, into the b, which is the one thing on the mark
   that is not going anywhere.

   Both ends are filter: none, so nothing survives the arrival. A filter left
   standing at zero offsets is still a filter: it holds a stacking context and
   a rasterised layer around the wordmark for the life of the page, under a
   text-shadow that is animating on every one of those 7s cycles.

   Base state is the FINAL form: amya at zero, tail at one. A browser that runs
   no animation at all gets b:call from the first frame rather than a wordmark
   stranded on the wrong word, and so does a reduced-motion visitor — see the
   block below, where the whole thing is taken off. It has to be taken off by
   name: the arrival is a HOLD, and turning off `.mark` and `.mark-colon` does
   not reach the two elements that carry it.

   The box that POSITIONS the outgoing word cannot be the box that fades the
   incoming one, which is why there are two. opacity applies to a whole
   subtree, not to one element's own painting, so a fading .mark-tail took the
   "amya" nested inside it along — computed opacity 1, rendered nothing, the
   only frame where both had to be legible at once. .mark-tail is the box and
   never fades; .mark-form is the settled wordmark and is the thing that
   arrives. .mark-form needs no layout of its own — an inline wrapper leaves
   the colon and the word laid out exactly as they were. */
.mark-tail {
  position: relative;
  display: inline-block;
}

.mark-form {
  animation: mark-form-in 0.42s ease-out backwards;
  /* Into the back of the tear, not after it: the form is already pulling into
     focus while the word that was there is still coming apart, so the two
     overlap and it is never a cut between two still words. */
  animation-delay: calc(var(--mark-hold) + 0.16s);
}

.mark-amya {
  position: absolute;
  left: 0;
  top: 0;
  opacity: 0;
  /* Toward the b: what is left of the word folds into the letter that stays. */
  transform-origin: left center;
  animation: mark-amya-out 0.21s ease-in backwards;
  animation-delay: var(--mark-hold);
}

/* 0.21s is 3% of the 7s clock — exactly where mark-split peaks, so the last of
   the word is spent under the widest breath of the shared arch. ease-in holds
   it legible and then drops it, rather than fading from the first millisecond.

   The streak is widest at 55%, NOT at the end, and that is the whole trick of
   an exit: a filter is painted at the element's own opacity, so a smear stated
   on the last keyframe is a smear drawn at opacity zero. It has to peak while
   there is still something to smear, and what the final keyframe carries is
   only where the ghosts were heading. */
@keyframes mark-amya-out {
  0% {
    opacity: 1;
    transform: scaleX(1);
    filter: none;
  }
  55% {
    opacity: 0.5;
    transform: scaleX(0.86);
    filter: drop-shadow(-0.14em 0 0.02em var(--brand-them))
            drop-shadow(-0.29em 0 0.06em var(--brand-you));
  }
  100% {
    opacity: 0;
    transform: scaleX(0.7);
    filter: drop-shadow(-0.22em 0 0.05em var(--brand-them))
            drop-shadow(-0.46em 0 0.11em var(--brand-you));
  }
}

/* The same streak read backwards: the form arrives already torn, trailing the
   pair back toward the b, and pulls into focus. It starts at opacity 0 because
   `backwards` fill paints the first keyframe for the whole two-second hold, and
   a ghost of "call" under "bamya" is the one thing this must never show. */
@keyframes mark-form-in {
  0% {
    opacity: 0;
    filter: drop-shadow(-0.24em 0 0.05em var(--brand-them))
            drop-shadow(-0.48em 0 0.12em var(--brand-you));
  }
  30% {
    opacity: 0.7;
    filter: drop-shadow(-0.17em 0 0.04em var(--brand-them))
            drop-shadow(-0.34em 0 0.08em var(--brand-you));
  }
  65% {
    opacity: 0.94;
    filter: drop-shadow(-0.07em 0 0.02em var(--brand-them))
            drop-shadow(-0.14em 0 0.03em var(--brand-you));
  }
  100% {
    opacity: 1;
    filter: none;
  }
}

.mark.small { font-size: 1.58rem; }

/* ---- the mark while a capture is running ---- */

/* At rest the colon is a signature: a spring that swings, holds, swings back
   and holds, on a 7s clock. Inside a conversation it stops being a signature
   and becomes an instrument, with three states and nothing in between:

     not started   both dots white. There are no lanes yet, and the brand pair
                   would be claiming "your side" and "theirs" about a call that
                   is not being recorded. White is the app's own "nothing here
                   means anything yet", and it keeps every hue free to mean
                   something once one does.
     laptop        whoever is speaking turns to the top, and beams run from
                   them down to the other dot as hard as they are talking.
     phone         one microphone carries both voices, so there is no top to
                   take: both dots go amber and beam at each other together.

   Only inside a conversation. On the home screen and on the sign-in card the
   mark is a logo, the dots keep the brand pair, and none of this applies —
   `mark-idle` is what says which, and renderMarkPulse writes it.

   `mark-split` goes off with it. That animation is keyed to the same 7s clock
   as the swing — its stops are sampled off that spring — so against an
   instrument that is answering the room the words would pull apart at moments
   that no longer mean anything. */
/* `capturing` and `mark-room`, NOT `live` and `room`. This stylesheet has one
   flat class namespace and `.live` was already in it — `.live { display: flex;
   flex-direction: column }` is the capture column beside the transcript. Putting
   it on the wordmark turned the mark into a vertical flex container: "b", the
   dots and "call" each on their own line, 39px wide instead of 105. Nothing
   errors, nothing fails a check, and the header just quietly falls apart the
   moment a capture starts. Check a class name against this file before adding
   one to an element that already has others.

   This stops `mark-split` and nothing else, and deliberately does not reach the
   arrival: those animations are on .mark-amya and .mark-form. A capture can
   only start after a click, but nothing says the click cannot land inside the
   first two seconds of a page load — Quick notetaker is one press from the home
   screen — and the arrival has to finish wherever it is interrupted, or the
   wordmark is stranded reading "bamya" for the rest of the call. */
.mark.capturing { animation: none; }

/* Both dots white, and nothing moving between them. A conversation is open and
   nothing is being recorded, so there is no "your side" and no "theirs" to
   report — the brand pair here would be an accent that means nothing, which is
   the one thing this palette does not allow. The colon still swings: that is
   the signature, and the signature is not a state. */
.mark.mark-idle .mark-colon::before,
.mark.mark-idle .mark-colon::after { background: var(--text); }

/* --lane-them and --lane-me are what each lane is WEARING right now, asked once
   here and answered once by phone mode below. Three things read them — the two
   dots and the two beams — and before they existed the phone-mode override had
   to restate a background and two box-shadow recipes to change one colour.
   `mark-room` sets both to amber and everything follows. */
.mark.capturing .mark-colon {
  --lane-them: var(--them);
  --lane-me: var(--me);

  /* The colon opens to make room for what runs through it. At rest the gap
     between the dots is 0.12em — about three pixels in the header — and three
     pixels is a flicker, not a beam. This grows the box UPWARD only: an empty
     inline-block takes its baseline from its bottom margin edge, so the lower
     dot stays exactly on the text baseline where a typeset colon sits and the
     upper one lifts away from it.

     0.78em, and the ceiling is real: go past the line box's own ascent and the
     mark gets taller, which in the topbar moves a row that has a live
     transcript under it. Measured at both sizes — the mark's box is unchanged
     at 0.78 and grows at 0.85. */
  height: 0.78em;

  /* The turn, and the whole of what makes the beams legible: the box carries
     its own background round with it, so a beam that always travels from the
     ::before end to the ::after end is always travelling AWAY from whoever the
     turn has just put on top. One number orients the dots and aims both beams.

     No transition — `--turn` arrives already eased, sixty times a second, from
     an envelope in renderMarkPulse that is deliberately slower than the dots'.
     A transition on top of that smears the release rather than smoothing it,
     which is the same reason the swell below does not have one either. */
  transform: rotate(calc(var(--turn, 0) * 180deg));

  /* Two streams, one from each dot, each as bright as its own lane is loud —
     so in a laptop capture only the speaker's runs, and in phone mode, where
     both lanes carry the same microphone, both run and the dots beam at each
     other. They are drawn from --pulse-them and --pulse-me, the same two
     numbers the dots swell on, so a beam and the dot it leaves can never
     disagree about how loud a lane is. There is no third level to write.

     Painted on the box, under the dots: the dots are children, so they cover
     the ends of the channel and only the gap between them shows. */
  background-image:
    linear-gradient(to bottom,
      color-mix(in srgb, var(--lane-them) calc(var(--pulse-them, 0) * 100%), transparent)
        0 0.07em, transparent 0.07em 0.15em),
    linear-gradient(to bottom,
      color-mix(in srgb, var(--lane-me) calc(var(--pulse-me, 0) * 100%), transparent)
        0 0.07em, transparent 0.07em 0.15em);
  background-size: 100% 0.15em;
  background-repeat: repeat-y;

  animation: mark-beam 0.3s linear infinite;
  /* .mark-colon sets the spring by name AND a two-second delay after its own
     shorthand — the arrival's — and both would otherwise still apply here. The
     shorthand above already clears the delay; this is the timing function. */
  animation-timing-function: linear;
}

/* One tile down and one tile up, so both loops are seamless and the two streams
   run against each other. The tile is the unit: travelling exactly one period
   means the frame after the last is identical to the first. */
@keyframes mark-beam {
  to { background-position: 0 0.15em, 0 -0.15em; }
}

/* --pulse-me and --pulse-them are written on the mark each frame by
   renderMarkPulse, 0 to 1 per lane, already smoothed. They are read here rather
   than the dots being styled from JS because the dots are ::before/::after and
   have no node to write to — which is also why they are worth keeping as
   pseudo-elements: one element, two indicators, no extra markup in the header.

   No `transition`. The value is replaced sixty times a second by an envelope
   that already has an attack and a release, and a transition on top of that
   would smear the release into a lag rather than smoothing anything.

   The lane tokens, not the brand pair. While live these dots are reporting
   which side is speaking, so they say what the meters and the turn labels say;
   at rest they go back to being --brand-you/--brand-them. The two pairs hold
   the same colours today, which is exactly why the distinction is worth writing
   down rather than discovering later. */
.mark.capturing .mark-colon::before {
  background: var(--lane-them);
  transform: scale(calc(1 + var(--pulse-them, 0) * 0.45));
  box-shadow: 0 0 0 calc(var(--pulse-them, 0) * 0.07em)
              color-mix(in srgb, var(--lane-them) 40%, transparent);
}

.mark.capturing .mark-colon::after {
  background: var(--lane-me);
  transform: scale(calc(1 + var(--pulse-me, 0) * 0.45));
  box-shadow: 0 0 0 calc(var(--pulse-me, 0) * 0.07em)
              color-mix(in srgb, var(--lane-me) 40%, transparent);
}

/* Phone mode: one microphone carries both voices, so there is no "each half" to
   report and nothing for the turn to point at. Both dots take the room lane's
   amber, both answer to the same level, and both beams run — which is the
   honest picture of a capture that cannot tell the two sides apart in the first
   place. renderMarkPulse writes the room lane's envelope into both properties
   and settles the turn level, so the rules above still do all the moving and
   only these two colours change. */
.mark.capturing.mark-room .mark-colon {
  --lane-them: var(--room);
  --lane-me: var(--room);
}

/* The colon opens and closes rather than jumping, at both ends of a call. This
   is the one thing about the mark that IS a transition: height changes once,
   when a class does, where every other value here is replaced sixty times a
   second by an envelope that already has its own attack and release. It cannot
   live in the rule above — that block is the landing's verbatim copy, and the
   landing's colon never opens because nothing there is being recorded. */
.mark-colon { transition: height 0.28s ease; }

@media (prefers-reduced-motion: reduce) {
  .mark-colon { transition: none; }
}

/* ---- the app's own, below here; everything above is the landing's copy ---- */

/* The mark swings here too, the same as on the landing and on the gate. It was
   held still in the header for a while on the theory that a logo moving every
   seven seconds beside a live transcript is a distraction — the answer is that
   the mark is the product's signature and it should behave the same wherever it
   appears. `prefers-reduced-motion` below is what turns it off for anybody who
   has asked for that, which is the right place for the question to be settled.

   The header's mark also sits in a flex row that would otherwise stretch it. */
.topbar .mark { flex: 0 0 auto; }

/* Nothing in this app had a reduced-motion answer before the mark arrived, and
   the mark is the first thing here that moves without being asked to. Scoped to
   it rather than the blanket `*` rule the landing uses: the spinners and the
   capture sweep elsewhere in this file are feedback about something actually
   happening, and stopping those says the app has hung. */
@media (prefers-reduced-motion: reduce) {
  .mark,
  .mark-colon,
  /* Both halves of the arrival, by name. Turning off `.mark` and `.mark-colon`
     stops the swing and the split and does not reach these two at all, and the
     failure is the loud one: the hold is a DELAY, so what would survive is the
     wordmark sitting on "bamya" and then cutting to b:call. Base state is the
     settled form, so taking the name off is the whole fix. */
  .mark-amya,
  .mark-form,
  /* Including the capture spin. renderMarkPulse asks the same question before
     it writes a level, so the dots hold still too — a scale driven from JS is
     not an animation and this block could not reach it. What survives is the
     part that carries the information rather than the motion: the dots still
     take their lanes' colours, and phone mode still turns them both amber. */
  .mark.capturing .mark-colon { animation: none; }
}
/* There is a SECOND reduced-motion block, near the foot of this file, for the
   narrow header's capture halo. It has to be down there rather than in here:
   the rule it turns off lives inside a `max-width` query further down, and an
   equally specific selector written up here would lose on file order. */

/* The sign-in card is a flex column, so the <h1> carrying the mark would
   stretch its box the full width. Shrink it back to the mark. */
.gate-card .mark { align-self: flex-start; }

.title-input {
  flex: 1;
  min-width: 0;
  font-size: 21px;
  font-weight: 600;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 7px;
  padding: 5px 8px;
  color: var(--text);
}

/* The name as a heading. Same size and weight the input had, so moving it into
   the dialog changed where you edit it and not how the page reads. */
.title-text {
  flex: 1;
  min-width: 0;
  font-size: 21px;
  font-weight: 600;
  padding: 5px 8px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The name field is an ordinary bordered box rather than the borderless heading
   it once was. It has now been a dialog, a sidebar panel and a dialog again —
   `#session-modal`, `#session-block`, `#convsettings-modal` — and the rule has
   been the same one every time, which is the tell that it belongs to the FIELD
   rather than to whatever is holding it. */
#convsettings-modal .title-input { border-color: var(--line); }

.title-input:hover:not(:disabled) { border-color: var(--line); }
.title-input:focus { border-color: var(--me); outline: none; background: var(--sunken); }
.title-input:disabled { opacity: 1; color: var(--faint); }

.banner {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  border-radius: 9px;
  padding: 11px 14px;
  margin: 0 auto 14px;
  max-width: 1320px;
  font-size: 13.5px;
  /* Red by default, amber for a refusal or a loss, --me when it is genuinely
     only telling you something. See .banner-info and .banner-warn below: the
     kind is the token and nothing else changes. */
  --tint: var(--bad);
  border: 1px solid var(--tint);
  background: color-mix(in srgb, var(--tint) 12%, transparent);
}

.banner-text { flex: 1; min-width: 0; }

.banner-close {
  flex: 0 0 auto;
  background: none;
  border: 0;
  color: inherit;
  opacity: 0.6;
  font-size: 18px;
  line-height: 1;
  padding: 0 2px;
  cursor: pointer;
}

.banner-close:hover { opacity: 1; }

.banner-info { --tint: var(--me); }

/* The middle tone: a refusal or a cancellation, which is neither a fault nor
   reassuring. `--me` used to carry everything that was not a fault, which put a
   cancelled capture in the same colour as "Template saved" — and in the colour
   that means YOUR LANE everywhere else on the page. Which message gets which
   tone is decided in `showBanner`, and the taxonomy is written down there rather
   than twice. */
.banner-warn { --tint: var(--warn); }

/* Parked into the header alongside `#echo-gate`, by the same code and for the
   same reason: an alert about an echo happening RIGHT NOW must survive being
   scrolled past, which a page-level banner above a scrolling transcript
   cannot promise once the header goes sticky — the short captions window — or
   the page runs past one screen — narrow. `margin: 0 auto 14px` and
   `max-width: 1320px` above were written for a banner centred under the
   header as a block of its own; as a flex item in the header row the auto
   margin does nothing useful and the cap is moot, so both are reset. */
.compact-echo-banner {
  flex: 1 0 100%;
  max-width: none;
  margin: 6px 0 0;
}

/* ------------------------------------------------------------------ layout */

.layout {
  display: grid;
  grid-template-columns: 270px minmax(0, 1fr);
  gap: 24px;
  align-items: start;
  max-width: 1320px;
  margin-inline: auto;
}

/* Collapsed, on a conversation: the history takes almost the whole page, but a
   thin rail stays so the toggle that reopens it has somewhere to live. The
   toggle sits INSIDE .sidebar now (the usual retract/expand icon, attached to
   the thing it controls), so the sidebar itself cannot go to display:none the
   way it used to — that would take the only way back to expanding it with it.
   #controls is what actually holds everything else, so hiding that alone
   leaves exactly the rail. */
.layout.sidebar-hidden { grid-template-columns: 34px minmax(0, 1fr); }
.layout.sidebar-hidden .sidebar #controls { display: none; }

/* The home screen, where there is no conversation to show controls for at all
   — not collapsed, not a thin rail, no sidebar. Written after sidebar-hidden
   above so it wins the cascade on the rare frame both classes are present
   (route() also takes care not to combine them). */
.layout.no-sidebar { grid-template-columns: minmax(0, 1fr); }
.layout.no-sidebar .sidebar { display: none; }

.icon-toggle {
  flex: 0 0 auto;
  padding: 6px 10px;
  font-size: 14px;
  line-height: 1;
  color: var(--dim);
}

.icon-toggle.on { color: var(--me); border-color: var(--me); }

.sidebar {
  position: sticky;
  top: 14px;
  max-height: calc(100vh - 28px);
  overflow-y: auto;
  font-size: 13px;
}

/* The retract/expand control, living on the sidebar itself rather than in the
   header. Full width of whatever the sidebar currently is, so it reads as a
   bar across the top when expanded and fills the 34px rail when collapsed —
   the one thing still visible there. */
.sidebar-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  margin-bottom: 8px;
  padding: 6px 0;
}
.sidebar-toggle svg { width: 17px; height: 17px; }
/* The "reversed" icon for reopening — the same glyph mirrored, rather than a
   second one drawn by hand. .on is applied exactly when the sidebar is
   collapsed (see renderSidebar in app.js), which is exactly when the chevron
   should point the other way. */
.sidebar-toggle.on svg { transform: scaleX(-1); }

.side-block {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px;
}

/* ------------------------------------------------------------------ inputs */

/* Every text-like input, listed by type because a bare `input` would take the
   checkboxes and the file pickers with it. `email` and `password` are here
   because they exist — they are the two fields on the sign-in page, they were
   the two this list forgot, and a type nothing styles falls all the way back to
   the browser's own control: a white box with dark text, on a dark page,
   directly under the wordmark. Nothing else in the app uses either type, which
   is why it survived so long.

   `url` and `datetime-local` are here for the second time it happened, in the
   Conversation settings dialog: three fields in a row rendering as bare browser
   controls, square and outlined in white, in a dialog where everything around
   them was a soft dark box. **Adding an input of a type not on this list means
   adding it to this list** — that is the whole rule, and this comment is now the
   second time it has been paid for. */
input[type="text"], input[type="search"], input[type="email"],
input[type="password"], input[type="number"], input[type="url"],
input[type="datetime-local"], input[type="date"], input[type="time"],
textarea, select {
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--text);
  padding: 7px 9px;
  font: inherit;
  width: 100%;
}

textarea { resize: vertical; line-height: 1.45; font-size: 12.5px; }
select { cursor: pointer; }

/* The parts of a native control the stylesheet cannot reach: the date picker's
   own popup, its calendar glyph, the scrollbars. `color-scheme` is the one
   property that tells the browser which way round this page is, and without it
   Chrome draws that glyph as dark-on-dark and opens a white calendar over a
   black dialog. It belongs on the input rather than on `:root` only because
   nothing else here has a native widget to render — but it is cheap and
   correct on both, and the two selectors below are what the date field needs.
   `opacity` on the indicator because `color-scheme` alone leaves it at the
   browser's own contrast, which is louder than every other glyph in the app. */
input[type="datetime-local"], input[type="date"], input[type="time"] {
  color-scheme: dark;
}
input[type="datetime-local"]::-webkit-calendar-picker-indicator,
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
  opacity: 0.55;
  cursor: pointer;
}
input[type="datetime-local"]::-webkit-calendar-picker-indicator:hover,
input[type="date"]::-webkit-calendar-picker-indicator:hover,
input[type="time"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }

input:disabled { opacity: 0.55; }

/* A pill, mono, uppercase and tracked — the landing's `.cta`, at the size a
   working tool can carry. It was a 6px-radius sans rectangle, which is the
   other half of why the app read as an admin panel: the buttons and the body
   copy were the same voice at the same weight, so nothing on the page looked
   like a control until you moused it.

   `text-transform` here rather than in the markup, so the labels stay written
   in sentence case in the templates and in the render functions — several are
   built from a mode's declared strings, which are prose and must not be
   shouted at their source. */
/* GHOST is the default and SOLID WHITE is the emphasis, exactly as on the
   landing — `.cta.ghost` is a hairline round transparency and `.cta.solid` is
   --text filled with --bg ink. Neither is an accent colour.

   That is the rule this app had backwards. An accent means something here: which
   lane spoke, whether a capture is running, whether the checklist item is
   covered. Spending teal on "the button that makes a thing" and green on "+ New"
   left the page shouting in four colours at once and, worse, made the colours
   that carry a meaning indistinguishable from the ones that carry none. */
/* `.header-button-link` is named here because it exists for something that is
   NOT a <button> — an <a> that has to pass for one, which is what Settings used
   to be. Naming it keeps that true without a second copy of the pill. */
button,
.header-button-link {
  background: transparent;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  color: var(--text);
  padding: 7px 15px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}

button:hover:not(:disabled),
.header-button-link:hover { border-color: var(--text); }
button:active:not(:disabled) { transform: translateY(1px); }
button:disabled { opacity: 0.4; cursor: default; }
button.danger { color: var(--bad); }

/* The one emphasised control on a screen. White, not teal: it is the most
   contrast available against this background, so it needs no hue to be the
   loudest thing on the page — which leaves every hue free to mean something. */
button.primary {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}

button.primary:hover:not(:disabled) { background: #fff; border-color: #fff; }

/* The exceptions, and they are all the same exception: a control whose label is
   somebody's PROSE rather than a fixed piece of chrome. A conversation title, a
   category name, a mode's own description and a document's filename are content
   that happens to sit in a button, and uppercase mono would both mangle them
   and, at 0.08em tracking, overflow the 270px sidebar.

   `.mode-menu button` and `.category-row button` used to be in this list and
   named nothing: both of those are IDS, and both elements carry `advice-menu`
   as their class — so `.advice-menu button` below was already covering them
   twice over. */
.row-category-button,
.doc-button,
.advice-menu button,
.conversation-row button,
/* A template's name is what somebody typed, exactly like a conversation title
   or a category name — so it takes the prose exception rather than the pill's
   mono uppercase. Without it a template called "Pipeline chase" was shouted
   back as PIPELINE CHASE, which is the mistake `.wizard-card` made with every
   mode's description. */
.template-pick,
.wizard-choice {
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
}

.icon-button {
  background: transparent;
  border: 0;
  font-size: 22px;
  line-height: 1;
  padding: 0 4px;
  color: var(--dim);
}

.pill {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--dim);
  white-space: nowrap;
}

/* Sign out is a POST with a CSRF token, so it is a form rather than a link.
   The wrapper must not disturb the row it sits in. */
.signout {
  display: inline-flex;
  margin: 0;
}

.signout .pill {
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: 11px;
}


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

.modal {
  position: fixed;
  inset: 0;
  background: rgba(8, 10, 13, 0.72);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 8vh 20px 20px;
  z-index: 20;
}

/* A ceiling and somewhere for the overflow to go, together — see the note on
   .modal-tall below, which diagnosed this on the wizard and then fixed only
   itself. A flex column with a max-height and neither of these is two bugs at
   once: the children shrink to make room, so their content overlaps inside
   boxes too short for it, and whatever still does not fit hangs out of the
   bottom onto a background that is not the card.

   It reached the Conversation settings dialog first, which was the tallest of
   these — nine rows plus a paragraph, about 560px for an account that belongs to
   an organisation, against 78vh. That wants a ~720px viewport; a 1366x768 laptop
   gives about 640, and any window that is not maximised gives less. That dialog
   is a dialog again, taller than ever — it holds everything that panel did as
   well as everything it held before — so it is both the worst case and the
   reason this rule is written on `.modal-card` rather than on one variant.

   Both halves belong here rather than on each variant: every one of them is a
   .modal-card first, and the ones that set their own ceiling (below) were
   inheriting the ceiling and not the escape. */
.modal-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 16px;
  width: 100%;
  max-width: 540px;
  max-height: 78vh;
  display: flex;
  flex-direction: column;
  gap: 12px;
  overflow-y: auto;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
}

/* Nothing in a dialog is allowed to be squeezed on the way to scrolling. A
   flex item's default is flex-shrink: 1, so without this the card scrolls only
   AFTER its children have been compressed past their content — which is the
   half of the .modal-tall fix that reads as optional and is not. EVERY card,
   including the ones that set their own ceiling by id — a variant must not
   restate this. */
.modal-card > * { flex-shrink: 0; }

/* The one child with a margin of its own. `.modal-card` spaces its children
   with `gap`, and a <p>'s default 1em top and bottom add to it — 36px where
   every other gap in the same dialog is 12. */
.modal-card > p { margin: 0; }

/* Double the ordinary dialog's width, for the one modal that browses many
   rows at once and wants room for a category chip and a capture-type icon
   beside each. */
/* Filling most of the viewport in both directions now, not just wider than an
   ordinary dialog: this is the one screen that browses everything you have
   recorded, and it wants the room in height as much as in width. vw/vh rather
   than a fixed px ceiling, so it scales with the window instead of stopping
   short on a large one or overflowing a small one. */
.modal-wide {
  max-width: 95vw;
  width: 1320px;
  max-height: 92vh;
}

/* #modal is the Conversations modal specifically, not the shared .modal
   class every dialog uses — it overlays the header too (.modal is already
   `position: fixed; inset: 0`, so it sits above everything on the page,
   header included) but the shared 8vh top padding left a gap above the card
   as if the header beneath still needed to show through. It does not: this
   is the one modal that reads as its own screen, so it starts at the top of
   the screen instead, with the same margin the other three sides keep. */
#modal { padding-top: 20px; }

/* No heading in this one modal — see the comment in index.html — so the close
   button is the only thing in its head row, and flex-end is what keeps it in
   its usual top-right corner rather than sliding to the left the way a single
   flex child does under .modal-head's ordinary space-between. */
/* Floated over the card's own corner rather than sitting in a reserved
   .modal-head row — see the comment in index.html. position: relative on the
   card is what makes this positioning context its own rather than the page's. */
.modal-wide { position: relative; }
.modal-close-float { position: absolute; top: 14px; right: 14px; z-index: 1; }

/* The mode and category filter rows can run to six or more pills; unlike the
   single-row tab strips elsewhere, these are allowed to wrap onto a second
   line rather than overflow the card. */
.modal-filter-tabs { flex-wrap: wrap; }

/* Lets a pill group's buttons act as direct children of #modal-filter-row —
   flex items of ITS row — rather than of a nested row of their own, so the
   square toggle button and whichever set of pills is active read as one
   line. `.hidden`'s `!important` still wins over this when a group is the
   inactive one, `display: contents` only applies to the visible group. */
.filter-pills { display: contents; }

/* One button, two icons: [data-filter-axis] on the button itself picks which
   shows, so the icon names the axis rather than a word beside it needing to.
   A square icon-toggle, not another pair of text pills, so switching axis
   costs no more room in the row than any other click there. */
.filter-axis-toggle svg { width: 17px; height: 17px; vertical-align: middle; }
.filter-axis-toggle .icon-category { display: none; }
.filter-axis-toggle[data-filter-axis="category"] .icon-mode { display: none; }
.filter-axis-toggle[data-filter-axis="category"] .icon-category { display: block; }

.modal-head { display: flex; align-items: center; justify-content: space-between; }
.modal-head h2 { margin: 0; font-size: 15px; }

/* A column now that this lives in its own narrow modal rather than inline in
   the wide Conversations one — the title, the picker and Create used to sit
   in a row because there was width to spare for it. */
.new-conversation { display: flex; flex-direction: column; gap: 10px; }

/* flex: 1 is what actually uses the taller modal-wide card: without it the
   list stays its own content height and the extra space below it sits empty
   rather than being filled with more rows before scrolling starts.
   min-height: 0 overrides a flex item's default of never shrinking below its
   content size, which is what lets overflow-y actually scroll inside a
   bounded card instead of pushing the card itself past max-height. */
.conversation-list { overflow-y: auto; display: flex; flex-direction: column; gap: 2px; flex: 1; min-height: 0; }
.conversation-list .empty { padding: 8px 2px; font-size: 13px; }

.conversation-row {
  display: flex;
  align-items: center;
  gap: 6px;
  border-radius: 8px;
  border-left: 2px solid transparent;
}

/* Title (with its badges), category, date and turns as four aligned columns
   across every row — no header row naming them, since the modal is wide
   enough now that what each column holds reads for itself at a glance
   without one. */
.conversation-link {
  display: grid;
  /* Title, category, date, length. Four, because the length REPLACED the turn
     count rather than joining it — see buildConversationRows. Each is its own
     column so they line up down the list; the same facts joined by middle dots
     read as one sentence nobody scans. */
  grid-template-columns: minmax(0, 1fr) 150px 190px 150px;
  align-items: center;
  gap: 4px 16px;
  flex: 1;
  min-width: 0;
  padding: 9px 10px;
  text-decoration: none;
}

.conversation-row:hover { background: var(--raise); }
.conversation-row.active { background: var(--raise); border-left-color: var(--me); }

/* Being recorded RIGHT NOW — by this account on another screen, or by a
   colleague whose conversation is shared here.

   One of the few things allowed a colour of its own: "a capture that is
   running" is on the short list at the top of this file, beside a speaker lane
   and the fresh/ageing/stale stamp. `--good` and not `--warn`, because amber on
   this page means a refusal or something needing attention, and a call in
   progress is neither — it is the healthy state, and the one row in a long list
   worth spotting without reading.

   The left border rather than a background: `.active` already owns the
   background here (which row is open is a different question from which row is
   live, and both can be true of one row), so they stack instead of fighting.
   `.active` still wins the border on the row you are actually in, being written
   below — that row is on screen already and needs no finding. */
.conversation-row.live-now { border-left-color: var(--good); }

.conversation-live {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--good);
  /* The same halo the Record button and the follow strip's dot use, in this
     one's own colour — see `--halo` on the keyframe. A box-shadow costs no
     layout, so it cannot nudge a title around once a second. */
  --halo: var(--good);
  animation: capture-halo 1.6s ease-out infinite;
}

/* Said to a screen reader, drawn for nobody. The dot carries this to anybody
   who can see it and carries nothing at all to anybody who cannot, and a
   `title` on a decorative span is not reliably announced. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Both carry their own colour and an outer border at rest now, rather than
   only gaining one on hover — they still only APPEAR on hover (a list of
   conversations stays a list rather than a toolbar), but once shown they
   need to read as clearly as any other coloured control, not as two faint
   grey marks somebody has to be looking for. */
.row-delete {
  background: color-mix(in srgb, var(--bad) 10%, transparent);
  border: 1px solid var(--bad);
  color: var(--bad);
  font-weight: 700;
  font-size: 17px;
  line-height: 1;
  padding: 4px 9px;
  border-radius: 6px;
}

.row-duplicate {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--dim);
  font-weight: 700;
  font-size: 14px;
  line-height: 1;
  padding: 5px 7px;
  border-radius: 6px;
}

/* A third colour, neither the danger red nor the duplicate orange: this one
   changes nothing about what the row is, only how it behaves. */
.row-settings {
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--dim);
  display: flex;
  align-items: center;
  padding: 5px 7px;
  border-radius: 6px;
}
.row-settings svg { width: 14px; height: 14px; }

/* The three actions are drawn at rest, not revealed by pointing at the row.
   They used to be `opacity: 0` until the row was hovered, and the row's grid
   already reserves their width — so nothing moved when they appeared, which
   means the hiding bought no layout and cost the whole of their discoverability:
   settings, duplicate and delete had no other door from this list, and somebody
   who never happened to rest the pointer on a row never learned they existed.
   What tells them apart is still hover, and that is what hover is for — the
   tint below says WHICH action is under the pointer, rather than that there are
   any. */
.row-delete:hover { background: color-mix(in srgb, var(--bad) 22%, transparent); }
.row-duplicate:hover { background: color-mix(in srgb, var(--warn) 24%, transparent); }
.row-settings:hover { background: color-mix(in srgb, var(--me) 24%, transparent); }

/* Title, capture-type icon and mode badge on one line, rather than the icon
   and badge trailing at the far end of the whole row the way they used to. */
.conversation-title-row {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

/* Its own grid column — see buildConversationRows, which always creates this
   cell, empty for a row with no category, so date and turns land in the same
   column on every row rather than sliding left when there is nothing here. */
.conversation-category-col {
  display: flex;
  min-width: 0;
}

.conversation-title {
  display: block;
  flex: 0 1 auto;
  /* A flex item's default min-width is its content size, which defeats the
     ellipsis below — this is what actually lets the title truncate rather
     than pushing the icon and badges off the row. */
  min-width: 0;
  font-weight: 600;
  font-size: 13.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mode-badge {
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: 20px;
  border: 1px solid var(--line);
  color: var(--dim);
  font-size: 11px;
  white-space: nowrap;
}

.conversation-row.active .conversation-title { color: #fff; }
.conversation-date,
.conversation-length {
  display: block;
  color: var(--faint);
  font-size: 11.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A date that has not happened yet: a time somebody put in the diary rather
   than a record of a call. Amber, which on this page means exactly this — not
   a fault, not a confirmation, something still outstanding — and it is gone the
   moment there is a real date to show instead, because `conversationWhen`
   stops returning it. The `title` on the element says the same thing in words:
   a colour alone is not a fact anybody can read. */
.conversation-date.scheduled { color: var(--warn); }

/* A `@media (hover: none)` block stood here giving a touch screen the three row
   actions permanently, because hover-to-reveal meant they never appeared at all
   there — settings, duplicate and delete were unreachable with no other door to
   any of them from the list. Every pointer gets that answer now, so the
   exception has nothing left to except: see `.row-delete` above. Worth keeping
   in mind that the fix for one input was the right answer for all of them. */

/* The list on a phone. Those four columns need about 538px before the row's
   own buttons, so at 390px the title column — `minmax(0, 1fr)`, the only one
   that can give — collapsed to nothing: the titles rendered at zero width
   underneath their own badges, and the list grew a horizontal scrollbar. The
   same four cells become two lines instead, title above and the three pieces
   of metadata below it, which is the shape they were in before the columns
   were introduced for a wide modal. */
@media (max-width: 700px) {
  /* Three lines: the title, then what the conversation IS (capture type,
     mode, category), then when it was and how much of it there is. Two was
     one too few — with the row's three buttons taking 110px of a 350px
     screen, the line under the title had 194px for five things, and the
     chip's `1fr` column was the one that rounded to nothing: it collapsed to
     zero width and painted its own text over the date beside it.

     Placed explicitly below rather than left to auto-flow, since it was
     auto-flow that put all five on one line. Column 1 is the capture icon's
     width, column 2 the badge's, and column 3 takes the slack, which the
     chip and the turns count get a line of each. */
  .conversation-link {
    grid-template-columns: auto auto minmax(0, 1fr);
    gap: 3px 8px;
    padding: 9px 8px;
  }

  /* Dissolved, so its three children become grid items of the row itself.
     That is what lets the title have a line of its own with the badge on
     the line below: as a flex row they shared 194px, and the title — the
     one thing that says which conversation this is — was losing, truncated
     to "Screening ..." beside a badge at full width. There is no DOM order
     to change here (the row is built once, for both this and the wide
     modal), so the layout has to be what moves. */
  .conversation-title-row { display: contents; }

  .conversation-title { grid-area: 1 / 1 / 2 / -1; }
  .row-capture-icon { grid-area: 2 / 1; }
  .mode-badge { grid-area: 2 / 2; }
  .conversation-category-col { grid-area: 2 / 3; }
  .conversation-date { grid-area: 3 / 1 / 4 / 3; }
  .conversation-length { grid-area: 3 / 3; }
  .conversation-date,
  .conversation-length { font-size: 11px; }

  /* Permanently visible here (see the hover: none rule above), so their size
     is width taken from the row rather than width borrowed on hover. */
  .row-delete { font-size: 15px; padding: 3px 7px; }
  .row-duplicate, .row-settings { padding: 4px 6px; }

  /* `flex: 0 0 auto` keeps the chip at its content width, which is right in a
     150px column and wrong in one that can now fall to 50px: it simply
     overflowed into the date beside it, printing one over the other. It can
     shrink here, and the ellipsis it already carries does the rest. */
  .conversation-category-col .category-chip,
  .conversation-category-col .row-category-wrap { flex: 0 1 auto; min-width: 0; }

  /* Wraps rather than ellipsising, now that it has the width to make that
     worth doing: most titles fit on one line here, and a long one spends a
     second line — height a phone has — instead of being cut off. Clamped at
     two so one very long title cannot push the rows apart. */
  .conversation-title {
    white-space: normal;
    overflow-wrap: anywhere;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }

  /* Able to give way, so the row can never overflow the way the four fixed
     columns did: the ellipsis it gains is a last resort rather than, as it
     was a moment ago, what every badge on the screen showed. */
  .mode-badge {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 10px;
    padding: 2px 6px;
  }
}

/* Siblings of the link in the row's flex layout, not text inside it — see
   buildConversationRows. Both fade in the row's own colour, matching the
   plain dingbat style every other icon in this app uses. */
.row-capture-icon {
  flex: 0 0 auto;
  display: flex;
  color: var(--faint);
  opacity: 0.75;
}
.row-capture-icon svg { width: 14px; height: 14px; }

.category-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 120px;
  padding: 2px 8px;
  border-radius: 20px;
  border: 1px solid var(--line);
  color: var(--dim);
  font-size: 11px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The category column as a picker: .advice-wrap already gives it
   `position: relative`, which is all `.advice-menu`'s own absolute
   positioning needs — one of these per row, each self-contained, rather than
   a single floating menu repositioned per click. */
.row-category-wrap { min-width: 0; max-width: 100%; }
.row-category-button {
  display: flex;
  align-items: center;
  min-width: 0;
  max-width: 100%;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 20px;
  padding: 1px;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.row-category-button .dim {
  padding: 2px 8px;
  font-size: 11px;
}
.row-category-button:hover,
.row-category-button:focus-visible { border-color: var(--line); }
.row-category-wrap .advice-menu { min-width: 168px; }

/* ---------------------------------------------------------------- controls */

.controls-block { display: flex; flex-direction: column; gap: 11px; }

/* Empty unless a flush failed, so it takes no room when all is well. */
.save-state:empty { display: none; }
.save-state { font-size: 11px; color: var(--bad); }

.release-button { align-self: flex-start; font-size: 11px; padding: 4px 9px; }

/* THE PICKER IS THE METER.

   Three level bars stood in the Audio panel, one per lane, and what replaced
   them is the two controls that choose the sources: the microphone picker
   pulses with the microphone's lane and the capture-mode picker with the far
   end's. A meter is a second object drawing a number the control could draw
   itself, and being behind a fold it made "is it hearing me" a question you had
   to open something to answer.

   `--level` is written by `tick` in app.js, 0 to 1, already through `laneScale`
   — so this and the wordmark's dots and the pre-flight's bars are all reading
   the same curve. A `box-shadow` and a border tint, so it costs no layout and
   cannot shove the row about: the same reason the Record button's halo is one.

   It is not an animation. There is nothing to time — the level IS the frame
   rate — so the only easing is a short transition, which stops a lane that
   drops between syllables from strobing. The wordmark's dots take the slow
   release instead; here the row is 270px away from three other controls and a
   slow decay would leave two buttons glowing at each other.

   `--level` defaults to 0 on the button itself, so a picker that is never
   pulsed (a camera mode, a template, the home screen) has no state to be left
   in: the shadow resolves to nothing rather than to `initial`. */
.lane-pulse {
  --level: 0;
  --pulse: var(--me);
  box-shadow: 0 0 0 calc(var(--level) * 4px)
    color-mix(in srgb, var(--pulse) calc(var(--level) * 45%), transparent);
  transition: box-shadow 90ms linear, border-color 90ms linear;
}
/* The far end's own colour on the control that governs it, so the two pulses
   say which lane they are about without a word — the same pair the transcript
   labels its lines with. */
#btn-capture-mode.lane-pulse { --pulse: var(--them); }

/* A device NAME is the whole answer this menu gives, and the operating system
   writes it: "Default - Headset (Realtek(R) Audio)" is 34 characters before
   anybody has plugged anything unusual in, against a menu the sidebar holds to
   about 214px. Every other menu here holds a label somebody here chose and can
   keep short, so `nowrap` — inherited from `button` — is right for those and
   clips this one. The button itself still ellipses: it is one row of a column
   that cannot grow, where the menu is an overlay that can. */
#mic-menu button { white-space: normal; }
#mic-menu strong { overflow-wrap: anywhere; }

.field { display: flex; flex-direction: column; gap: 4px; }
.field > span { color: var(--dim); }
.field small, .check small { color: var(--faint); font-size: 11px; line-height: 1.35; }

/* A field name that stacks above its control rather than sitting beside it, in
   the two places that happens. `.field-label` is for a setting whose control is
   a toggle rather than a labelled input — .field wraps one <label> around both
   the name and the control; a toggle row is two elements side by side instead
   (see .setting > .toggle in the Settings modal), so the name needs a class of
   its own rather than a nested descendant selector. The Conversation panel's
   span wants the same three, and a selector list keeps each at its own
   specificity. */
.field-label { display: block; margin-bottom: 4px; color: var(--dim); }

.check { display: grid; grid-template-columns: auto 1fr; gap: 1px 7px; align-items: center; font-size: 12.5px; }
.check small { grid-column: 2; }

.option-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.option-row .check { flex: 1; }
.option-select { width: auto; flex: 1; min-width: 0; font-size: 12.5px; padding: 5px 8px; }


/* --------------------------------------------------------- interview mode */

/* Same container as the interview block, but folded away: the microphone and
   its meters are set once and then only looked at when something is wrong. */
.panel-block {
  padding: 0 11px;
  margin: 2px -2px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.02);
}

.panel-block > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 9px 0;
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  list-style: none;
}

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

/* A caret that turns, rather than the browser's default triangle. */
.panel-block > summary > span:first-child::before {
  content: '';
  display: inline-block;
  width: 0;
  height: 0;
  margin-right: 7px;
  vertical-align: 1px;
  border: 4px solid transparent;
  border-left-color: var(--dim);
  transition: transform 0.15s ease;
}

.panel-block[open] > summary > span:first-child::before {
  transform: rotate(90deg) translateX(-1px);
}

.panel-block > summary .dim { font-weight: 400; font-size: 11px; color: var(--faint); }
.panel-block > *:last-child { padding-bottom: 11px; }
.panel-block .field { margin-bottom: 10px; }

/* One container for every document row, interview or not, so they read as one
   list rather than as two groups that happen to look alike. It is a <details>,
   sharing .panel-block's summary/caret treatment below — hence the same box
   model .panel-block uses (horizontal padding only, so the summary sits flush
   against the top border) rather than the padding on every side a plain box
   would want.

   The tint was shared with Audio, Actions and Conversation, which were the rest
   of one accordion group and read as one thing because of it. This is the only
   panel left, so the tint is now simply what a panel looks like — and it stays
   an ID selector rather than moving onto `.panel-block`, which would fold a
   second panel into the treatment before anybody had decided it should have
   it. */
#docs-block {
  border: 1px solid color-mix(in srgb, var(--me) 35%, transparent);
  background: color-mix(in srgb, var(--me) 6%, transparent);
}

/* Both take a flex column with a gap between rows. `#controls > .option-row`
   gives the same rhythm to the rows that sit directly in the sidebar, and a row
   nested inside a panel is not one of those — without this a panel's own rows
   butt together, a select against a checkbox against a text field. A gap on the container rather than a margin per row, so the last one
   needs no exception and `.panel-block > *:last-child` keeps supplying the
   bottom padding.

   **`[open]`, and that is load-bearing rather than tidy.** A closed <details>
   still lays out its hidden content as a box — `::details-content` is a real
   flex item with no height — and a flex `gap` applies between items whatever
   their size. So a FOLDED panel was 10px taller than the three beside it, which
   is exactly the sort of difference that reads as a wobbly list rather than as a
   bug with a cause. The declarations only ever mattered while the panel was
   open; scoping them there costs nothing and the row of four folded summaries
   comes out even.

   The `padding` and `margin` that used to be restated here are gone with the
   same change: they were identical to `.panel-block`'s own, so they said
   nothing, and restating them under `[open]` would have made a closed panel
   genuinely differ. */
#docs-block[open] {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.interview-rows { display: flex; flex-direction: column; gap: 10px; }

/* The one folded summary that speaks when there is nothing wrong to hide —
   every other one says nothing in that case. What the red rows exist to catch
   is exactly what folding the panel would otherwise hide, so this is the
   exception rather than the pattern. */
#docs-summary.bad { color: var(--bad); }

.doc-row { display: flex; align-items: center; gap: 8px; }
.doc-row button { flex: 1; text-align: left; }
.doc-row .dim { font-size: 11px; color: var(--faint); white-space: nowrap; }

/* A CV title can be long, and the button carries it. Truncate rather than let
   the sidebar widen or the label wrap onto three lines. */
.doc-button { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-button.cv-tailored { border-color: var(--tailored); color: var(--tailored); }

/* Red means the prompts would go out with this document too thin to be worth
   sending, which is the one thing worth noticing at a glance before an
   interview starts.

   `.must-press` is the same fact from the other end — the CONTROL that fixes
   it — so it is the same three declarations rather than a second recipe. It is
   worn today by the two Rewrite buttons, sidebar and wizard, when the selected
   highlights are over the ceiling and have never been rewritten: past that
   length `brag_section_for` sends none of it, so the switch reads on and the
   prompts get nothing. */
.doc-button.thin,
button.must-press {
  border-color: var(--bad);
  color: var(--bad);
  background: color-mix(in srgb, var(--bad) 8%, transparent);
}

/* HALF of what a row promises is there. Amber rather than red, and `--warn`
   rather than `--them`, for the reason the banner's three tones are what they
   are: red is a fault or something lost, amber is a state nothing broke in and
   nothing is reassuring about either. The objection row is what wears it — the
   switch is on and only one of its two lists exists, which is a call that will
   be answered from half of what it could have been.

   A traffic light on a document row is a colour carrying information, which is
   the only kind this palette allows: it is the same job the checklist's dot and
   the fresh/ageing/stale stamp do. Never the only carrier, though — the button's
   `title` says which half is missing, exactly as the scheduled-date chip says in
   words that it is a plan. */
.doc-button.partial {
  border-color: var(--warn);
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 8%, transparent);
}

/* And it pulses, in the app's one halo — a box-shadow, so it costs no layout
   and cannot shove the state line beside it about once a second.

   This is the exception the Record button's rule allows for rather than a
   second opinion about it: emphasis has to carry information, and what this
   carries is that a document somebody has selected is not being sent and one
   press is the whole of the fix. It is drawn ONLY in that state — never at
   rest, never merely because a set is selected, and never while the button is
   disabled, since a pulse on a control that refuses to be pressed says two
   things at once. `button.must-press:hover` restates the border because
   `button:hover:not(:disabled)` outranks a single class and would otherwise
   turn a red pulsing button white under the pointer. */
button.must-press:not(:disabled) {
  --halo: var(--bad);
  animation: capture-halo 1.6s ease-out infinite;
}

button.must-press:hover:not(:disabled) { border-color: var(--bad); }

/* One dot, the same everywhere it appears: document rows and Diarize. */
.help-dot {
  flex: 0 0 auto;
  width: 19px;
  height: 19px;
  padding: 0;
  text-align: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--faint);
  font-size: 11px;
  line-height: 1;
  font-weight: 700;
}

/* `.doc-row button { flex: 1; text-align: left }` is more specific than the
   block above, so those two properties have to be won back by name. */
.doc-row .help-dot { flex: 0 0 auto; text-align: center; }

/* Auto answers is a real .doc-button now, same box and same white text as
   every other document row — clicking it toggles the switch beside it, since
   there is nothing here to open. flex/align-items is what .doc-button does
   not otherwise need: the bulb sits INSIDE this one, so its label and the
   bulb have to sit on one line together rather than the bulb dropping below
   or the label being pushed off by it. Same shape as .help-button, which
   solves the identical problem for Help's bulb. */
#btn-auto-answer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.help-dot:hover:not(:disabled) { color: var(--text); border-color: var(--dim); }

/* The switch sits at the end of the document's own row, so the button already
   names it and the switch carries no label of its own. */
.toggle {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: 0 0 auto;
  cursor: pointer;
  font-size: 12.5px;
}

.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }

.toggle-track {
  position: relative;
  flex: 0 0 auto;
  width: 32px;
  height: 18px;
  border-radius: 9px;
  background: var(--line-strong);
  border: 1px solid var(--line);
  transition: background 0.15s ease;
}

.toggle-track::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--dim);
  transition: transform 0.15s ease, background 0.15s ease;
}

.toggle input:checked + .toggle-track { background: color-mix(in srgb, var(--me) 55%, transparent); border-color: var(--me); }
.toggle input:checked + .toggle-track::after { transform: translateX(14px); background: #fff; }
.toggle input:focus-visible + .toggle-track { outline: 2px solid var(--me); outline-offset: 2px; }
.toggle input:disabled + .toggle-track { opacity: 0.4; }

/* ------------------------------------------------- alerts and long waits */

/* Both open on top of another modal, so they sit above the rest of the stack
   rather than relying on document order. */
/* The three that are raised from inside other dialogs, so they draw over them.
   Their order at the foot of index.html agrees with this, which is what the
   focus trap reads — see topModal() in static/ui.js. */
#busy-modal, #prompt-modal, #alert-modal { z-index: 30; }

.modal-narrow { max-width: 440px; }
.alert-body { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--dim); }
.modal-actions button.hidden { display: none !important; }

.busy { display: flex; align-items: flex-start; gap: 12px; }
.busy strong { display: block; font-size: 14px; }
.busy .dim { display: block; margin-top: 3px; font-size: 12px; line-height: 1.45; }
/* So the × sits at the right-hand edge rather than against the words. */
.busy-text { flex: 1; min-width: 0; }

/* What is being built and why the step wants it, under the bar. Set like the
   note above it rather than louder: it is there to be read by somebody with
   half a minute to spare, not to compete with the thing that is happening. No
   margin, for the reason `.busy-live` gives below. */
.busy-detail {
  margin: 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--dim);
}

/* Shown only while a call is being recorded, where a half-minute wait costs the
   thing on the screen behind it. Amber rather than red: it is the cost of what
   was just asked for, not a failure. */
/* No margin: `.modal-card` is a flex column with its own gap, and a margin here
   would be added to it rather than replacing it. */
.busy-live {
  margin: 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--warn);
}

.spinner {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin-top: 2px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--me) 25%, transparent);
  border-top-color: var(--me);
  animation: spin 0.9s linear infinite;
}

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

/* An indeterminate bar: the wait is known to be long but not how long, so it
   shows movement rather than a percentage it would have to invent. */
.busy-bar {
  height: 3px;
  border-radius: 2px;
  background: var(--line-strong);
  overflow: hidden;
}

.busy-bar span {
  display: block;
  width: 34%;
  height: 100%;
  border-radius: 2px;
  background: var(--me);
  animation: sweep 1.5s ease-in-out infinite;
}

@keyframes sweep {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(330%); }
}

/* A fixed 80vh with nowhere for the overflow to go was two bugs at once. The
   card is a flex column, so when the wizard's content did not fit — which it
   stops doing around 400px wide, where every paragraph in it takes twice the
   lines — the children shrank to make room: the textarea and the state line
   both ended up 16px tall holding 35px of text. And what still would not fit
   simply hung out of the bottom, which put Next 32px below the card's own edge
   on a background that is not the card.

   The scroll and the flex-shrink floor that fix it have MOVED UP to
   `.modal-card`, where they cover every dialog rather than this one. They were
   written here, against the wizard, and the three variants that set their own
   ceiling inherited the ceiling without them — Conversation settings was a flat
   list of nine rows with no inner scroll region anywhere in it, and had exactly
   the failure described above on any viewport under about 720px. It is a sidebar
   accordion now rather than a dialog, so that particular instance is gone; the
   fix stays where it is because it was never only about that one. What is left here
   is only what is particular to a full-height dialog: the height itself, the
   sticky way out of it, and a textarea that fills the room. */
.modal-tall { max-width: 820px; height: 80vh; }

/* The Organisation dialog takes `.modal-tall`'s escape and its sticky footer,
   but not its fixed height. The wizard wants 80vh flat because every step has
   to be the same size — a card that resized between steps would move Next
   under the reader's cursor. This one has no steps: it is one screen whose
   length is the size of the company, so a small organisation gets a small card
   and a large one grows to the same 80vh ceiling before it starts scrolling.

   A ceiling with an escape inherited from `.modal-card`, never a ceiling on its
   own — that is the rule stated above `.modal-card`'s own `overflow-y`.

   **Written by id, so it MUST also be named in the full-screen block at the
   foot of this file**, and it is. (1,1,0) beats that block's (0,2,0) whatever
   the file order, so without being listed there this card would stay 80vh on a
   handset while every other dialog went edge to edge — which is precisely the
   bug that list exists to record, in the wizard, in the same words. */
#organisation-modal .modal-card { height: auto; max-height: 80vh; }

/* Now that the card scrolls, the way out of it must not scroll with it. In a
   400px column the wizard's own prose is tall enough that Next sat 120px below
   the fold on several steps — reachable, but only if you knew to look, and the
   one control every step needs is not a thing to go looking for. Sticky rather
   than fixed: it belongs to this card, and it rides up as the last of the
   content passes under it. The background is the card's own, or the text
   scrolling underneath would show through. */
.modal-tall .modal-actions {
  position: sticky;
  bottom: -16px;   /* cancels the card's own padding, so it sits on the edge */
  background: var(--panel);
  padding: 10px 0;
  margin-bottom: -6px;
  border-top: 1px solid var(--line);
}
.modal-tall textarea {
  /* Grows into spare room, never shrinks out of existence, and keeps a floor
     that is still a usable box on the narrowest screen. */
  flex: 1 0 auto;
  min-height: 120px;
  resize: none;
  font-size: 13.5px;
  line-height: 1.55;
}

/* Wider than an ordinary dialog but not the full-height modal-tall (which
   also sets a flex-filling textarea meant for the library editor, not this):
   the Settings modal holds a tab strip and lists, not one long document.
   Each pane also carries .settings-form (see index.html) for the same
   column-and-gap layout that class already gives the account/global-settings
   forms elsewhere on this page — .settings-pane itself is only the
   show/hide selector settings.js switches on.

   The scroll comes from .modal-card now, along with the flex-shrink floor this
   never had: it had the escape and not the floor, so on a short viewport the
   seven-tab strip and the active pane were compressed before the card would
   scroll at all. Only the ceiling is its own. */
.modal-settings { max-width: 640px; max-height: 82vh; }

/* Each document's row is tinted by what it does, and named by the TOKEN rather
   than by the colour it currently happens to be: --tailored rewrites, --good
   starts from a preset, --me picks, --warn collects the words. The comment here
   said "purple tailors, green starts from a preset, blue picks" and every one
   of those three names had moved by the time the palette landed.
   Naming the token is ALL a variant does. The fills are not uniform on purpose
   — --good and --me sit brighter, so they take a percent less; do not tidy them
   level. Every row is `class="tailor-row cv-row"` (see index.html and
   `row.className` in wizard.js), so a variant ties on specificity and wins on
   file order. */
.tailor-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 10px;
  --tint: var(--tailored);
  --tint-fill: 8%;
  border: 1px solid color-mix(in srgb, var(--tint) 40%, transparent);
  background: color-mix(in srgb, var(--tint) var(--tint-fill), transparent);
  border-radius: 8px;
}

.tailor-row select { width: auto; flex: 1; min-width: 0; font-size: 12.5px; }
.preset-row   { --tint: var(--good); --tint-fill: 7%; }
.cv-row       { --tint: var(--me);   --tint-fill: 7%; }
.keyterms-row { --tint: var(--warn); }
.keyterms-row button { flex: 0 0 auto; }
.tailor-row .dim { font-size: 11px; min-width: 0; flex: 1 1 auto; }

/* The wizard's two document buttons — read one out of a file, and whatever the
   step itself offers — sit side by side as plain controls, in no container at
   all. A tint is what says "this select and this button are one thing"; these
   two share nothing but a line, and two tinted rows stacked read as two panels
   to study rather than two buttons to press.

   Each part is its own element because the steps show them independently: the
   job description step has a file and no action, the checklist an action and no
   file. `.hidden` carries `!important`, so a part being laid out here cannot
   stop it being hidden. The state line takes the whole width and drops out
   entirely when it has nothing to say, which is what stops a file being read
   from moving the buttons. */
/* Gone entirely on a step that has neither button, rather than laid out empty:
   the card is a flex column with a gap of its own, so a zero-height row still
   costs a gap — which the language step wore as a blank band under its note. */
.wizard-doc-row { display: none; }
.wizard-doc-row:has(> .wizard-doc-part:not(.hidden)) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.wizard-doc-part { display: flex; align-items: center; gap: 8px; }
.wizard-file-state { flex: 1 0 100%; font-size: 11px; min-width: 0; }
.wizard-file-state:empty { display: none; }

/* Ten preset colours for categories, held on a row as an INDEX rather than a
   stored hex — see categories.colour in app/db.py. This is the one place that
   index becomes a colour; a category's row elsewhere in the client (the
   picker here, the modal's rows) reads --category-N through this same list, so
   repainting the palette is a one-file change. Picked apart on the colour
   wheel and away from the app's own semantic colours (--bad, --good, --me,
   --them) so a category chip is never mistaken for a status. */
:root {
  --category-0: #ff6b6b;
  --category-1: #ffa94d;
  --category-2: #ffd43b;
  --category-3: #94d82d;
  --category-4: #37b24d;
  --category-5: #20c997;
  --category-6: #4dabf7;
  --category-7: #748ffc;
  --category-8: #b197fc;
  --category-9: #f783ac;
}

.category-dot {
  /* flex: 0 0 auto when it is a direct flex child (the picker row); the
     explicit display/vertical-align is what makes it render as a proper
     circle everywhere else — inside a pill button (the modal filter, a
     conversation row), which is not itself a flex container. */
  display: inline-block;
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  vertical-align: middle;
  border-radius: 50%;
  background: var(--dim);
}
.category-dot[data-colour="0"] { background: var(--category-0); }
.category-dot[data-colour="1"] { background: var(--category-1); }
.category-dot[data-colour="2"] { background: var(--category-2); }
.category-dot[data-colour="3"] { background: var(--category-3); }
.category-dot[data-colour="4"] { background: var(--category-4); }
.category-dot[data-colour="5"] { background: var(--category-5); }
.category-dot[data-colour="6"] { background: var(--category-6); }
.category-dot[data-colour="7"] { background: var(--category-7); }
.category-dot[data-colour="8"] { background: var(--category-8); }
.category-dot[data-colour="9"] { background: var(--category-9); }

/* Sits before the label inside the category button and before each option's
   own title inside the menu below — .advice-menu button is a flex row with
   `align-items: baseline` for its text, which sets a 10px circle a little low;
   centering it here is the one adjustment a menu carrying a dot needs over
   the plain text ones. */
#category-button-dot { margin-right: 6px; }
.advice-menu button .category-dot {
  align-self: center;
  /* .advice-menu span (below) pushes a bare <span> — the secondary hint text
     like "Flash, ~2s" — to the right with margin-left: auto. The dot is a
     span too and was caught by the same rule, which is what pushed every
     named category to the right edge of the menu while "No category" (no
     dot, so no span to catch) stayed put on the left. Two classes beats one
     class + a type, so this wins regardless of which is written first. */
  margin-left: 0;
}

/* A session CV is not one of the library CVs, so it must not look like one.
   Only the closed control is tinted: options inherit the select's colour, so
   they have to be reset or the whole open dropdown turns purple. */
.cv-tailored {
  color: var(--tailored);
  border-color: var(--tailored);
  font-weight: 600;
}

.cv-tailored option {
  color: var(--text);
  font-weight: 400;
}

.option-tailored,
.cv-tailored option.option-tailored {
  color: var(--tailored);
  font-weight: 600;
}

.modal-actions { display: flex; flex-wrap: wrap; flex-shrink: 0; align-items: center; justify-content: space-between; gap: 12px; }
.modal-actions .dim { font-size: 11.5px; }
/* The hint spells out what the button is about to do, so it is the hint that
   wraps when it runs long, never the button that gets squeezed. */
.modal-actions button { padding: 8px 22px; flex: 0 0 auto; }

/* Lit = Flash, dark = Flash Lite. */
/* The bulb governs Help and nothing else, so it sits on the Help button rather
   than beside it. Small, because it is a secondary control on a primary one. */
.bulb {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-left: 8px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 5px;
  /* Against the blue Help button, not against the page. */
  color: color-mix(in srgb, var(--on-warn) 55%, transparent);
  /* Unlit it is still a control, and without an edge it read as part of the
     Help label rather than as something separately pressable. */
  border: 1px solid var(--warn);
  cursor: pointer;
  transition: color 120ms, background 120ms, box-shadow 120ms, border-color 120ms;
}

.bulb svg { width: 14px; height: 14px; fill: currentColor; }
.bulb:hover:not(.on) { color: color-mix(in srgb, var(--on-warn) 90%, transparent); background: color-mix(in srgb, var(--warn) 25%, transparent); }
.bulb:focus-visible { outline: 2px solid var(--on-warn); outline-offset: 1px; }

.bulb.on {
  color: var(--on-warn);
  background: #ffd54a;
  border-color: #ffd54a;
  box-shadow: 0 0 9px rgba(255, 213, 74, 0.65);
}

/* The same bulb on a GHOST button — the objection button, which is not the one
   filled control on the screen and must not become a second one.

   Only the UNLIT half changes, and `:not(.on)` is what makes that true rather
   than relying on file order: `.bulb.on` and `.bulb.bulb-ghost` are both
   (0,2,0), so a plain override written after it would win in both states and
   take the lit bulb's glow with it. Lit is the half that carries the
   information and has to read identically wherever the bulb sits.

   `--on-warn` is a dark ink for a bright fill, so the resting colours above —
   mixed against the Help button's own — are all but invisible on a transparent
   one. */
.bulb.bulb-ghost:not(.on) { color: var(--faint); border-color: var(--line-strong); }
.bulb.bulb-ghost:not(.on):hover {
  color: var(--text);
  background: color-mix(in srgb, var(--warn) 20%, transparent);
  border-color: var(--warn);
}
/* inline-flex for the reason `.help-button` takes it: the bulb sits on the text
   baseline row rather than below it. */
#btn-objection { display: inline-flex; align-items: center; }

/* inline-flex so the bulb sits on the text baseline row rather than below it */
.help-button {
  flex: 0 0 auto;
  padding: 7px 12px;
  font-size: 13px;
  display: inline-flex;
  align-items: center;
}

/* THE THREE THINGS YOU ASK FOR MID-CALL ARE ONE GROUP AND ONE SIZE.

   Answer, Advice and Objection sit in a row and do the same kind of thing:
   spend a model call on what is being said and put the reply in the same panel.
   They were three different sizes, and none of the differences meant anything.
   Answer took 13px from `.help-button` while the other two kept the 11px every
   button here starts at; and Answer and Objection each stand 36px tall because
   a `.bulb` inside them is taller than their own text, where Advice — which has
   no bulb, having no model left to choose — came out at 31.

   So the smallest of the three was the one whose INTERNALS were simplest, which
   is exactly the kind of difference a reader is entitled to read as meaning
   something. `min-height` rather than `height`, so a label that wraps at a
   narrow width still grows the box instead of spilling out of it. */
#btn-help,
#btn-advice,
#btn-objection {
  min-height: 36px;
  font-size: 13px;
}


/* ----------------------------------------------------------------- history */

/* Three states, one recipe: set `--tint` and the mixing below is not restated.
   A fourth state is one line. */
.hints {
  --tint: var(--good);
  --tint-fill: 8%;
  border: 1px solid color-mix(in srgb, var(--tint) 45%, transparent);
  background: color-mix(in srgb, var(--tint) var(--tint-fill), transparent);
  border-radius: 10px;
  padding: 12px 14px;
  margin-bottom: 14px;
}

/* `flex-wrap: wrap` at EVERY width, not only below 700px where this block used
   to be the whole answer. Settings reached seven tabs the day the Global tab
   had something in it — it is hidden while DEFINITIONS is empty — and the
   seventh sat off the right edge of the card with a horizontal scrollbar under
   the entire modal. A strip that cannot fit should take a second row; nothing
   about that is narrow-screen-specific. */
.hints-tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-bottom: 9px;
}

/* Already uppercase before any of this; what it was missing was the face and
   the tracking that make the landing's chips read as a set rather than as bold
   words in a row. */
/* Three states, no hue, and each one is a step up in weight rather than a
   different colour: a plain white word, the same word in a hairline pill, and
   the same word inverted. Nothing here, something here, you are here.

   It used to be three shades of grey climbing to white, which put the labels of
   every strip in the app at `--faint` — the floor, and a floor is the wrong
   place for the one control that says what you are looking at. White reads as
   the chrome it is; the pill and the inversion carry the two facts that used to
   be carried by how bright the text was. */
.hints-tab {
  background: transparent;
  border: 1px solid transparent;
  color: var(--text);
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 4px 11px;
  border-radius: 999px;
}

/* A tab holding something it has been given. It was a brighter grey, which is
   nothing at all once every label is already white, so the fact moved into the
   pill: an outlined tab has an answer in it, a plain one does not. */
.hints-tab.has-result { border-color: var(--line-strong); }

/* The one you are on, inverted — the same solid white `button.primary` gives
   the emphasised control on a screen, for the same reason: white is the most
   contrast this background has, so it needs no hue to be unmistakable, and
   every hue stays free to mean something.
   Nothing sets a competing colour on `.hints-tab` in any panel that borrows the
   strip, so a panel needs no active rule of its own. Check that before adding
   one — there were three, all identical to this and overriding nothing. */
.hints-tab.active {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}

/* Hover is a BACKGROUND, not a border: the generic `button:hover` lights the
   hairline, which is now how a tab says it is holding an answer — so pointing
   at an empty tab would have made it claim to have one. */
.hints-tab:hover:not(.active) {
  background: color-mix(in srgb, var(--text) 12%, transparent);
  border-color: transparent;
}

.hints-stamp {
  margin-left: auto;
  font-size: 11px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}

/* How long ago, not when: green under a minute, amber through the second,
   red after — the same three colours the checklist marks coverage with, but
   never applied to the coverage ratio itself (see .stamp-age below), so the
   two scales are never asking to be read against each other. See ageClass in
   core.js for the boundaries.
   Answers/Advice colour the whole stamp, since it is only ever a status word
   or the age, never both at once. The side panel's stamp can hold both — a
   ratio AND an age — so there the colour lands on a nested .stamp-age span
   rather than on #side-stamp itself. */
.hints-stamp.fresh { color: var(--good); }
.hints-stamp.ageing { color: var(--warn); }
.hints-stamp.stale { color: var(--bad); }
#side-stamp .stamp-age.fresh { color: var(--good); }
#side-stamp .stamp-age.ageing { color: var(--warn); }
#side-stamp .stamp-age.stale { color: var(--bad); }

/* Advice is a different kind of answer, so the box says so. */
.hints[data-tab="advice"] { --tint: var(--me); }

/* An error states itself: a SOLID edge rather than the 45% mix, and more fill.
   Both selectors are spelled out because `.hints[data-tab="advice"]` is (0,2,0)
   and would otherwise win --tint back to --me on an Advice panel that failed. */
.hints-error,
.hints[data-tab="advice"].hints-error {
  --tint: var(--bad);
  --tint-fill: 10%;
  border-color: var(--tint);
}
.hints-message { margin: 0; font-size: 13px; color: var(--dim); }
.hints ul { margin: 0; padding-left: 18px; }
.hints li { font-size: 16.5px; line-height: 1.5; color: #fff; margin-bottom: 3px; }

/* Placeholders before Help has been pressed, so the box keeps its height. */
.hints-idle { border-color: var(--line); background: rgba(255, 255, 255, 0.02); }
.hint-empty::after {
  content: '';
  display: inline-block;
  width: 42%;
  height: 1px;
  vertical-align: middle;
  background: rgba(255, 255, 255, 0.09);
}

.hints li.hint-empty { color: var(--faint); }

.history { min-width: 0; }
.live { display: flex; flex-direction: column; }

/* The strip above a FOLLOWED transcript — a colleague reading a conversation
   its owner shared, or the owner's own second screen. See follow.js.

   `--tint` and nothing else, like every other tinted box in this file: the two
   states it has differ by which token it names, not by a second copy of the
   mixing. Amber while a capture is running, because amber is what a running
   capture is everywhere else here — the Record button's halo included — and
   `--faint` once it has stopped, because a finished conversation is not an
   event, it is just something you may only read. */
.follow-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  border-radius: 9px;
  padding: 8px 12px;
  margin-bottom: 12px;
  font-size: 13px;
  --tint: var(--faint);
  --tint-fill: 8%;
  border: 1px solid color-mix(in srgb, var(--tint) 55%, transparent);
  background: color-mix(in srgb, var(--tint) var(--tint-fill), transparent);
}

.follow-bar:has(.follow-dot.live) { --tint: var(--warn); }

/* `min-width` rather than the `min-width: 0` most flex children here carry, and
   NOT inside a media query. This is a whole sentence sitting beside two
   buttons, so the row is one that cannot give: with `min-width: 0` the sentence
   is the only thing that shrinks and it went to about 100px — four words a line
   — while the buttons kept their full width beside it. A floor makes the flex
   line wrap instead, dropping the buttons onto a row of their own, which is the
   right answer at every width and not only the narrow one. Same reasoning as
   `.modal-actions` and `.tailor-row`, which are also out here. */
.follow-note { flex: 1; min-width: 220px; color: var(--dim); }

.follow-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--tint);
}

/* Pulsed with a box-shadow rather than a transform, the same way the Record
   button is: it costs no layout, so it cannot nudge the sentence beside it
   about once a second. Only while live — a dot that pulses on a conversation
   that finished last week is an advert, and this file spends a section on why
   emphasis has to carry information. */
.follow-dot.live { animation: capture-halo 1.6s ease-out infinite; }

.follow-listen { flex: 0 0 auto; }
.follow-listen.listening { border-color: var(--warn); color: var(--warn); }

/* The `?` explains Listen, so it goes wherever Listen goes. Written as a
   consequence of the button's own state rather than as a second thing for the
   render to remember — the same shape as `body:has(#controls.hidden)`.

   It covers three cases at once and was a real if small bug in two of them: the
   button is withdrawn when a call ends, and hidden entirely where the browser
   cannot speak, and in both a lone question mark was left behind explaining a
   control that was no longer on the screen. The third is SPEAK_OFFERED in
   follow.js being false, which is what it is today. */
.follow-bar:has(#btn-listen.hidden) .help-dot { display: none; }

.live-block { padding: 9px 0; }
.live-block:not(.active) { display: none; }
.live-block[data-lane="me"] .turn-who { color: var(--me); }
.live-block[data-lane="them"] .turn-who { color: var(--them); }

.live-block .turn-text {
  font-size: 21px;
  line-height: 1.45;
  color: #fff;
}

.live-block .turn-translation { font-size: 21px; }
/* The original drops back in the live block exactly as it does in the history. */
.live-block .turn-original { font-size: 13px; }

.live-block .turn-text::after,
.live-block .turn-translation::after {
  content: '';
  display: inline-block;
  width: 7px;
  height: 17px;
  margin-left: 4px;
  vertical-align: -2px;
  background: var(--me);
  animation: blink 1.1s steps(2, start) infinite;
}

/* Only the half being read carries the caret. */
.live-block .turn-original::after { content: none; }

@keyframes blink { to { visibility: hidden; } }

.transcript { display: flex; flex-direction: column; }

.turn { padding: 9px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
.turn-head { display: flex; align-items: baseline; gap: 9px; margin-bottom: 2px; }

.turn-who { color: var(--faint); }
.turn[data-lane="me"] .turn-who { color: var(--me); }
.turn[data-lane="them"] .turn-who { color: var(--them); }

/* A speaker's name is a BUTTON where it can be changed — a real one, so it is
   reachable by keyboard and announced as a control, rather than a span with a
   click handler bolted on. That means undoing the pill this file gives every
   button: the border, the radius, the padding and the fixed colour would
   otherwise draw a lozenge over every line of the transcript and paint all
   three lanes the same.
   `color: inherit` rather than a value, so the lane rules above keep deciding
   it — a colour written here would have to be written three times and would go
   stale the moment a fourth lane existed.
   The font is NOT reset: `.turn-who` is in the LABEL VOICE list at the top of
   this file and outranks the `button` rule on specificity, so the name is
   already set the way every other label is. */
button.turn-who {
  border: 0;
  border-radius: 0;
  padding: 0;
  background: none;
  color: inherit;
  cursor: pointer;
}

/* The affordance, and it is deliberately quiet at rest. A dotted underline says
   "there is something under this" without turning every speaker tag in a long
   transcript into a row of live-looking controls — the transcript is for
   reading, and renaming is a thing done once. It resolves on hover and focus.
   `text-underline-offset` because the tag is 10.5px and tracked: an underline
   sitting on the baseline touches the descender of a Cyrillic д. */
button.turn-who {
  text-decoration: underline dotted color-mix(in srgb, currentColor 45%, transparent);
  text-underline-offset: 3px;
}

button.turn-who:hover,
button.turn-who:focus-visible { text-decoration: underline solid currentColor; }

/* A touch target that a finger can actually hit. The tag itself is 10.5px, so
   on a phone the visible text is well under the ~44px a thumb needs — and this
   sits in a row beside a timestamp and two icon buttons. Padding would move the
   text and break the baseline the head row is aligned on, so the target is
   grown with a pseudo-element instead, which costs no layout at all. */
button.turn-who { position: relative; }

button.turn-who::after {
  content: "";
  position: absolute;
  /* The tag's own line box is about 19px, which is a 10.5px label and nothing a
     thumb can aim at. 13px either side takes the real target past 45px without
     moving a pixel of text — measured by clicking above the glyphs rather than
     by reading the element's box, which does not include this. */
  inset: -13px -10px;
}

.turn-time { font-size: 11px; color: var(--faint); font-variant-numeric: tabular-nums; }

.turn-delete {
  background: transparent;
  border: 0;
  color: var(--faint);
  font-size: 15px;
  line-height: 1;
  padding: 0 5px;
  border-radius: 4px;
  opacity: 0;
  transition: opacity 90ms;
}

.turn:hover .turn-delete { opacity: 1; }
.turn-delete:hover { color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, transparent); }
.turn-delete:focus-visible { opacity: 1; }

/* The same defect the conversation rows had, in the transcript: a pointer that
   cannot hover never reveals this, so on a touch screen the only way to delete
   a line was invisible and unreachable. Same question, same answer — the input
   decides it, not the width.

   It is a SECOND `hover: none` block rather than three more selectors in the
   one at the top of the list section, and that is forced: media queries add no
   specificity, so `.turn-delete { opacity: 1 }` written up there would tie with
   `.turn-delete { opacity: 0 }` below it and lose on file order — silently, and
   in the direction that looks like the rule simply does not work. Same reason
   this file already carries a second `prefers-reduced-motion` block. A rule's
   escape lives after the rule it escapes. */
@media (hover: none) {
  .turn-delete { opacity: 1; }
}

/* `pre-wrap`, not the default: a line's text is set with `textContent`, so any
   newline the speaker — or the assistant — actually wrote is already in the
   DOM, and HTML was silently folding every one of them into a space. An answer
   written as five lines was being read back as one paragraph. It still wraps
   normally at the edge; it just stops discarding breaks somebody meant. */
.turn-text {
  margin: 0;
  font-size: 15px;
  color: var(--mute);
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

/* The newest portion is the one being read, so it gets the weight, unless a
   lane is mid-portion, in which case the live block is the current one. */
.turn-current .turn-text { font-size: 21px; line-height: 1.45; color: #fff; }
.turn-current .turn-who { font-size: 12.5px; }

.history.live-active .turn-current .turn-text { font-size: 15px; color: var(--mute); }
.history.live-active .turn-current .turn-who { font-size: 11.5px; }

/* Turning translation on means the original is not the language being read, so
   the translation carries the weight and the original becomes the reference
   under it. It is set exactly like any other line — same colour, same size —
   and the ORIGINAL underneath is what says it was translated.

   It used to be green, to be "distinguishable at a glance from a line that was
   never translated". The glance worked; the cost was that every paragraph of a
   translated call was a saturated colour, so a translated conversation and an
   untranslated one looked like two different applications rather than two
   conversations. The signal it was buying is already free: a line with an
   original quoted under it was translated, and a line without one was not.

   THESE BELONG HERE, above the 860px block, beside the .turn-text rules they
   are the other half of. Filed below it — which they were, stranded in the
   sign-in card's section — `.turn-current .turn-translation` is (0,2,0) in both
   places, so the later one wins and the query never applies: a translated
   current line stayed 21px beside an 18px untranslated one at every width. Its
   .turn-text twin escaped only by being written above the query. No check here
   can see that, which is why the rules for one thing stay together. */
.turn-translation {
  margin: 0;
  color: var(--mute);
  font-size: 15px;
  line-height: 1.45;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}

.turn-original {
  margin: 3px 0 0;
  padding-left: 9px;
  border-left: 2px solid var(--line);
  color: var(--faint);
  font-size: 12.5px;
  line-height: 1.5;
}

/* The newest portion is read from across the room, so both halves grow. */
.turn-current .turn-translation { font-size: 21px; line-height: 1.45; color: #fff; }
/* The "newest portion" rules above turn .turn-text white and large, and they
   outrank .turn-original on specificity, so the original is put back by name. */
.turn-current .turn-original { font-size: 13px; color: var(--faint); }
.history.live-active .turn-current .turn-translation { font-size: 15px; color: var(--mute); }
.history.live-active .turn-current .turn-original { font-size: 12.5px; color: var(--faint); }

.session-divider {
  color: var(--faint);
  font-size: 11px;
  text-align: center;
  margin: 12px 0;
  border-top: 1px dashed var(--line);
  padding-top: 8px;
}

.footer {
  color: var(--faint);
  font-size: 11.5px;
  margin-top: 22px;
  border-top: 1px solid var(--line);
  padding-top: 10px;
}

@media (max-width: 860px) {
  /* One column, and the two retracted states have to be NAMED here or they win.
     A media query adds no specificity, so `.layout.sidebar-hidden` (0,2,0),
     written 1600 lines above, beat the bare `.layout` (0,1,0) in this block at
     every width — the 34px rail survived into a layout that has no beside.
     Combined with the `order` below, which puts `.history` first, the rail then
     became the CONTENT column: the whole conversation was laid out in 34px, one
     word per line with the words broken mid-word, while the empty sidebar took
     the remaining 314. Reported as "the 400 mode looks very bad", and it needed
     the reader to have retracted the sidebar on a wide screen first, which is
     why every narrow-width sweep that did not do that came back clean.

     `.sidebar-toggle` is hidden in this block as well, so down here the reader
     cannot even undo the state that is doing it. */
  .layout,
  .layout.sidebar-hidden,
  .layout.no-sidebar { grid-template-columns: 1fr; }
  /* And the rail's other half: with one column the sidebar is stacked, not
     collapsed, so `#controls` must not be hidden by a preference about a
     column that no longer exists. Below 700px it has left this block entirely
     — `applyCompactControls` parks it in its dialog — but between 700 and 860
     it is still here and still the only way to set a conversation up. */
  .layout.sidebar-hidden .sidebar #controls { display: block; }

  /* The sidebar goes LAST — under the answers and under the transcript, not
     above them. Stacked, it was 500-odd pixels of setup standing between the
     header and the conversation, so the thing the screen is for started below
     the fold and everything that gets it ready came first. Beside the
     transcript that order costs nothing, which is why it is only reordered
     here; `order` rather than a change to the markup, because there is one
     template and the wide layout wants the sidebar first.
     `main.history` holds the answers panel and the transcript both, so one
     pair of orders puts the whole conversation above the whole sidebar. */
  .history { order: 1; }
  .sidebar { position: static; max-height: none; order: 2; }

  /* The retract control collapses the sidebar to a 34px rail BESIDE the
     transcript. From here down there is no beside, so it is a full-width bar
     offering something it cannot do. Hidden at 860 rather than at 520, which is
     where it first went: the layout collapses here, so this is where the
     control stops meaning anything. */
  .sidebar-toggle { display: none; }
  .title-input { font-size: 17px; }
  .live-block .turn-text, .turn-current .turn-text { font-size: 18px; }
  .live-block .turn-translation, .turn-current .turn-translation { font-size: 18px; }
  .live-block .turn-original, .turn-current .turn-original { font-size: 13px; }
}

/* Phone capture: one lane carrying both voices, in --room. It read --tailored
   until the palette moved, which was two mistakes sharing one token — the CV
   accent has nothing to do with a speaker, and its violet is now exactly what
   --them holds, so a transcript carrying both kinds of line (which is the
   ordinary case: the capture mode can change mid-call) would have labelled the
   far end and the room in the same colour. */
/* Phone mode puts both voices on one microphone, so the mic picker is reporting
   the ROOM rather than your own half and says so in the room's colour. The
   capture picker reports nothing there — see `tick`, which hands it no lane —
   and the amber is what stops the one pulse left on screen from claiming to be
   your side alone. */
body:has(#btn-capture-mode[data-value="phone"]) #btn-mic.lane-pulse { --pulse: var(--room); }
.option-row.phone-capture { opacity: 0.45; }
.turn[data-lane="room"] .turn-who { color: var(--room); }
.live-block[data-lane="room"] .turn-who { color: var(--room); }

.advice-button { flex: 0 0 auto; padding: 7px 14px; border-color: var(--line-strong); color: var(--text); }
.advice-button:hover:not(:disabled) { border-color: var(--text); }



/* Advice picks its model when pressed, so the button opens a menu. */
.advice-wrap { position: relative; flex: 0 0 auto; }
.advice-menu {
  position: absolute;
  top: calc(100% + 5px);
  left: 0;
  z-index: 30;
  min-width: 148px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.5);
}

.advice-menu button {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  background: transparent;
  border: 0;
  border-radius: 6px;
  padding: 7px 9px;
  text-align: left;
}

.advice-menu button:hover { background: var(--raise); }
.advice-menu strong { font-size: 12.5px; font-weight: 600; }
.advice-menu span { margin-left: auto; font-size: 11px; color: var(--faint); }

/* ------------------------------------------------------------------- gate */

/* A COLUMN. The card and the "Part of a Bamya organisation?" line below it are
   two children of this box, so a row laid them out side by side — the alternate
   sign-in sentence sat out to the right of the card, level with the password
   field, against a page that had nothing else on it. `.gate-alt` has said
   `margin: 14px 0 0` and `text-align: center` the whole time, which is what it
   would want directly underneath; the axis is what was wrong. */
.gate {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 80vh;
  padding: 20px;
}

.gate-card {
  width: 100%;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  gap: 13px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 26px 24px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
}

/* No font-size here any more: the <h1> is the wordmark itself, and .mark sizes
   it. A 19px override would have shrunk the mark to below the body text. */
.gate-card h1 { margin: 0; }
.gate-card .dim { margin: -8px 0 4px; }
.gate-card .primary { margin-top: 5px; padding: 9px; }

.gate-alt {
  margin: 14px 0 0;
  font-size: 13px;
  text-align: center;
}

.gate-alt .pill { margin-left: 6px; }

.gate-error {
  margin: 0;
  padding: 9px 11px;
  border-radius: 8px;
  border: 1px solid var(--bad);
  background: color-mix(in srgb, var(--bad) 12%, transparent);
  font-size: 13px;
}

/* The quiet counterpart to .gate-error: something worth knowing before you
   submit, not something that went wrong. Same width and rhythm, no colour —
   an accent here would compete with the error above it, and this is
   information rather than a state. */
.gate-note {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--faint);
}

/* -------------------------------------------------------- the text editor

   A standalone page, in the same category as .gate above rather than a mode
   of the app shell — see the route's own docstring in app/ui/routes.py. One
   box, filling whatever height the window gives it. */
.text-editor-page {
  display: flex;
  flex-direction: column;
  height: 100vh;
  padding: 16px;
  gap: 12px;
}
.text-editor-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.text-editor-head h1 { margin: 0; font-size: 16px; }
.text-editor-actions { display: flex; gap: 8px; flex: 0 0 auto; }
#text-editor-box {
  flex: 1;
  min-height: 0;
  resize: none;
  font-size: 14px;
  line-height: 1.5;
}

/* Confirmation, in the one place the app says something went RIGHT before you
   are signed in. `--good` earns its use here by the same test every accent
   does: it reports a state that changes what you can do — you may now record. */
.gate-ok {
  margin: 0;
  color: var(--good);
  font-size: 13px;
  line-height: 1.5;
}

/* The context editor borrows the tab strip whole — `.hints-tab.active` already
   draws the neutral accent, so there is nothing to win back here. */
.modal-card .context-tabs { margin-bottom: 0; }

/* ------------------------------------------------------------- settings */

.settings-form { display: flex; flex-direction: column; gap: 20px; }
/* Label and control on one line, help underneath spanning both. The previous
   shape stacked all three, which is right for a textarea and wrong for a
   number: six four-digit fields each given the full width of the card turned
   one short form into a scroll. */
.setting { display: flex; flex-direction: column; gap: 6px; }
.setting-help { margin: 0; line-height: 1.5; }

/* A setting whose value is one number puts the name and the box on ONE line.
   `.field` is a column — right for a textarea, wrong for four digits — and
   stacking them gave six full-width boxes and a form you had to scroll. The
   name takes the space and the control takes what it needs. */
.setting-field {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 4px 14px;
}

/* `input.setting-number`, not `.setting-number`. The shared control rule is
   `input[type="number"] { width: 100% }`, and an attribute selector counts as
   a class — so a bare class ties on nothing and LOSES to it. This is the same
   flat-namespace trap the stylesheet warns about elsewhere, one specificity
   level down. */
input.setting-number {
  width: 9ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* What a price in pence actually costs, beside the box. Typing 19 into a pence
   field charges nineteen pence, and this is the only thing on the screen that
   would say so before a customer did. */
.setting-echo {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  min-width: 7ch;
  text-align: right;
}

/* The pair sits together on the right of the row. */
.setting-control { display: flex; align-items: center; gap: 8px; }
.settings-actions { display: flex; align-items: center; gap: 12px; }

/* One row per category, each carrying its own actions rather than one shared
   management row acting on whichever category a picker elsewhere happens to
   have selected — see categories-block in settings.html. */
.categories-list { display: flex; flex-direction: column; gap: 8px; list-style: none; margin: 0; padding: 0; }
.category-row-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
}
.category-row-item .category-row-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.category-row-item button { flex: 0 0 auto; }

/* ------------------------------------------------------------------- home */

/* With no conversation open, this is the page — the search results and the
   stats. Conversations, + New, Settings and search moved up into the header
   — see .topbar-home-controls — so there is no toolbar of its own here any
   more. */
/* Same width and centering as .layout — the conversation view's own container
   — rather than a narrower column of its own: this is the other page the app
   has, and the two should read as one interface rather than as a wide screen
   and a cramped one bolted together. */
/* `width: 100%` beside the auto margins, and it is load-bearing rather than
   belt-and-braces. This is a flex item inside `.history`, and **an auto margin
   on a flex item absorbs the free space instead of stretching the item** — the
   same trap `.layout`'s children already carry a note about. Without it the
   section shrinks to its widest child and the two auto margins split the rest,
   so the whole home screen sat in a column as wide as whatever happened to be
   in it: 563px of margin either side of a 192px block.
   `max-width` still centres it past 1320px, which is what the auto margins are
   actually there for. */
.home {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 100%;
  max-width: 1320px;
  margin-inline: auto;
}

/* Renders as `display: contents` — its children (+New, Settings, search) are
   flex items of .topbar directly, not a row nested inside it, so they read as
   more buttons on the same header line rather than a second toolbar. Shown
   only with nothing open, in the room #detail-title-text sat empty in — see
   route() in app.js, which toggles both together. `.hidden`'s `!important`
   still wins over this while a conversation IS open. */
.topbar-home-controls { display: contents; }

/* What a link needs ON TOP of the ghost pill it takes from the base `button`
   rule (see INPUTS): an anchor is inline, and it underlines. */
.header-button-link {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

/* Both + New buttons carry `class="primary"` in index.html and take the solid
   white from `button.primary` in INPUTS — an emphasised control is the same
   control wherever it is, so neither gets a colour of its own by id.
   What IS this one's own is where it sits: the far end of the tab row rather
   than a row of its own, so Conversations/Templates read as the view and this
   reads as the one action beside it. */
#modal-new { margin-left: auto; }

.topbar-search {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  flex: 0 1 320px;
  min-width: 0;
}

.topbar-search input { flex: 1; min-width: 0; }

/* A square icon button rather than a labelled "Search" — the box beside it
   already says what this is. Same family as the header's other icon-toggle
   buttons (the gear, the download arrow), just holding a magnifying glass. */
.topbar-search .icon-toggle {
  flex: 0 0 auto;
  width: 33px;
  height: 33px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.topbar-search .icon-toggle svg { width: 16px; height: 16px; }

/* A KPI row of stat tiles — a handful of headline numbers, not a chart. Value
   in the app's own semibold weight at a size that reads from across the room;
   label in sentence case underneath, in the app's muted ink. Proportional
   figures throughout: these are standalone values, not a column that needs to
   align, so tabular-nums would only make small numbers look loose. */
.home-stats {
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px 14px;
  /* A canvas below this divides what is left of a viewport this screen is not
     allowed to scroll, so this block must not grow with its content. It never
     does — at most a plan row, a total, two capped buckets and the lead pool —
     but saying so keeps a future row from being the thing that reintroduces a
     fold nobody meant. */
  flex: 0 0 auto;
}

/* The plan, when it renews, and whether a card has failed. */
.usage-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}

/* Takes the label voice above — it is furniture naming a tier, not prose. */
.usage-plan { color: var(--text); }

.usage-when { color: var(--faint); font-size: 12px; }

/* --warn, not --them. Amber is the warning colour; the far end's violet is a
   speaker lane and nothing else. */
.usage-flag {
  color: var(--warn);
  font-size: 12px;
  font-weight: 600;
  margin-left: auto;
}

/* One row per meter: head, figure, then the bar across the full width. The
   grid is what keeps the figures aligned down the block while the heads vary
   in length — mono at this size runs long, and a flex row would ragged them. */
.usage-meter {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 2px 10px;
}

.usage-meter-value {
  font-size: 12px;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

.usage-note {
  grid-column: 1 / -1;
  font-size: 11px;
  color: var(--faint);
}

.usage-track {
  grid-column: 1 / -1;
  height: 6px;
  background: var(--sunken);
  border-radius: 4px;
  overflow: hidden;
}

.usage-fill {
  height: 100%;
  width: 0;
  border-radius: 4px;
  transition: width 200ms ease;
}

/* The three states an allowance can be in. A quota bar is one of the few
   things here entitled to a colour: it reports something that changes what you
   can do, which is the test every accent in this stylesheet has to pass. Same
   shape as the fresh/ageing/stale stamp, reading the same tokens. */
.usage-good { background: var(--good); }
.usage-warn { background: var(--warn); }
.usage-bad { background: var(--bad); }

/* The lead pool sits under this, and the rule is load-bearing rather than
   decorative: those hours renew on a different date, are not counted in the
   total above, and no upgrade changes them. Without the break it reads as one
   more bucket of the tier. */
.usage-rule {
  height: 1px;
  background: var(--line);
  margin: 2px 0;
}

/* ------------------------------------------------------------ home charts */

/* A fixed two-column grid — five cards read as 2/2/1 rather than reflowing to
   three across at whatever width happens to fit a third 420px card, which is
   the one thing auto-fit cannot promise. */
.home-charts {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14px;
}

@media (max-width: 700px) {
  .home-charts { grid-template-columns: 1fr; }
}

/* The window every card below shares, said once. Spans both columns so it reads
   as a heading over the block rather than as a first card. */
.home-charts-heading {
  grid-column: 1 / -1;
  margin: 10px 0 0;
}

.chart-card {
  display: flex;
  flex-direction: column;
  /* A grid item's default `min-width: auto` is its MIN-CONTENT width, and a
     canvas contributes its own pixel width to that. So once Chart.js had sized
     the canvas for a wide window, this card could never be narrower than that
     canvas again: the track shrank to 376px and the card stayed at 649,
     overflowing the page. It looked like the charts "only resize on refresh",
     because a reload is the one moment Chart.js measures a container that has
     not yet been floored by a canvas.
     Chart.js does its half correctly — `responsive: true` and a
     `position: relative` wrapper of a fixed height — and could do nothing about
     a parent that refused to shrink. */
  min-width: 0;
  gap: 10px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 14px;
}

/* Wraps, because a card can carry two pill rows now (Me vs Them has a range
   as well as an axis) and four pills where it used to have two. The title
   takes the slack rather than the gaps doing it, so the toggles stay together
   at the right instead of drifting apart as pills are added. */
.chart-card-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.chart-card-head h3 {
  margin: 0 auto 0 0;
  color: var(--dim);
}

.chart-axis-toggle { display: flex; gap: 4px; }
.chart-axis-toggle .hints-tab { margin-bottom: 0; }

/* Stands in for the canvas when the chosen range holds nothing, at the height
   the canvas would have had — a card that collapsed to its title would make
   the grid jump every time somebody pressed Today. */
.chart-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 240px;
  margin: 0;
  font-size: 13px;
  color: var(--faint);
}

/* Chart.js reads its box's height in pixels, not its aspect ratio, once
   maintainAspectRatio is off — see statscharts.js, which turns that off
   specifically so a wide two-column layout does not stretch every chart
   into an letterbox. A fixed height here is what it reads instead. */
.chart-canvas-wrap { position: relative; height: 240px; min-width: 0; }
/* ABSOLUTE, not just sized. A canvas carries its own pixel height as intrinsic
   size, so in the flow it floors the wrapper exactly the way its width floored
   the card — the rows could not shrink and the fourth chart was clipped off the
   bottom of the page instead of being made smaller. Out of the flow it can
   never decide anything about the box it is painted into, which leaves the
   wrapper free to be whatever height is left. `.chart-canvas-wrap` is already
   `position: relative`, which is what makes this the wrapper's own corner. */
.chart-canvas-wrap canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* ------------------------------------------------------------ home, phones */

/* The home screen on a phone. Everything below is layout the desktop rules
   get right by having room, and get wrong at 390px by not having any — the
   header sprawling over three lines before the first number, five stat tiles
   leaving a hole where a sixth would go, and 40px of the width spent on page
   padding. 520px rather than the 700px the charts already break at: this is
   the width where the header stops fitting, and a tablet holding two chart
   cards side by side does not want any of it. */
/* ---------------------------------------------------------------------------
   The docked column — and the phone, which is the same thing.

   The desktop shell can run this page as a sidebar pinned to the side of a
   screen, about 400px wide. A handset is 390px. They are the full interface at
   a width the full interface was never drawn for, and every rule below is about
   that; there is no second, smaller product to fall back on any more, so these
   rules are what a phone gets as well.

   Written at 520px rather than 400 so the same treatment covers a phone-sized
   browser window on a laptop, which has the identical problem.
   --------------------------------------------------------------------------- */
@media (max-width: 520px) {
  /* 40px of a 390px screen was going to side padding — a seventh of it. */
  body { padding: 12px 12px 16px; }

  /* Every tab strip wraps here. They are single-row by design at full width —
     see .modal-filter-tabs, which is called out as the exception — and that
     design silently loses a tab in a 400px column: Settings has five, and AI
     Instructions sat 114px past the right edge of the card with no way to
     scroll to it. A tab nobody can reach is worse than a second line. */
  .hints-tabs { flex-wrap: wrap; }

  /* And a notch tighter, because wrapping is the fallback and fitting is
     better. The Conversations strip missed a single row by four pixels — two
     tabs and a "+ New" needing 296 of the 292 the card had left after the
     close button's corner — so it spent a whole row on one button held out to
     the right by `margin-left: auto`. Tracking is what gives here rather than
     the size: these are already 10.5px, and the letterspacing is what makes a
     mono label wide in the first place. */
  .hints-tab { padding: 4px 9px; letter-spacing: 0.09em; }

  /* The conversation's name gets a line of its own. It is `flex: 1` in a header
     that also carries the mark, Settings, Answer, Advice and the flag, so at
     this width it was being handed 144px for 351px of text and truncated to
     "Another conversation numb…" — and the title is the one thing in that
     header that says WHICH conversation is open.

     Order rather than DOM: the header is one element for both layouts, and the
     row it wants here is identity, then name, then what you can do about it.
     The mark and Settings keep the default 0; the search box's own `order: 1`
     below never collides with this, because a title and a search box are never
     both on screen — one belongs to a conversation and the other to the home
     screen. */
  #detail-title-text {
    order: 1;
    flex: 1 0 100%;
    font-size: 16px;
    /* Wraps to a second line instead of ellipsing. At full width the name
       shares a row with everything else and truncation is the only option; on
       a line of its own it can simply be two lines, and a conversation is
       named by a whole sentence often enough — "Quarterly pipeline review with
       the Acme team" — that the tail is the part telling two of them apart. */
    white-space: normal;
    overflow: visible;
    line-height: 1.25;
  }

  /* Every control that acts on the open conversation, in one group on the
     third row. A new one added to the header has to be named here or it keeps
     the default 0 and lands ABOVE the title, among the identity controls: the
     objection button did exactly that, and rendered on its own line beside the
     hamburger with Answer and Advice — its two siblings — two rows below it. */
  #btn-help,
  #btn-advice,
  #btn-objection,
  #btn-questions,
  #btn-check-checklist,
  #btn-bookmark { order: 3; }

  /* Three rows, in the order they are wanted: what this is and where you can
     go, then which conversation, then what you can do to it. Conversations is
     pulled back up beside the mark — its `order: 2` below belongs to the home
     screen, where it sits under the search box, and without this it drifted
     down among the live actions and landed next to Answer.

     `:has()` is what tells the two screens apart without inventing a second
     class for the header: the title is in the flow exactly when a conversation
     is open, and the search box exactly when one is not. */
  .topbar:has(#detail-title-text:not(.hidden)) #btn-conversations { order: 0; }

  /* The step's explanation goes. It is the longest thing in the wizard — seven
     lines on the highlights step at this width, 113px of prose above the two
     controls it is describing — and it is the part a narrow column can most
     afford to lose: the title says what the step is and the controls under it
     are a toggle, a picker and a button.

     Only the explanation. `#wizard-alert` stays, because it is not prose about
     the step but a standing warning about what is in the box — the 400-character
     floor, and why a build would be refused. So does `#wizard-state`, and so
     does the file row's line, which reads as prose only until somebody picks a
     file and then becomes "reading CV.docx…", the character count, or the error.
     Hiding a status line because it happens to be long at rest is how a failure
     becomes silent. */
  #wizard-note { display: none; }


  /* A dialog's buttons share the row rather than sitting at their own widths.
     `.modal-actions` wraps everywhere now, which stops Next falling off the
     card; here they also grow, so four of them land as two tidy pairs instead
     of three-then-one with a ragged gap. The spacer that holds Cancel apart
     from the rest goes with them — on two lines it only pushes the wrap into
     the wrong place. */
  .modal-actions button { flex: 1 1 auto; }
  .wizard-spacer { display: none; }

  /* Two rows instead of three, and in the order they are wanted: the mark
     and the search box, then the three buttons sharing the width equally.
     Left alone, .topbar's wrap put Settings on a line of its own — the
     three buttons are 30px too wide for one line together with the mark,
     and the search's `margin-left: auto` then held it out on a third.
     `order` is what re-groups them: .topbar-home-controls is
     `display: contents`, so its buttons are flex items of the header
     itself and can be ordered beside one that is not in it. */
  .topbar { gap: 8px; }
  /* Below .mark.small again, the way .brand was pulled down from 1.25rem here:
     the mark shares this row with a search box that needs 150px of it. */
  .topbar .mark { font-size: 1.3rem; }
  /* 60% rather than 150px, and the number is doing one job: it fills the first
     row so that no BUTTON can squeeze onto it. Flex wraps greedily and decides
     where the break goes from base sizes before it grows anything, so a 150px
     basis left ~90px trailing the hamburger — enough for `+ New`, the shortest
     of the three, which then sat up beside the search with its two siblings on
     the row below. A group of three doors split 1-and-2 across two rows, by
     which of them happens to have the shortest label.
     The search grows into whatever is actually left, exactly as it did. */
  .topbar-search { order: 1; flex: 1 1 60%; margin-left: 0; }

  /* The hamburger's `order: 1` (see #session-wrap above) puts it last in this
     group, which on the home screen is enough — the search box grows into the
     gap and leaves it flush right. In a CONVERSATION the group is the
     hamburger and then the title, and the title takes a whole row of its own
     below, so the first row would end after the hamburger with the free space
     trailing it. This is what pushes it to the edge. It is safe to be here and
     not above: the only other auto margin on this row, the search box's, is
     reset to 0 on the line before, so there are never two of them dividing the
     space between them. */
  #session-wrap { margin-left: auto; }
  /* #mobile-visual is in this list rather than left to find its own place: it
     is `display: none` at every other width, so on the one screen it appears on
     it is a fourth button in a row built for three and would otherwise sit at
     its own content width beside three that share the rest. It is also
     `.hidden` for everybody but an admin, and a hidden flex item takes no room,
     so the row is the ordinary three for almost everyone. #home-quick is here
     for the same reason and is visible for everybody, so this row is now three
     wide as a rule: Conversations, +New and Quick notetaker. That last label is
     the longest of the three, so at 390px the row wraps onto a second line
     rather than ellipsing — which is the right way for it to fail. */
  #btn-conversations,
  #home-new,
  #home-quick,
  #mobile-visual,
  #btn-settings { order: 2; flex: 1 1 auto; justify-content: center; }

  /* Tighter, and the meter heads wrap onto their own line rather than
     squeezing the figure — at 400px "Assisted calls" and "2.7 / 3 h" do not
     share a line without one of them ellipsing. */
  .home-stats { padding: 10px 12px; gap: 7px; }
  .usage-meter { grid-template-columns: 1fr; }
  .usage-meter-value { grid-row: 2; }

  /* Four cards of chart plus their heads is a long scroll on a phone, and
     the height is the only part of it that is not carrying information. */
  .chart-card { padding: 12px; gap: 8px; }
  .chart-canvas-wrap, .chart-empty { height: 200px; }

  /* The pills are the densest thing on the page and the first to wrap into
     an extra row each. Slightly smaller and closer, they keep an axis of
     four on one line at this width — including Me vs Them, which carries a
     range row as well. */
  .chart-axis-toggle { gap: 2px; }
  .chart-axis-toggle .hints-tab { font-size: 11px; padding: 3px 7px; letter-spacing: 0.02em; }
}

/* --------------------------------------------------------------- lane dots */

/* The proof both halves are live without unfolding the Audio panel. Grey until
   that lane has actually carried sound, so a flat lane is visible at a glance. */
.lane-dots {
  display: flex;
  gap: 7px;
  margin: 7px 0 0;
  align-items: center;
}

.lane-dot { display: flex; align-items: center; }

.lane-dot i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--line-strong);
  display: inline-block;
  transition: background 120ms linear;
}

.lane-dot.live i { background: var(--good); }
/* Carried sound once, but nothing recently: the lane may have died. */
.lane-dot.stale i { background: var(--warn); }

/* --------------------------------------------------------------- talk time */

.talk-time {
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid var(--line);
  font-size: 11px;
}

.talk-bar {
  height: 4px;
  border-radius: 2px;
  background: var(--them);   /* the far end fills the remainder */
  overflow: hidden;
  margin-bottom: 4px;
}

/* Your share, drawn from the left. */
.talk-fill {
  height: 100%;
  background: var(--good);
  width: 0;
  transition: width 400ms linear;
}

.talk-time.talking-too-much .talk-fill { background: var(--warn); }

/* ------------------------------------------------------------- write-ups */

.wide-card { max-width: 880px; }



/* One tab per section the mode splits out, plus the brief. Wraps rather than
   scrolls: these are four short labels, and a strip that scrolls sideways hides
   the one you wanted. Same chip as the Help/Advice tabs, so the two read as the
   same control. */
.writeup-tabs { flex-wrap: wrap; }

/* Fills whatever the card has left rather than claiming a share of the
   viewport, and scrolls inside it.

   It was `max-height: 60vh`, which ADDED to the card's own 78vh instead of
   fitting inside it: the head, the note, a tab strip that wraps to two rows for
   a seven-section prep and the actions come to about 245px, so the two only
   coexisted on a viewport taller than 1356px. Below that — every real screen —
   a full body pushed the Build/Copy/Done row out of the card.

   `min-height: 0` because a flex item's default floor is its content size,
   which is what stops an overflow-y child from ever actually scrolling; the
   same trap `.conversation-list` and `.layout` carry the same line for. */
.writeup-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-right: 6px;
}

.writeup-section { margin-bottom: 18px; }

.writeup-section h3 {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
  margin: 0 0 6px;
}

.writeup-section ul { margin: 0; padding-left: 18px; }
.writeup-section li { margin-bottom: 5px; line-height: 1.5; }
.writeup-section p { margin: 0; line-height: 1.6; }

.writeup-empty { color: var(--dim); font-style: italic; }

/* A write-up built before templates: headings this build cannot parse, so it is
   shown exactly as it was stored rather than guessed at. `pre-wrap` because it
   is a document with its own line breaks, not code. */
.writeup-legacy {
  white-space: pre-wrap;
  font-family: inherit;
  line-height: 1.6;
  margin: 0;
}

/* A tab is a unit of work here, not only a view, so the strip has to say which
   parts exist yet. Two states beyond the ordinary one, and both are quieter
   rather than louder: an unbuilt part is not a warning, and a running one is
   already reported by the progress box. No hue on either — see the palette
   rules; what these mean is "not yet", which is what --faint is for. */
.hints-tab.tab-pending { color: var(--faint); border-style: dashed; }
.hints-tab.tab-running { color: var(--text); border-color: var(--me); }

/* ------------------------------------------------------------ pre-flight */

.preflight-lanes { display: flex; flex-direction: column; gap: 14px; margin: 4px 0 16px; }

.preflight-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 12px;
  margin-bottom: 5px;
}

.preflight-tick { color: var(--line-strong); font-size: 14px; }
.preflight-lane.ok .preflight-tick { color: var(--good); }
.preflight-lane.ok .preflight-head { color: var(--good); }

/* Ghost, not accent: it belongs to the far end's lane and is one of two things
   that can prove it, beside simply playing sound on the call. Chrome, in the
   file's own terms — the emphasised control on this dialog is the start, and
   there is only ever one of those. */
.preflight-tone {
  margin-top: 8px;
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--dim);
  font-size: 12px;
  padding: 4px 10px;
  border-radius: 999px;
}

.preflight-tone:hover:not(:disabled) { color: var(--text); border-color: var(--text); }
.preflight-tone:disabled { opacity: 0.5; }

/* What the test tone proved about echo cancellation. Shown in the pre-flight
   only when it went wrong, and in the Audio panel either way. */
.echo-warning {
  font-size: 12px;
  line-height: 1.45;
  color: var(--bad);
  border-left: 2px solid var(--bad);
  padding-left: 9px;
  margin: 0 0 14px;
}

/* Hover for the working, click to copy it. */
.echo-status { font-size: 11px; margin-top: 8px; cursor: pointer; color: var(--dim); }
.echo-status.bad { color: var(--bad); }

/* The echo gate sits in the sidebar, directly under the capture button: it acts
   on the capture that is running, and it belongs beside the control that started
   it rather than floating over the page. Above the two folded panels, because a
   remedy you have to open a panel to find is one nobody finds mid-call. */
.echo-gate { margin-top: 10px; }

.echo-gate-button {
  display: block;
  width: 100%;
  border: 1px solid var(--bad);
  border-radius: 9px;
  background: var(--bad);
  color: var(--on-bad);
  font: inherit;
  font-size: 13.5px;
  font-weight: 600;
  padding: 9px 14px;
  cursor: pointer;
}

/* Off is the loud state, because off is the state that needs pressing. Once it
   is on the button has done its job and should stop competing with the call. */
.echo-gate-button[aria-pressed="true"] {
  background: color-mix(in srgb, var(--bad) 14%, transparent);
  color: var(--bad);
}

/* Leads the note rather than sitting in the button, because the button is mono
   uppercase and cannot wrap — see the comment on it in index.html. It keeps the
   warning colour against the note's --dim so the number still reads as the thing
   being spent, and it is inside that paragraph rather than on a line of its own
   so an empty count costs no vertical space. */
.echo-gate-count { color: var(--bad); font-weight: 600; }

.echo-gate-note {
  margin: 6px 2px 0;
  font-size: 11px;
  line-height: 1.4;
  color: var(--dim);
}

/* Parked into the header by `applyCompactControls` whenever there is no room
   beside the transcript for `#controls` — narrow width or the desktop shell's
   short captions window alike, see mobile.js. `.echo-gate-button` above is
   `width: 100%` because the sidebar is a fixed column; as a bare flex item in
   the header row that 100% would resolve against nothing and the button would
   either collapse or blow the row out, so it takes its own line and a size of
   its own here instead. No `order` here: the narrow layout's own header rules
   set one, below 700px, to slot it in after Set-up and the flush warning; the
   wide short window has no competing `order` to sort against, so the plain DOM
   position `park()` left it in — right after `#save-state` — is already
   correct there. */
.compact-echo-gate {
  flex: 1 0 100%;
  margin: 6px 0 0;
}

.compact-echo-gate .echo-gate-button {
  width: auto;
  display: inline-block;
  padding: 7px 14px;
  font-size: 12.5px;
}

/* ---------------------------------------------------------------- search */

.search-results { margin-bottom: 18px; }

.search-hit {
  display: block;
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: inherit;
}

.search-hit:hover { background: var(--panel); }

.search-hit-meta {
  display: block;
  font-size: 11px;
  color: var(--dim);
  margin-bottom: 3px;
}

.search-hit-text { display: block; line-height: 1.5; }
/* snippet() marks the matched terms with << >>, swapped for these on render. */
.search-hit-text mark { background: color-mix(in srgb, var(--good) 22%, transparent); color: inherit; border-radius: 2px; }

/* ------------------------------------------------------------------ flags */

/* A moment marked during the call. Deliberately loud in the history: the whole
   point is to find it again afterwards. */
.turn.flagged { border-left: 2px solid var(--warn); padding-left: 8px; }
.turn-flag {
  background: none;
  border: none;
  color: var(--line-strong);
  cursor: pointer;
  font-size: 12px;
  padding: 0 4px;
}
.turn.flagged .turn-flag { color: var(--warn); }
.turn-flag:hover { color: var(--warn); }

/* Open questions answering "nothing" is the good outcome, so it reads as a
   clear result rather than as a failure or an empty waiting state. */
.hints-clear { color: var(--good); }

/* The two folded panels sit together as a pair, so the second needs a hairline
   of its own rather than reading as part of the first. */
.panel-block + .panel-block { margin-top: 8px; }

/* --------------------------------------------------------------- mode menu */

/* What a conversation is decides everything under it, so it sits at the top of
   the sidebar. Same shape as the Advice menu, but full width, because a sidebar
   control has the room and a truncated "Mode: Assisted meeting" reads badly. */
.mode-wrap { width: 100%; margin-bottom: 2px; }

.mode-button {
  width: 100%;
  justify-content: space-between;
  font-weight: 600;
}

/* The caret is drawn rather than typed, so the label can be set as plain text
   from one place without carrying markup with it. */
.mode-button::after { content: '\25BE'; margin-left: 8px; color: var(--faint); }

/* The microphone picker's own label is written by the OPERATING SYSTEM, not by
   anybody here — "Default - Headset (Realtek(R) Audio)" is an ordinary one —
   so it is the only mode-button whose text can outrun a 270px column. Ellipsed
   rather than wrapped: this is a row of a column that cannot grow, and the
   whole name is on the menu entry a press away. `min-width: 0` is the half that
   does the work — a flex item's default `auto` floors it at its own content
   width, which is the same trap `.layout` and the chart cards carry a note
   about. The caret keeps its place because it is a separate box. */
#btn-mic { display: flex; align-items: center; justify-content: center; }
/* `min-width: 0` is the half that does the work: a flex item's default `auto`
   floors it at its own content width, so without it the label refuses to shrink
   and pushes the caret out of the button instead of ellipsing — the same trap
   `.layout` and the chart cards each carry a note about. */
#mic-label { overflow: hidden; text-overflow: ellipsis; min-width: 0; }

.mode-wrap .advice-menu { width: 100%; min-width: 0; }
.mode-wrap .advice-menu span { color: var(--faint); }

/* Beside a help dot — in the sidebar for the capture type and the mode, or in
   the settings dialog for anything else shaped like it — the picker takes the
   room that is left instead of the whole width, exactly as the selects in the
   rows around it do. */
.option-row .mode-wrap { flex: 1; min-width: 0; margin-bottom: 0; }

/* The capture-type and mode rows sit directly in the sidebar rather than
   inside a folded panel, so they need their own spacing rather than borrowing
   a <details> block's. */
#controls > .option-row { margin-bottom: 8px; }

/* ---------------------------------------------------------- capture button */

/* One control for start, stop, what is happening and how long it has been
   happening. It changes colour rather than moving, so the thing you reach for
   mid-call is always in the same place. */
/* Full width on its own; inside .option-row, beside its `?`, it takes the
   room that is left instead, the same rule .auto-answer-toggle follows
   below. */
.option-row .capture-toggle { width: auto; flex: 1; }
.capture-toggle {
  width: 100%;
  padding: 9px 12px;
  font-size: 13.5px;
  font-weight: 600;
  /* The elapsed time is inside the label, so the digits must not reflow it. */
  font-variant-numeric: tabular-nums;
}

/* Recording. Amber rather than red: red now means something is wrong — it is
   the colour of the echo gate, which appears in this same column and says the
   far end is being written down as you. A capture running normally is not a
   problem, and two red controls a few pixels apart would make the one that is a
   problem read as ordinary. Filled amber against the outlined amber of
   `is-waiting` below: same hue for "the capture is doing something", filled for
   running and outlined for mid-flight. */
.capture-toggle.is-live {
  background: var(--warn);
  border-color: var(--warn);
  color: var(--on-warn);
}

/* Connecting or finishing: mid-flight, and not pressable either way. */
.capture-toggle.is-waiting {
  background: transparent;
  border-color: var(--warn);
  color: var(--warn);
  opacity: 1;
}

/* The two write-up rows read as document rows: one `?`, one full-width button.
   `.option-row` is `justify-content: space-between`, so without this the help
   dot and the button are pushed to opposite ends of the row with a gap down the
   middle — which is the shape every other row in the sidebar is not.

   They sit directly in the sidebar now rather than inside a panel, which the
   rule never cared about: what it is about is a `?` beside a button. */
#prebrief-row .doc-button,
#debrief-row .doc-button { flex: 1; text-align: left; }

/* One control, three states, cycling on a press — off/fast/smart — with
   colour as the only thing that tells them apart: grey at rest, green for
   the cheap model, blue for the strong one. Sits in an .option-row beside its
   `?`, so it takes the room .mode-wrap does there. */
.option-row .auto-answer-toggle { flex: 1; text-align: center; font-weight: 600; }
.auto-answer-toggle {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--faint);
}
.auto-answer-toggle:hover:not(:disabled) { border-color: var(--dim); color: var(--dim); }
/* Two attributes, one vocabulary. Auto answers writes `data-state`, the visual
   assistant's reply switch writes `data-value`; same two-position shape, so the
   same two colours must keep meaning the same two things. Named together for
   that reason rather than tinted twice, where one copy can drift. */
.auto-answer-toggle[data-state="fast"],
.auto-answer-toggle[data-value="fast"] {
  background: color-mix(in srgb, var(--good) 14%, transparent);
  border-color: var(--good);
  color: var(--good);
}
.auto-answer-toggle[data-state="smart"],
.auto-answer-toggle[data-value="smart"] {
  background: color-mix(in srgb, var(--me) 14%, transparent);
  border-color: var(--me);
  color: var(--me);
}

/* --------------------------------------------------- the visual assistant */

#camera-task { resize: vertical; min-height: 62px; }
#camera-shoot { width: 100%; }

/* The follow-up box and its Send on one line: what is typed here is always
   about the photograph just taken, so it reads as part of that exchange
   rather than as a second control somewhere else. */
.camera-ask-row { display: flex; gap: 6px; }
.camera-ask-row input { flex: 1; min-width: 0; }
.camera-ask-row button { flex: 0 0 auto; }

/* Empty unless something is in flight or has just failed, so it takes no room
   the rest of the time — same rule .save-state follows. */
#camera-state:empty, #camera-task-state:empty { display: none; }
#camera-state, #camera-task-state { font-size: 11.5px; }

/* Zero, one or two actions in the alert, so the container has to collapse. */
.alert-extra { display: contents; }

/* The two lists are a tab strip above the create form, not part of the card
   header, so they need a little air under them. */
.list-tabs { margin-bottom: 12px; }

/* Reserve the corner the floating × sits in. `#modal-close` is out of the flow
   and positioned against the card (`.modal-close-float`, 14px in), so the row
   that starts at the top of the card runs straight under it — and the far end of
   that row is exactly where `+ New` is pushed by `margin-left: auto`. Two hit
   targets stacked on one another, and the × underneath.
   34px clears the glyph, its padding and the inset it sits at.

   **At every width, which is the correction.** This lived inside the 520px
   block, because a phone is where it was first noticed. Nothing about it is
   narrow-specific: the × is positioned against the card whatever the viewport
   is, and so is the button, so the collision was there on a full-size screen
   too. A rule about two elements that are both positioned relative to the same
   box does not belong in a media query.

   The id, not `.modal-wide .list-tabs`: the filter row below carries that class
   too, and it starts about 58px down — well clear of a × that ends at 36px. Given
   the padding it did not need, its pills would stop 34px short of a right edge
   every row beneath them runs to. Only the row the × actually covers reserves. */
#modal-tabs { padding-right: 34px; }

/* Its own width, not a wider .modal-narrow: that class is shared with the alert
   and busy dialogs, and a sentence of confirmation text does not want the room
   two capture cards and a mode menu do. 30% over the 440px they keep. */
/* A fixed 90% of the viewport rather than the height of whatever step it is
   showing. The dialog's content changes as you fill it in — picking a mode
   reveals a description block — so a content-sized card grew and shrank under
   the cursor between one click and the next. At a constant height the controls
   stay where they were put.

   dvh, not vh, for the reason the home screen uses it: on a phone-shaped window
   vh is the height with the browser chrome retracted, which is more than is
   actually visible. Below 700px this loses to the full-screen block at the foot
   of the file, which names this same selector — there the dialog is the whole
   screen and 90% of it would be a gap at the bottom for nothing. */
#new-modal .modal-card {
  max-width: 572px;
  height: 90dvh;
  max-height: 90dvh;
  overflow-y: auto;
}

/* The two questions a conversation is created with — how you are joining, and
   what kind of conversation it is — each a small label over its control. They
   are the wizard's first two steps, moved here; the capture cards reuse
   .wizard-choice so the one question looks the same wherever it is asked. */
.new-field { display: flex; flex-direction: column; gap: 6px; }
.new-field-label { font-size: 12px; color: var(--dim); }
.new-field-detail:empty { display: none; }
.new-field-detail { font-size: 12px; line-height: 1.45; }

/* Details… and what has been answered behind it, on one line at the foot of the
   New form. The state line is what stops a dialog that saves nothing until the
   conversation exists from looking as though it forgot everything. */
.new-details-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.new-details-row .dim { font-size: 11.5px; min-width: 0; }

/* ------------------------------------------------- conversation settings */

/* The dialog holding what a MEETING is: its category, its links, when it is due
   and the notes beside it. Rows stack, because every one of them is a field
   with a name above it rather than a control beside a question — see .field,
   which does the stacking; this only spaces them.

   It sets a CEILING of its own now and nothing else. It holds everything the
   sidebar's Conversation panel used to as well as everything it already had —
   two tabs of it — and 540px is the width the far side's name, a 60-language
   select and a schedule were all being asked to share. The escape from the
   ceiling stays `.modal-card`'s own `overflow-y: auto` and the `flex-shrink: 0`
   floor under its children, which is why neither is restated here: a variant
   that sets a height without inheriting those is how the wizard once put its
   Next button outside its own card.

   `max-width`/`max-height` have to be released beside the width and the height,
   because `.modal-card` caps every dialog at 540px and 78vh — a width alone is
   silently held to that, which is a card claiming to be wider than it renders.
   Same four lines, and the same reason, as #template-modal above. */
#convsettings-modal .modal-card {
  width: min(720px, 96vw);
  max-width: none;
  height: auto;
  max-height: 88vh;
}
#convsettings-modal .field { margin-top: 12px; }

/* The tab strip, and the panes under it. `.hints-tabs`/`.hints-tab` are the
   app's one tab treatment — the answers strip, Settings, Organisation and the
   write-up dialog all wear it — so this needs nothing but a little room above
   the first field, which `.field`'s own `margin-top` does not supply after a
   strip rather than after another field. */
#convsettings-tabs { margin-top: 12px; }
.convsettings-pane > .option-row { margin-top: 12px; }
/* A `.field` here wraps its control in an `.option-row` so the `?` can sit
   beside it — the shape the category row already used. `.field` is a flex
   column with a 4px gap, which is the spacing between a heading and its
   control; the row inside needs none of its own on top of that. */
.convsettings-pane .field > .option-row { margin-top: 0; }

/* The category button's own contents, left-aligned like every field beside it.
   `.mode-button` is `space-between`, which is right in the sidebar where the
   button holds one label and a caret — but this one also carries a colour dot,
   so three children divided evenly put the label in the middle of the control
   while the four boxes under it start at the left edge. The caret keeps its
   place with an auto margin. */
#category-row .mode-button { justify-content: flex-start; gap: 8px; }
#category-row .mode-button::after { margin-left: auto; }
#convsettings-modal textarea { min-height: 90px; resize: vertical; }

/* ---- the date picker's palette ----

   Air Datepicker (static/vendor/air-datepicker.js, MIT, vendored for the reason
   Chart.js is) is the one third-party widget in this app, and it is themed
   entirely through its own `--adp-*` variables — so the vendor file stays
   BYTE FOR BYTE what was downloaded and replacing it with a newer release is a
   copy rather than a merge. Do not edit that file; add a variable here.

   Every value below is one of this app's own tokens. That is the whole point:
   a calendar in somebody else's blue, with somebody else's radius, is exactly
   the "looks like a third-party widget" this replaced. `--me` for the selected
   day, because it is a selection rather than a warning or a state — the same
   token the active conversation row uses for the same job. */
.air-datepicker {
  --adp-font-family: var(--sans);
  --adp-font-size: 13px;
  --adp-width: 268px;
  --adp-z-index: 40;
  --adp-padding: 8px;
  --adp-border-radius: 10px;
  --adp-border-color: var(--line-strong);
  --adp-border-color-inner: var(--line);
  --adp-background-color: var(--panel);
  --adp-background-color-hover: var(--raise);
  --adp-background-color-active: var(--raise);
  --adp-color: var(--text);
  --adp-color-secondary: var(--faint);
  --adp-accent-color: var(--me);
  --adp-color-current-date: var(--me);
  --adp-color-other-month: var(--faint);
  --adp-color-disabled: var(--faint);
  --adp-color-other-month-hover: var(--dim);
  --adp-cell-border-radius: 6px;
  --adp-cell-background-color-hover: var(--raise);
  --adp-cell-background-color-selected: var(--me);
  --adp-cell-background-color-selected-hover: var(--me);
  --adp-nav-color-secondary: var(--dim);
  --adp-nav-arrow-color: var(--dim);
  --adp-day-name-color: var(--dim);
  --adp-day-name-color-hover: var(--text);
  --adp-time-track-color: var(--line-strong);
  --adp-time-track-color-hover: var(--dim);
  --adp-time-day-period-color: var(--faint);
  --adp-btn-color: var(--text);
  --adp-btn-color-hover: var(--text);
  --adp-btn-background-color-hover: var(--raise);
  --adp-btn-background-color-active: var(--raise);
  --adp-btn-border-radius: 6px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}

/* The little arrow joining the calendar to the field. Its background is a
   literal `#fff` in the vendor file — the one colour in there that is not a
   variable — so it is the one thing here that has to be named by selector
   rather than re-pointed. Without this it is a white diamond on a dark dialog.
   `--adp-border-color-inline` is what draws its two edges, so that is set with
   it rather than left as the library's own grey. */
.air-datepicker { --adp-border-color-inline: var(--line-strong); }
.air-datepicker--pointer:after { background: var(--panel); }

/* The day names and the two buttons are furniture, so they take the app's
   label voice — see LABEL VOICE at the top of this file. Written here rather
   than added to that list because the selectors belong to a vendored file and
   this whole block is where its appearance is decided. */
.air-datepicker-body--day-name,
.air-datepicker-button {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* The selected day takes dark ink, like every other bright fill in this app —
   `--on-me` is `--me`'s partner and moves with it, which a hand-picked hex
   would not. The library sets `#fff` on the cell. */
.air-datepicker-cell.-selected-,
.air-datepicker-cell.-selected-.-current-,
.air-datepicker-cell.-selected-:hover { color: var(--on-me); }

/* Which conversation this is about, under the heading. Two of the three doors
   open this from somewhere that does not otherwise name it — a row in a long
   list, and a conversation that does not exist yet. */
.convsettings-note { margin: 0; font-size: 12px; line-height: 1.45; }

/* What the date row does, under the date row. Faint rather than dim: it is an
   explanation of a control, which is the same job the sentence under a wizard
   card does. */
.convsettings-hint { margin: 4px 0 0; font-size: 11px; line-height: 1.4; color: var(--faint); }

/* Its own overflow rather than the wizard's: this grid sits inside a short
   dialog beside two other controls, not alone on a step that owns the height.
   The cards stack in a narrow modal, which is what auto-fit already does. */
.new-choices { overflow: visible; }
.new-choices .wizard-choice { padding: 10px 12px; }

/* --------------------------------------------------------- paste-and-assess

   The New dialog's third tab, and the two dialogs after it — see
   static/assess.js. Every field reuses an existing class (.field,
   .option-row, .mode-wrap, .new-field) rather than a fresh one; what follows
   is only what is genuinely new. */

/* The paste box's own tab: a plain textarea sized like the wizard's, plus a
   running character count beneath it in the same voice every other counter
   in this app takes. */
#assess-text { min-height: 200px; resize: vertical; }
.assess-text-count {
  text-align: right;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

/* The job-description/scenario rows: a preview rather than an editable box,
   because there is nowhere yet to save what would be typed into one — see
   the module docstring in assess.js. Faint, like every other explanation of
   a control on this page, and never mistaken for the field's own value. */
.assess-longtext-preview {
  margin: 4px 0 8px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--faint);
  white-space: pre-wrap;
}
/* The second field inside the scenario row (session details), named rather
   than left to read as a continuation of the one above it. */
.assess-longtext-label {
  display: block;
  margin-top: 10px;
  font-size: 12px;
  color: var(--dim);
}

/* Step 3's progress list: several rows visible at once, which is the one
   thing generating()'s shared busy box does not do — see runAssessPipeline.
   The mark is a plain circle rather than an icon font, coloured by state
   alone, the same restraint the rest of this app's status dots keep. */
.assess-progress-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.assess-progress-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.assess-progress-mark {
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--line-strong);
  align-self: center;
}
.assess-progress-label { flex: 0 0 auto; }
.assess-progress-detail { flex: 1; min-width: 0; }

/* Four states, and the mark is the only thing that carries a colour — a
   running stage pulses the app's one halo, the same way a live capture does,
   because it is the one row on this screen actually worth a glance. Pending
   is what every row starts as (a stage that does not apply to this
   conversation gets no row at all — see runAssessPipeline — so there is no
   fifth, greyed-out "skipped" state to draw here any more), drawn dim rather
   than full brightness so a glance at the list says how much is left rather
   than only what has happened so far. */
.assess-progress-row.is-pending { color: var(--dim); }
.assess-progress-row.is-running .assess-progress-mark {
  background: var(--warn);
  --halo: var(--warn);
  animation: capture-halo 1.6s ease-out infinite;
}
.assess-progress-row.is-done .assess-progress-mark { background: var(--good); }
.assess-progress-row.is-pending .assess-progress-mark { background: var(--line-strong); }
.assess-progress-row.is-failed .assess-progress-mark { background: var(--bad); }
.assess-progress-row.is-failed .assess-progress-detail { color: var(--bad); }

@media (prefers-reduced-motion: reduce) {
  .assess-progress-row.is-running .assess-progress-mark { animation: none; }
}

/* The hint sits beside the mode's name, not flung to the far edge of the menu.
   `.advice-menu span` pushes it right with `margin-left: auto`, which reads
   fine on the sidebar's own ~200px menus and badly on this one: at the width of
   the dialog it left half a line of empty space between "Sales Call" and
   "selling to a customer" and the two stopped reading as one entry. */
#new-mode-menu span { margin-left: 0; }
#new-mode-menu button { align-items: baseline; }

/* ------------------------------------------------- the panel beside the answers */

/* Two thirds the answers, one third the panel. A ratio rather than the fixed
   column it was, because what is in the panel is now sentences to read out
   loud rather than five short lines to glance at, and a fixed 236px wrapped
   every one of them onto four rows. */
/* --hints-split is the answers column's share, in flex-grow units that read
   directly as a percentage because both children share flex-basis 0 — see
   .hints-main/.side-panel below. 66.67 is the old fixed 2:1 ratio, kept as the
   default so a reader who has never dragged it sees exactly what they always
   saw. app.js clamps every value it ever writes here to [33.33, 66.67]: the
   two columns may never differ by more than a third, which also rules out a
   1:3 (25%) split some reader might otherwise drag to. */
.hints { display: flex; align-items: flex-start; --hints-split: 66.67; }
.hints-main { flex: var(--hints-split) 1 0; min-width: 0; }

/* The drag handle that sits between them, replacing the plain gap the layout
   used to have — its own width (16px) is what used to be .hints's `gap`, so
   the resting visual spacing is unchanged; only now it is grabbable, and the
   divider line sits centred inside it. Hidden together with .side-panel,
   since a handle with nothing on the other side of it is not a control. */
.hints-drag {
  flex: 0 0 auto;
  align-self: stretch;
  width: 16px;
  cursor: col-resize;
  position: relative;
}
.hints-drag::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  background: var(--line);
  transform: translateX(-50%);
  transition: background 120ms;
}
.hints-drag:hover::before,
.hints-drag.dragging::before,
.hints-drag:focus-visible::before { background: var(--me); }
.hints-drag:focus-visible { outline: none; }

/* Dragged well past where the two-sided range stops — not just to its edge,
   past it — reads as "get this out of my way" rather than "make it as small
   as the range allows", so the side is hidden outright instead of pinned at
   its narrowest legal width. --hints-split itself is untouched underneath,
   which is what dragging back from the edge resumes at.

   `display: none`, rather than the zeroed-out box this was: a column squeezed
   to `width: 0` is still IN the row, so every word in it wrapped onto a line
   of its own and the flex line took that height. Collapsing therefore made the
   box 61px TALLER while emptying it — an answers panel holding five blank
   bullets with sixty pixels of nothing under them, which is what "the whole
   thing jumps" looks like. `overflow: hidden` hid the words and could do
   nothing about the height they had already claimed. It also left the hidden
   column's own tabs in the tab order, so keyboard focus walked into a panel
   with no width. Out of the flow, the surviving column is the only flexible
   item left in the row and takes it without being told to. */
.hints.collapsed-main .hints-main,
.hints.collapsed-side .side-panel { display: none; }

/* While a side is hidden, the handle sits at the very edge doing nothing
   visible — so it pulses to say it is still a control, not furniture, and
   that dragging it back is how the hidden side returns. Stops the instant a
   fresh result reveals that side on its own; see revealHintsSplitIfCollapsed
   in app.js. */
@keyframes hints-drag-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
.hints-drag.collapsed { animation: hints-drag-pulse 1.8s ease-in-out infinite; }
.hints-drag.collapsed::before { background: var(--me); }

.side-panel {
  flex: calc(100 - var(--hints-split)) 1 0;
  min-width: 0;
  align-self: stretch;
}

/* The tab strip is the answer box's, at the size the panel's headings were:
   these are labels on a column, not a second set of controls competing with
   the ones above the hints. */
.side-tabs { margin-bottom: 8px; }
.side-tabs .hints-tab { font-size: 11px; padding: 3px 8px; }
.side-panel .hints-stamp,
#side-stamp { margin-left: auto; font-size: 11px; color: var(--dim); }

.side-panel ul { list-style: none; margin: 0; padding: 0; }

/* ---- and below 700px the two columns become two rows ----

   The split is a share of the row, so it shrinks with the row and never stops:
   in a 400px column the side panel resolved to 109px, and "AUTO ANSWERS -
   SMART" is a 162px `white-space: nowrap` label. It ran straight out of its own
   column, across the answers beside it and 27px off the right edge of the page
   — two tab strips printed on top of each other, which is what a 400px docked
   window actually looked like.

   700px is `NARROW_PX`, the width at which this app already says there is no
   room for two things beside each other — the set-up block becomes a dialog on
   the same number. A breakpoint of its own here would be a fifth width to keep
   in step for the sake of the ~40px between where it stops being comfortable
   and where it starts being broken.

   Written AFTER the rules it overrides rather than inside the 520px block
   above, which is where it was first tried: media queries add no specificity,
   so `.hints` there loses to the identical `.hints` written later in the file.

   Immediately after this block, the section it belongs to. */
@media (max-width: 700px) {
  .hints { flex-direction: column; }

  /* Both children stop dividing a row and simply take the width. `flex: 1 1
     auto` rather than the percentage basis: stacked, the share means nothing,
     and a `flex-basis: 0` in a column would make each one exactly as tall as
     the other's share of the height. */
  .hints-main,
  .side-panel { flex: 1 1 auto; width: 100%; min-width: 0; align-self: auto; }

  /* A column-resize handle between two stacked blocks is not a control. */
  .hints-drag { display: none; }

  /* And a side collapsed by dragging on a wide screen must come back here.
     That state is a preference about how to divide a row; with no row to
     divide it would be half the answer box missing, with the handle that
     restores it hidden by the rule above. */
  .hints.collapsed-main .hints-main,
  .hints.collapsed-side .side-panel { display: block; }

  /* The gap the handle used to hold, plus a rule to say the second block is a
     different thing from the first rather than more of it. */
  .side-panel {
    margin-top: 12px;
    padding-top: 11px;
    border-top: 1px solid var(--line);
  }
}

/* --------------------------------------------------------- auto answers */

/* One entry per question they have left hanging, newest first. The newest is
   the one being waited on, so it is the one that reads at full weight; the
   rest are what is still owed and sit under it, quieter. */
.auto-list { list-style: none; margin: 0; padding: 0; }

.auto-entry { margin-bottom: 11px; }
.auto-entry:last-child { margin-bottom: 0; }

.auto-question {
  margin: 0 0 2px;
  font-size: 11px;
  line-height: 1.35;
  color: var(--dim);
}

.auto-answer {
  margin: 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--dim);
}

/* Bold and white, and only the top one. Something has to be the line you say
   now, and a panel where every entry shouts equally is one you have to read
   from the top every time it changes. */
.auto-entry.is-newest .auto-answer { font-weight: 600; color: #fff; font-size: 14px; }
.auto-entry.is-newest .auto-question { color: var(--text); }

.auto-body .hints-message { font-size: 12px; }

.checklist-item {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 12px;
  line-height: 1.4;
}

/* Red, amber, green. The dot carries the state and the text stays readable at
   every one of them, so the colour is a signal rather than a legibility cost. */
.checklist-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-top: 4px;
  background: var(--bad);
}

.state-yellow .checklist-dot { background: var(--warn); }
.state-green .checklist-dot { background: var(--good); }

.checklist-text { color: var(--dim); }
.state-yellow .checklist-text { color: var(--text); }
.state-green .checklist-text { color: var(--text); text-decoration: line-through; text-decoration-color: var(--good); }

/* --------------------------------------------------------------- bookmark */

/* Amber rather than blue: the three beside it each spend a model call, and this
   one only writes a flag. Same colour as the mark it leaves in the history. */
/* Square, and the glyph is the one it leaves on the line. Sized off the same
   7px vertical padding its neighbours use, so the row of buttons still lines
   up: a fixed height here would drift the moment the others change. */
.bookmark-button {
  flex: 0 0 auto;
  width: 30px;
  padding: 7px 0;
  font-size: 15px;
  line-height: 1;
  text-align: center;
  border-color: color-mix(in srgb, var(--warn) 55%, transparent);
  color: var(--warn);
}

.bookmark-button:hover:not(:disabled) { border-color: var(--warn); }

/* ----------------------------------------------------------------- wizard */

/* One scaffold with its parts shown per step, so the styling is of the parts
   rather than of eleven panels. The card is the tall editor's, because it is
   the same kind of screen: a box you read and change, with a row of controls
   that act on it. */

/* Sized to the step rather than to the tallest step: two choice cards do not
   need eighty per cent of the window.

   A step with a box keeps the tall editor's fixed 80vh, and for the reason that
   number is 80 rather than 90 — `.modal` reserves 8vh above the card and 20px
   below it, so anything taller pushes its own footer off the bottom of the
   screen. The box is the flex child that absorbs the difference, and it has to
   be able to shrink to nothing to do that: a `min-height` on it stops the card
   clamping and the buttons walk off the page again. */
#wizard-modal .modal-card { height: auto; max-height: 80vh; }

/* Writing one write-up template: nearly the whole window, over the Settings
   dialog. Five parts of five headings is more than a pane inside another
   dialog can hold without becoming a scroll inside a scroll, and what is being
   written here is prose — instructions to a model — which wants the width.

   A CEILING only; the escape from it is `.modal-card`'s own `overflow-y: auto`
   and the `flex-shrink: 0` floor under its children, which is why neither is
   restated here. A variant that set a height without inheriting those is how
   the wizard once put Next 32px outside its own card. */
#template-modal .modal-card,
#objection-modal .modal-card {
  /* All four, because `.modal-card` caps every dialog at 540px and 78vh — a
     width and a height alone are silently held to those, which is a card
     claiming to be nearly the whole window and rendering at 42% of it. */
  width: min(1080px, 96vw);
  max-width: none;
  height: 92vh;
  max-height: none;
}
#template-modal, #objection-modal { z-index: 25; }
#wizard-modal.wizard-tall .modal-card { height: 80vh; }

/* Which steps this mode has. A step already reached can be clicked back to;
   one not yet reached cannot, because its own answer often decides whether the
   steps after it exist at all. */
.wizard-progress {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-bottom: 4px;
}

.wizard-pill {
  padding: 3px 9px;
  font-size: 11.5px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--faint);
}

.wizard-pill.done { color: var(--dim); border-color: var(--line); }
.wizard-pill.current {
  color: var(--bg);
  border-color: var(--text);
  background: var(--text);
}
.wizard-pill:disabled { cursor: default; opacity: 0.55; }
.wizard-pill:not(:disabled):hover { color: var(--text); border-color: var(--dim); }

/* The reason the step exists, not a restatement of its title. Long on purpose:
   this is where the trade-off behind each choice is explained, and it is the
   only place most of it is ever read. */
.wizard-note {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--dim);
}

/* A card each, laid out side by side when there is room. The detail is what
   makes the choice answerable, so it is part of the control rather than a
   tooltip on it. */
.wizard-choices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  align-content: start;
  gap: 10px;
  overflow-y: auto;
}

.wizard-choice {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 13px;
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: transparent;
  height: 100%;
  /* Buttons are nowrap everywhere else in this app, which is right for a label
     and wrong for a paragraph: without this the detail runs off the side and
     the dialog grows a horizontal scrollbar. */
  white-space: normal;
}

.wizard-choice strong {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--text);
}
.wizard-choice span { font-size: 12px; line-height: 1.5; color: var(--faint); }
.wizard-choice:hover { border-color: var(--dim); }
.wizard-choice.chosen {
  border-color: var(--text);
  background: rgba(255, 255, 255, 0.05);
}
.wizard-choice.chosen span { color: var(--dim); }

.wizard-switch { display: flex; align-items: center; gap: 9px; font-size: 12.5px; }

/* The name of the call. Only the leads run shows it — every other way in has
   already been given a title by the field that opened the wizard. */
.wizard-name-row { display: flex; align-items: center; gap: 10px; }
.wizard-name-row label { font-size: 12.5px; color: var(--dim); white-space: nowrap; }
.wizard-name-row input { flex: 1; min-width: 0; }

/* A standing warning about the box under it, and on the step that has one it is
   the ONLY prose above that box — it replaces the step's note rather than
   sitting under it, because the same point made twice at two lengths is how a
   dialog fills with text nobody reads.

   Orange rather than red: it is not reporting a failure, it is saying that a box
   which will accept anything deserves more than the least you can put in it.
   --warn rather than var(--them): this was a literal for exactly that reason,
   written down when the far end's lane colour was the only orange in the file
   and anything borrowing it would be dragged along by a change to it. That is
   what --warn is for, and the palette swap proved the point — every accent
   still riding --them turned violet with the lanes. */
.wizard-alert {
  margin: 0;
  padding: 9px 11px;
  font-size: 12.5px;
  line-height: 1.5;
  --tint: var(--warn);
  --tint-fill: 9%;
  color: var(--tint);
  border: 1px solid color-mix(in srgb, var(--tint) 40%, transparent);
  border-left-width: 3px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--tint) var(--tint-fill), transparent);
}

/* The same block reporting a finished job rather than warning about an
   unfinished one. Green because it is the colour this app already uses for
   "this is done" — a covered checklist item, a live lane. */
.wizard-alert.done { --tint: var(--good); --tint-fill: 8%; }

/* Three controls rather than one, so this step has its own row. */
/* One question per line. The two selects sat side by side and the two
   checkboxes wrapped underneath them, so a step asking four separate questions
   read as one row of settings and one row of afterthoughts — and at a docked
   width the pair collapsed into a column anyway, which is the arrangement they
   are readable in. Stacked, each row is its own question and the labels line up
   down the left. */
.wizard-language { display: flex; flex-direction: column; align-items: stretch; gap: 14px; }
.wizard-language .field { min-width: 0; }

.wizard-state { margin: 0; font-size: 12px; line-height: 1.45; min-height: 16px; }

/* Cancel is the destructive one and sits apart from the three that navigate,
   so it cannot be hit while reaching for Back. */
.wizard-actions { align-items: center; }
.wizard-spacer { flex: 1; }

/* ---------------------------------------------------------------- the handset

   There is no stripped mobile shell here any more. A phone gets the SAME
   interface a 400px docked desktop column gets — the charts, the search box,
   Advice, the documents, every mode in the create dialog — laid out by the
   `max-width: 520px` block above and the `max-width: 700px` block below. Both
   of those are WIDTH queries, and that is the whole point: a handset and a
   dragged-narrow laptop window have the same problem, so they get the same
   answer and there is one layout to keep right instead of two.

   What stood here was a second, smaller product on the same URL: forty lines of
   `[data-shell="mobile"]` hiding `.home-charts`, `.topbar-search`,
   `#advice-wrap`, `#talk-time`, every `.help-dot`, the whole sidebar but for
   the capture block, and `#home-new` itself — which is what left a phone with
   one way to create a conversation, a one-tap button locked to the unassisted
   quick mode. None of that was a limitation of the DEVICE. A phone can read a
   chart, choose a mode and prepare a document; it simply could not do any of it
   here.

   NOTHING in this stylesheet reads `data-shell` now. mobile.js still writes it
   on the body as a diagnostic — see the header there — and the one real
   difference a handset has is behavioural rather than visual: it cannot share a
   screen, so the Laptop entry in the capture picker is hidden by
   `renderCaptureModeMenu`. That is a `.hidden` on one menu item, not a layout.

   The one rule left is the class that replaced `.mobile-only`, and it is
   default-invisible for the reason that one was: a control that appeared on a
   laptop because a stylesheet failed to load would be worse than one that never
   appeared at all. It is shown in the narrow block below. */
.narrow-only { display: none; }

/* The running-capture halo, used by the header's round Record button below.
   Declared out here rather than inside the media query because the rest of this
   file keeps its @keyframes at the top level, and the class it is applied
   through (`.compact-capture-row`) only exists below 700px anyway — the width
   scoping is already carried by the DOM. */
/* The one halo in the app, and the three things that mean "recording right now"
   all wear it: the Record button, the follow strip's dot, and a live row in the
   conversation list.

   `--halo` so the colour is the caller's, defaulting to the amber the Record
   button has always used. A second keyframe per colour is the mirrored-literal
   mistake in CSS — two copies of a measured animation kept honest by nothing —
   and the list's dot is green because a call in progress is a healthy state
   rather than something wanting attention. Both ends `color-mix` into
   `transparent` rather than the keyword, so the fade stays in its own hue
   instead of passing through grey. */
@keyframes capture-halo {
  from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--halo, var(--warn)) 55%, transparent); }
  to { box-shadow: 0 0 0 11px color-mix(in srgb, var(--halo, var(--warn)) 0%, transparent); }
}


/* ---------------------------------------------------------------------------
   Narrow: the compact header, the set-up dialog behind it, and the phone.

   Width, and ONLY width. A docked desktop column is 400px with a mouse and a
   screen it can share, and a handset is 390px with a finger and no screen to
   share; they have exactly the same amount of room, so they get exactly the
   same layout. The rules that were written under [data-shell="mobile"] were
   this block's other half, and the dock got none of them — which is why this
   is a media query, and why the shell block above is now empty of layout.
   `applyCompactControls` in mobile.js asks the same NARROW_PX, so the DOM move
   and the styling can never disagree about what narrow means.
   --------------------------------------------------------------------------- */
/* An icon instead of a label, at every width. `.btn-icon` is hidden by default
   so a control that has both shows the word; #btn-session below is the one that
   does the opposite, because what it opens is a MENU — three unrelated things,
   one of which signs you out — and a hamburger says that where the word
   "Settings" said something narrower and where a gear said something wrong. */
.btn-icon { display: none; }

/* The round icon button, drawn once. `.icon-round` is NOT width-scoped even
   though Set-up only exists on a narrow screen: the DOM already says so —
   `#btn-controls` is the only element wearing it, it ships `.hidden`, and
   `applyCompactControls` is the only thing that takes that off. Same argument
   @keyframes capture-halo makes for itself further down. Each selector keeps
   its own specificity here, which is what still lets #btn-session out-rank the
   `.header-button-link` pill it also wears. */
#btn-session,
.icon-round {
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: transparent;
  color: var(--text);
}

.icon-round svg { width: 18px; height: 18px; }

#btn-session .btn-text { display: none; }
#btn-session .btn-icon { display: block; width: 19px; height: 19px; }

/* The two header doors that keep BOTH: an icon and the word. `#btn-session`
   swaps one for the other because what it opens is a menu and a hamburger says
   that on its own; "Conversations" and "Quick notetaker" say something a glyph
   cannot, so the glyph goes beside the word rather than instead of it.

   `inline-flex` on the button and a gap, rather than a margin on the icon: the
   label is 11px mono with 0.08em tracking and the icon is 15px, so they have
   different centres and only a flex line lands them on one. */
#btn-conversations,
#home-quick {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

#btn-conversations .btn-icon,
#home-quick .btn-icon {
  display: block;
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
}

/* ---- the hamburger is the last thing in the header, at every width ----

   It is the app's one MENU — Account settings, Organisation, Admin, Sign out
   — and a menu belongs at the end of a bar rather than in the middle of the
   controls it is not one of. It used to sit third in the DOM, so on the home
   screen it landed between the mark and the search box and in a conversation
   between Conversations and the title: in both cases inside the row rather
   than at the end of it.

   `order` alone does it at every width above 520, because whatever precedes it
   there already absorbs the free space — `.title-text` is `flex: 1` in a
   conversation and `.topbar-search` carries `margin-left: auto` on the home
   screen. Nothing else in the header sets an order at those widths, so this is
   simply "last". Below 520 it needs an auto margin of its own; see there. */
#session-wrap { order: 1; }

/* And the menu hangs from its RIGHT edge, so it opens leftwards — inwards,
   over the header it came from, rather than off the side of the page. That
   follows from the button being at the far end: `left: 0` would put a 300px
   menu almost entirely outside a 390px viewport. `left: auto` is the necessary
   half — `right: 0` alone leaves the inherited `left: 0` in force and the box
   just stretches between the two.

   Unconditional, unlike the same treatment for the other menus in the 700px
   block: those hang off buttons that are still mid-row on a wide screen, and
   this one never is. */
#session-wrap .advice-menu { left: auto; right: 0; }

/* ONE ICON PER MENU ROW, and what they are for is telling seven rows of
   similar text apart at a glance.

   Drawn inline on the same 24x24 grid every other icon in this app uses rather
   than fetched from an icon font: a CDN is refused by `default-src 'self'`
   before it is a preference, and vendoring a webfont plus its stylesheet for
   seven glyphs is a great deal of weight beside the two libraries that do earn
   it — a chart engine and a calendar.

   **Stroked, not filled, and `--faint`.** They are furniture: the words are the
   thing being read and an icon that competed with them would be the accent-with-
   no-meaning this stylesheet spends a section on. They take the row's own ink on
   hover, so the pointer lights the whole row rather than half of it.

   `align-self: center` because `.advice-menu button` aligns its text on the
   BASELINE — which for an SVG is its bottom edge, so without this every icon
   sits a few pixels low. The category dot one section up needs the same line
   for the same reason. */
.menu-icon {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  align-self: center;
  color: var(--faint);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: color 0.15s;
}

.advice-menu button:hover .menu-icon { color: var(--text); }

/* Signing out is not the same kind of thing as opening a settings pane, so it
   is set apart — a hairline above it and the danger ink, the same red every
   other irreversible action in this app uses. */
.session-menu-out {
  margin-top: 3px;
  border-top: 1px solid var(--line) !important;
  border-radius: 0 0 6px 6px !important;
}

.session-menu-out strong { color: var(--bad); }
/* And its icon, which would otherwise be the one row whose glyph disagreed with
   its own label about how serious it is. Hover included, or the pointer would
   turn the one red thing in the menu white. */
.session-menu-out .menu-icon,
.advice-menu button.session-menu-out:hover .menu-icon { color: var(--bad); }

@media (max-width: 700px) {
  /* ---- the three round header buttons, on a narrow screen ----

     Settings, Set-up and Record, as icons in circles. The header is the whole of
     the chrome on a handset and three words would not fit beside the mark and the
     title; a round target is also the easiest thing on the screen for a thumb.

     .btn-icon / .btn-text is how Settings shows one or the other: the word on a
     wide screen, the gear here. Nothing rewrites that button's contents, so an
     SVG can live inside it. */
  .header-button-link .btn-icon { width: 18px; height: 18px; }

  /* A dropdown hangs from the left edge of its button by default, which runs it
     off the screen when the button is a 38px circle near the right-hand end of a
     400px header — the menu is 148px at its narrowest and the gear has nothing
     like that much room to its right. Anchored to the button's right edge here
     instead, so it opens inwards. `left: auto` is the necessary half: `right: 0`
     alone leaves the original `left: 0` in force and the box simply stretches
     between the two. */
  .advice-wrap .advice-menu {
    left: auto;
    right: 0;
    /* Anchoring it inwards is only half. Each item is a name and a description
       on ONE baseline — "Account settings" then "Account, library, categories"
       pushed right by `margin-left: auto` — which made the menu 370px wide, so
       right-anchored at a gear 342px in it still hung 28px off the left edge.
       Capped, and the two halves stack instead of sharing a line. */
    max-width: min(300px, calc(100vw - 28px));
  }

  .advice-wrap .advice-menu button {
    display: block;
  }

  .advice-wrap .advice-menu span {
    display: block;
    margin-left: 0;
    margin-top: 2px;
    white-space: normal;
  }

  /* #session-wrap is the positioning context for the menu that drops out of the
     gear, so it must not be laid out as a flex item in its own right. */
  #session-wrap { display: inline-flex; }

  /* ---- Record, in the header, drawn by CSS ----

     The label is rewritten every frame by renderCaptureButton — it carries the
     elapsed clock — so anything put INSIDE this button is wiped on the next tick.
     The icon is therefore a pseudo-element, and the text is collapsed rather than
     removed: `font-size: 0` hides the words without touching the node the button
     keeps writing to.

     A filled circle at rest, a square while recording — the two shapes every
     recorder has used for forty years, and the state class they key off
     (`.is-live`, `.is-waiting`) is the one renderCaptureButton already sets. */
  .compact-capture-row {
    display: flex;
    align-items: center;
    margin: 0;
    order: 3;
  }

  /* Set-up travels with Record rather than staying at the default 0. They are
     one pair — the two things you do to the conversation that is open — and
     left alone Set-up sat up on the first row beside the mark while Record was
     two rows below it, which read as two unrelated controls. */
  #btn-controls { order: 3; }

  .compact-capture-row .help-dot { display: none; }

  /* ---- and neither of them exists with nothing open ----

     Both were parked in the header by applyCompactControls, which takes them
     OUT of #controls — and `#controls.hidden` is exactly how the router says
     "no conversation is open" and how renderControls says "this is a camera
     mode". So on the home screen the header carried a Record button with
     nothing to record into and a Set-up button opening a dialog whose body was
     hidden: an empty screen with a × on it.

     Read off `#controls` rather than off `#home`, because there are two
     reasons that block is hidden and both mean the same thing here. And CSS
     rather than a third branch in `route()`: the router already writes this
     fact, and a second place writing it is a second place to forget.

     The rule itself has MOVED to the foot of this file, out of the width query
     it was written in. It never depended on the width — it depends on the row
     having been parked in the header, and a short window parks it at any width
     at all, which put a Record button on the home screen of a 900px-wide
     500px-tall window. Same lesson as `.modal-close-float`: before leaving a
     fix inside a media query, ask whether what it fixes actually varies with
     the width. */

  .compact-capture-row .capture-toggle {
    width: 38px;
    height: 38px;
    min-width: 0;
    padding: 0;
    font-size: 0;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1px solid var(--line-strong);
  }

  .compact-capture-row .capture-toggle::before {
    content: "";
    width: 15px;
    height: 15px;
    border-radius: 50%;
    background: var(--bad);
    transition: border-radius 0.15s, width 0.15s, height 0.15s, background 0.15s;
  }

  /* Recording: the dot becomes a stop square, in the amber that means a capture
     is running everywhere else in this app — and the button pulses.

     A halo expanding out of the ring and fading, once every 1.6 seconds. It is
     `box-shadow`, so it costs no layout and cannot push the two round buttons
     beside it about; a `transform: scale` on a 38px flex item in a header that
     wraps would do both. Both ends are `color-mix` into `transparent` rather
     than the keyword, so the fade stays in the amber rather than passing
     through grey on the way out.

     Only while `.is-live`, and that is the whole reason it earns the motion.
     At rest this is the primary action on the screen and a pulse would be an
     advert; running, it is the one thing worth a glance from across a table —
     is this still recording — which is exactly the question a phone left on a
     table for forty minutes raises. Same rule as the palette: emphasis has to
     carry information. */
  .compact-capture-row .capture-toggle.is-live {
    border-color: var(--warn);
    background: color-mix(in srgb, var(--warn) 16%, transparent);
    animation: capture-halo 1.6s ease-out infinite;
  }
  .compact-capture-row .capture-toggle.is-live::before {
    width: 13px;
    height: 13px;
    border-radius: 3px;
    background: var(--warn);
  }

  /* Mid-flight, and not pressable either way. */
  .compact-capture-row .capture-toggle.is-waiting::before {
    background: var(--dim);
  }

  .compact-capture-row .capture-toggle:disabled { opacity: 0.4; }

  /* Lifted into the header beside Record by applyCompactControls, on a line of
     its OWN so a failed flush reads as a sentence rather than being squeezed
     between two round buttons. `flex: 1 0 100%` is what buys the line; `order`
     puts it under the row it belongs to rather than wherever the header's wrap
     happens to drop it. Still `:empty { display: none }` from the base rule, so
     it costs a line only when there is something wrong to say — and what it has
     to say is that speech is not reaching the server, which is why it is out
     here rather than in the set-up dialog with the rest of #controls.

     This is the whole reason the block is width-scoped rather than shell-
     scoped. It was written as `[data-shell="mobile"] .mobile-save-state`, and
     when the mechanism became applyCompactControls the class it applies became
     `compact-save-state` — so the rule stopped matching anything, silently, and
     the warning went back to being squeezed between the two buttons. A docked
     desktop column parks this element in the header too and never had the rule
     at all. */
  .compact-save-state {
    flex: 1 0 100%;
    order: 4;
    margin: 0;
    font-size: 12px;
    text-align: left;
  }

  /* Echo, right after the flush warning it keeps company with in the header —
     see the move in mobile.js. Sizing lives outside this query, on
     `.compact-echo-banner`/`.compact-echo-gate` themselves, because the wide
     short window parks them here too and needs it just as much; only the
     ORDER is specific to this width, since the wide window has no other
     `order` here to sort against and the DOM position is already right. */
  .compact-echo-banner { order: 5; }
  .compact-echo-gate { order: 6; }

  /* The set-up dialog's body is the sidebar block itself, moved in. It arrives
     carrying the panel styling it had in the sidebar, which is right — it is the
     same block — but not the outer card, which the dialog now provides. */
  #controls-modal-body > #controls {
    background: transparent;
    border: 0;
    padding: 0;
  }

  /* The sidebar is left holding only #camera-panel, which is `position: fixed`
     and so draws nothing here. Deliberately NOT `display: none`: that would take
     the camera panel with it — an ancestor's `display: none` beats a descendant's
     `position: fixed` — and the visual assistant's whole interface would vanish
     at the one width it was built for. */
  .sidebar { padding: 0; border: 0; background: none; }

  /* ---- what a phone used to have to itself ----

     Everything from here down was written under [data-shell="mobile"] and is
     here now for the reason the block above is: a 400px docked column has the
     same problem and never got any of it. Nothing below hides anything — the
     hiding is what went. */

  /* The class that replaced `.mobile-only`. `#mobile-visual` is the only thing
     wearing it; camera.js still holds a second, harder gate on the same button
     (the registry, so admin-only), and `.hidden`'s !important wins over this. */
  .narrow-only { display: inline-flex; }

  /* The transcript is the screen at this width, and it is read at arm's length
     on a table rather than on a monitor. The 860px block already does this for
     the LIVE block; these are the finalised turns above it. */
  .turn-text,
  .turn-translation { font-size: 17px; line-height: 1.5; }
  .turn-original { font-size: 13px; }

  /* List rows a thumb can hit without aiming. This was written as `.row` /
     `.row-title` and matched nothing for as long as it existed, so every list
     row on a phone stayed at the desktop's 9px padding; the classes
     buildConversationRows actually writes are these. */
  .conversation-row { padding: 13px 12px; }
  .conversation-title { font-size: 15px; }

  /* The visual assistant's panel becomes the bottom bar, the one place a thumb
     goes. Fixed rather than sticky, because the exchange scrolls underneath it
     and the shutter has to stay reachable at any scroll position.

     #controls does NOT get this: it is the set-up dialog's body at this width
     (see applyCompactControls), and Record has already been lifted out of it
     into the header. A camera mode has no Record and no dialog — it is one
     panel you shoot from — so it is the one thing still pinned to the edge. */
  #camera-panel {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* BELOW the modal layer (.modal is 20, #busy-modal and #alert-modal 30),
       and it has to stay that way. This bar used to be 40, which put it over
       every dialog in the app on a phone. A dialog below 700px is full screen
       (the block at the foot of this file), so a bar that outranks it draws
       across the bottom of whatever is open. Being fixed is already enough to
       lift it clear of the page scrolling underneath; it never needed to
       outrank a dialog to do that. */
    z-index: 10;
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0;
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--line);
    border-radius: 0;
    background: var(--panel);
  }

  /* And the page ends above that bar rather than under it — but ONLY when the
     bar is there. The old rule padded every mobile page by 96px unconditionally,
     which was harmless while the capture block was pinned down there too and is
     a dead strip under every other screen now. `:has()` asks the same question
     renderCameraPanel answers, rather than inventing a body class for it. */
  body:has(#camera-panel:not(.hidden)) {
    padding-bottom: calc(96px + env(safe-area-inset-bottom, 0px));
  }
}

/* The capture halo is motion and has to be able to stop. This is a SECOND
   reduced-motion block rather than an entry in the one near the top of the
   file, and that is forced: the rule it overrides is inside the width query
   above, so a selector of equal specificity written earlier would simply lose
   on file order. The information survives the stop — a running capture is
   still an amber ring with a stop square in it — which is the same test the
   first block applies to the wordmark. */
@media (prefers-reduced-motion: reduce) {
  .compact-capture-row .capture-toggle.is-live { animation: none; }
  /* The follower's live dot, for the same reason and by the same test: it is
     declared around line 1990, so an `animation: none` in the block near the
     TOP of the file would tie on specificity and lose on file order — which is
     the whole reason this second block exists. What the dot is saying survives
     without the pulse, because it is amber either way and the sentence beside
     it says the call is live in words. */
  .follow-dot.live { animation: none; }
  /* And the conversation list's live dot, by the same test: it stays a green
     dot on a green-edged row, and the row still says "Being recorded now" to
     anything reading the page aloud. */
  .conversation-live { animation: none; }
  /* The Rewrite button's "press me or this document is not sent". Declared
     around line 1780, so it is here for the file-order reason the rest of this
     block is, and it passes the same test: the button stays red, in a red tint,
     under a document row that has itself gone red, and the line beside it says
     in words that none of the set is being sent. The motion was the loudest
     part of that and the least of it. */
  button.must-press:not(:disabled) { animation: none; }
}

/* ---------------------------------------------------------------------------
   The home screen fits the viewport.

   Four charts at a fixed 240px each plus the stat tiles ran past the bottom of
   every window, so the one screen that exists to be TAKEN IN AT A GLANCE had to
   be scrolled to see. Here the page is exactly the viewport and the charts
   divide whatever is left after the header, the tiles and the heading — two
   rows of `minmax(0, 1fr)`, so they shrink as far as they have to.

   Above 700px only. Below it the grid is a single column and four charts cannot
   share one screen's height without each being a stripe; there, scrolling is
   the right answer and the page keeps it.

   **And not in the short window**, which is the same sentence about the other
   axis: 500px of height has nothing to divide between two rows of cards either.
   That is why every selector here carries `:not(.short-view)` rather than the
   short block at the foot of this file restating the ten declarations back to
   their defaults — a second copy of a layout is how the two end up disagreeing,
   and the block simply not applying leaves each rule's own base value in force.
   Left on, it did not merely look cramped: the cards were cut off at the fold
   with `overflow: hidden` above them, so the bottom two were unreachable.

   `100dvh` rather than `100vh`: on a phone-shaped window `vh` is the height
   with the browser chrome retracted, which is taller than what you can see, so
   `vh` would guarantee the scrollbar this block exists to remove.

   `:has()` scopes it to the home route without inventing a body class for the
   router to maintain — #home carries `.hidden` on every other screen already,
   and that is the same fact the router is already writing. */
@media (min-width: 701px) {
  /* html as well as body. `overflow: hidden` on body alone does not stop the
     document scrolling — the viewport's scrollport belongs to the root element,
     so the bar simply moved up a level and the page still scrolled. */
  html:not(:has(body.short-view)):has(#home:not(.hidden)) {
    height: 100%;
    overflow: hidden;
  }

  body:not(.short-view):has(#home:not(.hidden)) {
    height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  /* The header keeps its own height; the layout takes the rest. `min-height: 0`
     on every link in the chain, or each one floors at its content's height and
     the overflow simply moves down a level instead of being divided. */
  /* `width: 100%` matters as much as the flex line here. .topbar carries
     `margin-inline: auto` to centre itself under its 1320px cap, and an auto
     margin on a flex item absorbs the free space instead of stretching into it
     — so the header collapsed to its content width and sat centred over
     full-width tiles, with the search box wrapped onto a line of its own. */
  body:not(.short-view):has(#home:not(.hidden)) > .topbar {
    flex: 0 0 auto;
    width: 100%;
  }

  body:not(.short-view):has(#home:not(.hidden)) > .layout {
    flex: 1 1 auto;
    width: 100%;
    min-height: 0;
    /* And the row inside it has to be allowed to shrink. A grid row defaults to
       `auto`, which sizes to its item's content, so `.history` stood 12px taller
       than the track that was supposed to contain it and pushed the difference
       out of the bottom of the page. `min-height: 0` on the ITEM does not fix
       that — the track is what needs the freedom. */
    grid-template-rows: minmax(0, 1fr);
    /* .layout is `align-items: start`, which is right beside a conversation —
       the sidebar should be as tall as its own contents and no taller. Here it
       meant .history sat at its content height of 291px inside a 759px track
       and never received the room the whole chain below it was waiting to
       divide. Stretch is what hands that room down. */
    align-items: stretch;
  }

  body:not(.short-view):has(#home:not(.hidden)) .history {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  body:not(.short-view):has(#home:not(.hidden)) .home {
    flex: 1 1 auto;
    min-height: 0;
  }

  /* Search results are the one thing here that can be arbitrarily long, so they
     scroll inside themselves rather than pushing the charts off the screen. */
  body:not(.short-view):has(#home:not(.hidden)) .search-results {
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
  }

  /* The heading takes its own height and the two card rows split the rest. */
  body:not(.short-view):has(#home:not(.hidden)) .home-charts {
    flex: 1 1 auto;
    min-height: 0;
    grid-template-rows: auto repeat(2, minmax(0, 1fr));
  }

  body:not(.short-view):has(#home:not(.hidden)) .chart-card { min-height: 0; }

  /* The canvas wrapper stops being 240px and becomes "whatever the card has
     left". Chart.js is watching this box, so it repaints to match. */
  body:not(.short-view):has(#home:not(.hidden)) .chart-canvas-wrap,
  body:not(.short-view):has(#home:not(.hidden)) .chart-empty {
    flex: 1 1 auto;
    height: auto;
    min-height: 0;
  }
}

/* ------------------------------------------------------------------ plans

   The pricing dialog. Cards are chrome, not accents: the emphasised control on
   a card is the landing's solid `.primary`, everything else is a ghost pill,
   and no hue is spent here at all — which is what leaves every colour in this
   stylesheet free to mean something elsewhere. */
.pricing-plans {
  display: grid;
  /* auto-fit rather than a fixed count: there are three tiers today and the
     grid should not have to be edited to add a fourth. `minmax(0, …)` because a
     grid item's default min-width is its content, and a long detail paragraph
     would otherwise stop the track ever shrinking — the same trap the chart
     cards carry a note about. */
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: 10px;
}

.pricing-card {
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 14px;
}

/* Label voice — see the block at the top of this file. */
.pricing-name {
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--dim);
}

.pricing-price { font-size: 20px; font-weight: 600; }

/* The mode's own paragraph, so it is prose and takes the sans exception the
   label block calls out. */
.pricing-detail { margin: 0; font-size: 12.5px; line-height: 1.5; flex: 1 1 auto; }

/* Where the button would be on the others, so the cards stay the same shape. */
.pricing-current {
  align-self: flex-start;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 11px;
  font-size: 11px;
  color: var(--faint);
}

.pricing-packs { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }

.pricing-pack {
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 9px 12px;
}

/* The price takes the free space so the Buy button sits at the far end, and
   the row survives a longer pack name without the button moving. */
.pricing-pack .pricing-price { font-size: 15px; margin-left: auto; }

/* The way to a plan from the allowance block. Pushed to the far end of the
   head row, which is where the eye already ends up after the plan name and the
   renewal date. */
.usage-upgrade { margin-left: auto; }

/* ------------------------------------------------- dialogs on a small screen

   LAST IN THE FILE, and it has to stay last. Two of the rules it overrides are
   id-scoped — `#wizard-modal .modal-card` sets height:auto/max-height:80vh — and
   naming the same id here only ties on specificity, which the later rule wins.
   The wizard stayed 80vh inside a full-screen dialog until this moved down here.
   Anything added below it that sizes a dialog will win the same way.

   Edge to edge, in both directions. A centred card with a margin round it is
   right when the margin costs nothing; on a 400px column it is 40px of the
   width and an 8vh strip of the height spent on a view of the page behind,
   which is not a view anybody wants while filling in a form. So the dialog
   stops being a card and becomes the screen: no inset, no rounding, no border,
   no shadow, and the whole height to work in.

   700px because that is exactly `NARROW_PX` in static/mobile.js, the width at
   which the set-up block moves into a dialog of its own. Writing it here means
   a dialog and the block that has just become one can never disagree about the
   width, and a phone in landscape at 700x400, where a 520px query would not
   fire, is covered by construction.

   Every variant is named because each sets its own ceiling and each is a single
   class, so `.modal-wide` alone would out-order `.modal-card` here. The extra
   `.modal` in front is what wins on specificity rather than on file order. */
@media (max-width: 700px) {
  .modal {
    padding: 0;
    align-items: stretch;
  }

  /* #modal keeps a 20px strip above it at full width so the card reads as its
     own screen rather than a sheet stuck to the top. Edge to edge, there is no
     page behind it for the strip to show. */
  #modal { padding-top: 0; }

  /* The last three are named because they set their own geometry by id, and an
     id beats any number of classes — #wizard-modal .modal-card is (1,1,0)
     against this rule's (0,2,0), so without them the wizard stayed 80vh while
     everything around it went full screen. Listed rather than forced with
     !important: a selector list matches each selector at its own specificity,
     so naming the id is the whole fix. */
  .modal .modal-card,
  .modal .modal-wide,
  .modal .modal-tall,
  .modal .modal-narrow,
  .modal .modal-settings,
  .modal .wide-card,
  #new-modal .modal-card,
  #organisation-modal .modal-card,
  #wizard-modal .modal-card,
  #wizard-modal.wizard-tall .modal-card,
  /* Sized by id above, so both have to be named here or they keep their 92vh
     inside a full-screen dialog — which is the exact bug that moved this block
     to the end of the file. */
  #template-modal .modal-card,
  #objection-modal .modal-card,
  /* And this one, for the same reason: it sizes itself by id above. */
  #convsettings-modal .modal-card {
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    border-width: 0;
    border-radius: 0;
    box-shadow: none;
    /* Every one of them scrolls now. Full height removes the old ceiling, so
       nothing is squeezed by it, but a long form still outruns a short screen
       and the card is the thing that has to move. */
    overflow-y: auto;
    /* Clear of a notch and a home indicator. The 16px is the card's own
       padding, kept as the floor on any screen without either. */
    padding: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
             max(16px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  }

  /* The sticky footer's offset is measured off the card's padding, which has
     just changed, so it is restated against the new one. */
  .modal-tall .modal-actions { bottom: calc(0px - max(16px, env(safe-area-inset-bottom))); }

  /* The floating × moves in with the padding rather than staying at a fixed
     14px, which would put it under a notch on a handset. */
  .modal-close-float {
    top: max(14px, env(safe-area-inset-top));
    right: max(14px, env(safe-area-inset-right));
  }
}

/* ------------------------------------------- the write-up template editor

   `.hints-tab.org-tab` stood here, tinting the three Settings tabs that were
   the ORGANISATION's rather than the account's. Those tabs are
   #organisation-modal's now, where every tab is the organisation's, so an
   accent marking the scope would be telling a reader something the whole
   dialog already says.

   `.org-scope` stood here too — the same sentence in words above each of the
   three lists, saying that what follows belongs to the organisation rather than
   to the reader. It has gone for the reason the tint went: the dialog it is in
   is called Organisation and every tab in it is the organisation's, so a
   paragraph at the top of three of them was the screen restating its own title
   three times. What the editors themselves say when you open one is kept —
   that is about the thing being edited rather than about the screen it is on.
   `--org` is still read by `.template-pick.active` below. */

.template-screen { display: flex; flex-direction: column; gap: 14px; }

.template-list { display: flex; flex-direction: column; gap: 6px; }

.template-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.template-pick {
  flex: 1;
  text-align: left;
  background: none;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  padding: 7px 11px;
  cursor: pointer;
  min-width: 0;
}
.template-pick.active { border-color: var(--org); }

.template-editor {
  display: flex;
  flex-direction: column;
  gap: 12px;
  border-top: 1px solid var(--line);
  padding-top: 14px;
}

/* One part of a template: a model call, with everything that decides it. */
.template-tab {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.template-tab-head,
.template-part-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
}
.template-tab-number,
.template-part-number {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--faint);
}

.template-field { display: flex; flex-direction: column; gap: 4px; }
.template-label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
}
.template-hint { margin: 0; font-size: 12px; }

.template-textarea {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  padding: 8px 10px;
  font: inherit;
  line-height: 1.5;
  resize: vertical;
}

.template-parts { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
/* A flex item stretches to the cross size by default, so both of these ran the
   whole width of the column and read as a bar rather than as a control. */
.template-parts > .template-part { align-self: stretch; }
.template-part {
  display: flex;
  flex-direction: column;
  gap: 6px;
  border-left: 2px solid var(--line);
  padding-left: 10px;
}

/* Which template a conversation's write-up is built from. A row rather than a
   line in the note, because it is a CONTROL — the note above it describes what
   is about to be written and this decides it. */
.writeup-template-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 12px;
  font-size: 12px;
}
.writeup-template-row .option-select { flex: 0 1 260px; min-width: 0; }

/* Inside its own dialog the editor is the whole body, so the rule that
   separates it from a list above it in the Settings pane does not apply. */
#template-modal-body, #objection-modal-body { border-top: 0; padding-top: 0; }

/* The way out of a dialog that scrolls must not scroll away — the same rule
   `.modal-tall` keeps, and this card is 92vh of instructions. */
#template-modal .modal-actions,
#objection-modal .modal-actions {
  position: sticky;
  bottom: 0;
  background: var(--panel);
  padding-top: 10px;
  margin-top: auto;
}

/* How many parts a saved template has, on its row in the list. Told apart from
   the name by weight and colour rather than by another line, because the list
   is a list of names and this is a fact about one. */
.template-parts-count { color: var(--faint); font-size: 12px; margin-left: 10px; }

/* The editor's own tab strip: the tabs being written, drawn the way the
   write-up dialog will draw them once they are built. `+ Tab` is the last
   thing in it rather than a control elsewhere, so adding one is the same
   gesture as choosing one. */
.template-strip { margin-bottom: 4px; }
.template-add-tab { color: var(--dim); border-style: dashed; }

/* The source switches: the app's own `.toggle`, laid out in a row that wraps.
   The name goes after the track, which is what `.toggle input:checked +
   .toggle-track` requires of the order. */
.template-sources { display: flex; flex-wrap: wrap; gap: 10px 18px; }
.template-source {
  font-family: var(--sans);
  font-size: 13px;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text);
}

/* The count keeps the label voice: it is furniture about the row, not the name
   somebody chose for it. */
.template-parts-count {
  font-family: var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* The built-in template, open for reading. Every box is disabled, so the only
   thing this adds is saying WHY they are: a disabled input on its own reads as
   a control that is broken rather than one that belongs to somebody else. */
.template-readonly .template-tab { border-style: dashed; }
.template-readonly .option-input,
.template-readonly .template-textarea {
  color: var(--mute);
  cursor: default;
  opacity: 1;
}

/* ------------------------------------------------- the objection lists */

/* Fills whatever the card has left and scrolls inside it — `flex: 1 1 auto` with
   `min-height: 0`, never a share of the viewport. Twenty pairs is well past a
   screen, and a `vh` here would ADD to the card's own ceiling and push Build and
   Done out of the bottom of it, which is exactly what `.writeup-body` carries a
   note about. */
.likely-objections-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-right: 6px;
}

/* Nothing built yet. A paragraph rather than an empty box: an empty dialog is
   indistinguishable from a broken one, which is the failure a help dot with no
   topic has and the one `#assess-unread` exists to answer. */
.likely-objections-empty { color: var(--faint); margin: 0; }

/* ---------------------------------------------------------------------------
   The short window — the desktop shell pinned above everything else.

   About 500px tall at an ordinary width. Nothing about it is narrow, so not one
   rule in the `max-width` blocks above applies, and none of them would help:
   what has run out is HEIGHT. The conversation view stacks the answer box, the
   live line and the transcript down the page, and at this height the third of
   them is off the bottom of the window.

   So the two halves take a tab each — see `shortview.js`, which shows one group
   and hides the other and moves nothing. Everything here keys off
   `body.short-view`, written by the ONE watcher in `mobile.js`: the pixel
   figure lives in `SHORT_PX` and this file never repeats it, so a `max-height`
   query cannot drift from the height the JavaScript is acting on.

   Last in the file on purpose. `.layout.sidebar-hidden` and `.layout.no-sidebar`
   are (0,2,0) like the rule below that has to beat them, and at equal
   specificity the later rule wins.
   --------------------------------------------------------------------------- */

/* Every pixel of vertical padding is one the transcript does not get. */
body.short-view { padding: 8px 16px 10px; }

/* The header holds Record, Stop, Answer, Advice and the flag, and this window
   scrolls — so it is pinned. A control that acts on a call in progress must not
   be something you scroll back up to find, and the tab strip under it is the
   way back to the other half, so it is pinned to the same edge. */
body.short-view > .topbar {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--bg);
  margin-bottom: 10px;
  padding-top: 6px;
  padding-bottom: 8px;
}

/* No sidebar, and no way to ask for one. `#controls` has already been carried
   into the set-up dialog by `applyCompactControls` — the same move a 400px
   column makes, for the same reason — and the button that opens that dialog is
   withheld here: a window pinned over somebody else's meeting is not where a
   language or a mode gets chosen. The retract control goes with it, since there
   is nothing left to retract.

   `display: none` on `.sidebar` is exactly what must NOT happen: it is left
   holding `#camera-panel`, which escapes the flow through `position: fixed`,
   and an ancestor's `display: none` beats a descendant's `position: fixed`.
   Emptied of `#controls` it collapses to nothing on its own. */
body.short-view .layout { grid-template-columns: minmax(0, 1fr); gap: 0; }
body.short-view .sidebar {
  position: static;
  max-height: none;
  padding: 0;
  border: 0;
  background: none;
}
body.short-view .sidebar-toggle { display: none; }

/* The row carries its help dot with it — that is the point of moving the whole
   `.option-row` — but in a header it is a `?` about a button whose own words
   say what it does. The narrow layout reached the same conclusion; this is the
   other way of having no room. */
body.short-view .compact-capture-row .help-dot { display: none; }

/* And the door to the set-up dialog is withheld — but ONLY where the window is
   short and not also narrow, which is the pinned window and nothing else.
   A handset turned on its side is short too, and there that button is the ONLY
   way to the language, the mode and the documents: `#controls` has been parked
   in a dialog and there is no sidebar at any orientation. Taking it away would
   cost somebody the use of their own call for as long as they held the phone
   sideways, which is the failure `canBeLaptop` and `capturedElsewhere` both
   resolve their unknowns away from. Withholding it in a window that has a full
   header and a laptop behind it costs nothing but a trip back to the big
   window. */
@media (min-width: 701px) {
  body.short-view #btn-controls { display: none; }
}

/* The strip itself, pinned under the header for the reason given above. */
body.short-view .short-tabs {
  position: sticky;
  top: 46px;
  z-index: 4;
  background: var(--bg);
  padding: 4px 0;
  margin-bottom: 8px;
}

/* One group at a time. The bodies are the page's own — the same nodes drawn by
   the same renderers at every other height — so this is the whole of the
   division, and taking the two classes off is the whole of the undo. */
body.short-view .history.showing-ai .follow-bar,
body.short-view .history.showing-ai #live,
body.short-view .history.showing-ai #transcript { display: none; }
body.short-view .history.showing-conversation #hints { display: none; }

/* The Conversation tab is the live line and the one finished message before it,
   and no more than that. The transcript is drawn newest-first and already marks
   its newest line `.turn-current`, so the cut is one rule rather than a second
   renderer that would have to be kept in step with the first. `.empty` is not a
   `.turn` and survives, which is right: "Nothing captured yet." is the honest
   answer for a call that has not started. */
body.short-view .history.showing-conversation #transcript > .turn:not(.turn-current),
body.short-view .history.showing-conversation #transcript > .session-divider {
  display: none;
}

/* The standing note about telling the other participants is read once, before a
   call, on a screen that has room for it. Here it is a line of the two this tab
   has to spare. */
body.short-view .footer { display: none; }

/* The home screen scrolls here, and nothing in this section says so: the block
   that stops it scrolling carries `:not(.short-view)` on every selector, so it
   is off and each rule's own base value stands. See the note over it — undoing
   a layout is not the same as writing the opposite one, and only the first of
   those keeps one description of the screen. */

/* Parked in the header and belonging to nothing.

   Moved down here out of the `max-width: 700px` block, where it was written:
   the header carries the capture row whenever there is no room for the set-up
   block, and a short window has none at any width, so on the home screen of a
   900x500 window this was a Record button with nothing to record into and a
   Set-up button opening a dialog whose body was hidden.

   Still read off `#controls` rather than off `#home`, because there are two
   reasons that block is hidden and both mean the same thing here, and still CSS
   rather than a branch in `route()`, which already writes the fact. */
body:has(#controls.hidden) .compact-capture-row,
body:has(#controls.hidden) #btn-controls { display: none; }
