/* public site
   The unauthenticated customer shop.

   Loaded by `stylesheet_link_tag :app`, which includes every .css under
   app/assets sorted by filename — so the NUMBER PREFIX IS THE CASCADE ORDER.
   Renaming a file moves it in the cascade. */

/* ---------------- public customer site ----------------
   Photo-led and roomier than the internal tool: this is a shop window, not a
   workbench. Same type and colour system so the two never look unrelated. */
.pub { background: var(--surface); }

/* The header's height, named, because two things need to agree about it: the
   header itself and anything that sticks BELOW it. `--sticky-top` is the knob
   .list-bar reads — see 17-list-toolbar. */
body.pub { --pub-header-h: 72px; --sticky-top: var(--pub-header-h); }

.pub-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 0 32px;
  height: var(--pub-header-h);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  background: var(--surface);
  z-index: 20;
}
.pub-brand { font: 600 17px var(--ui); color: var(--ink); text-decoration: none; letter-spacing: -0.02em; }
.pub-basket {
  font: 500 14px var(--ui);
  color: var(--ink);
  text-decoration: none;
  padding: 8px 14px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.pub-basket:hover { border-color: var(--ink); }
/* The sticky bar, on the shop.

   The pattern is the app's own — .list-bar + shared/search + a results frame —
   and only two things differ here. The bar has to hide the rows scrolling
   under it, and the shop's ground is --paper like the back office's, so that
   comes free; the TAB HOVER does not, because it tints with --paper and on
   this site that is the page itself, so hovering did nothing you could see. */
/* Below the site header, not under it — see --sticky-top above. The shared bar
   sticks at 0, which is right in the back office where the chrome is a
   sidebar; here the header is sticky too and 72px tall, so the bar slid
   straight beneath it. Measured: elementFromPoint over the middle of the
   search box returned `pub-header-right`. The box was not merely hidden, it
   was unclickable — the one thing a sticky search box exists to stop. */
.pub-list-bar { margin-bottom: 8px; }
.pub-list-bar .filter-tabs { margin-top: 0; }
.pub-list-bar .filter-tabs a:hover { background: var(--surface); }
.pub-list-bar .filter-tabs a.on:hover { background: var(--ink); }
.pub-list-bar .filter-tabs .muted { margin-left: 2px; font-size: 12px; }
.pub-list-bar .filter-tabs a.on .muted { color: rgba(255, 255, 255, .65); }

/* The head of a list, with the way to start another beside it. */
.pub-list-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 20px; flex-wrap: wrap; margin-bottom: 22px;
}
.pub-list-head h1 { margin: 0; }
.pub-list-actions { display: flex; align-items: center; gap: 14px; }
.pub-list-actions form { margin: 0; }

/* Asking us to correct something, rather than typing over an address an
   invoice is raised to. */
.pub-note-line { margin: 0 0 14px; font-size: 14px; color: var(--ink-2); line-height: 1.55; }
.pub-change { max-width: 560px; }
.pub-change textarea { width: 100%; margin-bottom: 12px; }

/* What there is to do here. Left of centre and next to the brand, because it
   is the shape of the site — the right-hand side is who you are and what you
   are holding, which is a different question. */
.pub-nav { display: flex; align-items: center; gap: 4px; margin-right: auto; }
.pub-nav-link {
  padding: 7px 12px;
  border-radius: var(--r);
  font: 500 14px var(--ui);
  color: var(--ink-2);
  text-decoration: none;
}
.pub-nav-link:hover { background: var(--paper); color: var(--ink); }
/* The attribute does the drawing, so what is announced and what is seen cannot
   drift apart. */
.pub-nav-link[aria-current="page"] { background: var(--paper); color: var(--ink); }

.pub-header-right { display: flex; align-items: center; gap: 12px; }

.pub-account {
  font: 500 14px var(--ui); color: var(--ink); text-decoration: none;
  display: inline-flex; align-items: center; gap: 8px;
}
.pub-account:hover { text-decoration: underline; }

/* Said quietly and said always. Somebody waiting on approval should not have
   to click anything to find out they are still waiting. */
.pub-flag {
  font: 500 10.5px var(--ui); letter-spacing: .05em; text-transform: uppercase;
  background: var(--signal-bg); border: 1px solid var(--signal-line);
  color: var(--signal); border-radius: 999px; padding: 3px 8px; white-space: nowrap;
}

/* The global `button` rule paints a dark pill and a black hover. Sign out is a
   button_to because it is a DELETE, and it has to read as the plainest thing
   in the header — so all four are undone, the hover background included. */
.pub-plain, .pub-plain button, form.button_to .pub-plain {
  font: 500 14px var(--ui); color: var(--ink-2); text-decoration: none;
  background: none; border: 0; padding: 8px 4px; cursor: pointer; line-height: 1;
}
.pub-plain:hover, .pub-plain button:hover { background: none; color: var(--ink); text-decoration: underline; }

.pub-cta {
  font: 500 14px var(--ui); color: var(--surface); background: var(--ink);
  text-decoration: none; padding: 9px 16px; border-radius: 999px; white-space: nowrap;
}
.pub-cta:hover { background: var(--ink-2); }

/* `width: 100%` is load-bearing here and is not obvious. body.pub is a flex
   column — done for the job screen, which needs a definite height — which makes
   this a flex ITEM, and an item's `margin: 0 auto` on the cross axis BEATS the
   default stretch: it shrink-wraps to its content and centres that. Measured:
   every page on the shop narrowed from 1120px to whatever its widest row
   happened to be, 637px on the quotes list. Nothing looked broken; it just
   quietly stopped using the page. */
.pub-main { width: 100%; max-width: 1120px; margin: 0 auto; padding: 40px 32px 80px; }
.pub-footer {
  border-top: 1px solid var(--line);
  padding: 28px 32px 44px;
  text-align: center;
  font-size: 13px;
  color: var(--ink-2);
}
.pub-footer .muted { margin-top: 5px; font-size: 12.5px; }

.pub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
  gap: 26px;
}
/* Cards are equal height so the row reads as a set, and the footer sits on a
   shared baseline instead of wherever the description happens to end. */
.pub-card { text-decoration: none; color: inherit; display: flex; flex-direction: column; }
.pub-thumb {
  aspect-ratio: 4 / 3;
  border-radius: 12px;
  overflow: hidden;
  background: var(--paper);
  border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
}
.pub-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pub-thumb .placeholder { color: var(--ink-3); font-size: 13px; }
.pub-card:hover .pub-thumb { border-color: var(--line-2); }
.pub-card h3 { font: 500 16px var(--ui); margin: 0 0 4px; letter-spacing: -0.01em; }
/* Two lines, cut at a word by the browser rather than mid-syllable by us. */
.pub-card p {
  color: var(--ink-2); font-size: 13.5px; margin: 0; line-height: 1.5;
  display: -webkit-box; -webkit-line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden;
}

/* ---------------- shop, refined ---------------- */

.pub-card-body { display: flex; flex-direction: column; flex: 1; padding-top: 13px; }

.pub-card-foot {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-top: auto; padding-top: 12px;
}

/* Lead time is useful, not headline: it was the loudest thing on the card,
   competing with the product's own name. */
.pub-lead { font-size: 12px; color: var(--ink-3); }

.pub-empty {
  max-width: 460px; padding: 40px 0;
}
.pub-empty h2 { font-size: 20px; margin: 0 0 8px; letter-spacing: -0.01em; }
.pub-empty p { color: var(--ink-2); font-size: 14.5px; margin: 0; }

/* Something the page has to say for itself — a session that ended under a
   panel, a quote that has not started yet. Bordered so it reads as a state of
   the panel rather than as the agent having said it. */
.pub-note {
  padding: 22px; border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--sunken);
}
.pub-note p { margin: 0 0 10px; font-size: 14px; color: var(--ink-2); line-height: 1.55; }
.pub-note p:last-child { margin-bottom: 0; }
.pub-note strong { color: var(--ink); font-weight: 500; }

@media (max-width: 640px) {
  .pub-steps { gap: 12px; }
  .pub-steps li { font-size: 12.5px; }
}

/* The default price level: still a level, just not one anybody chose. */
.badge-quiet { background: transparent; border: 1px dashed var(--line-2); color: var(--ink-3); }

/* --- the account page ---------------------------------------------------
 *
 * Written once against classes that were never styled — .pub-cols, .pub-dl and
 * .pub-list had no rules at all, so the page rendered with browser defaults:
 * a definition list indented like a glossary, bulleted blue links, and a
 * two-column layout computing to display:block. Everything here is the styles
 * that markup was always asking for, plus the order it should have been in.
 *
 * What changed in the SHAPE: somebody arriving here wants to start something.
 * The old page led with their own address and offered no way to do anything,
 * while the copy told them they could place a quote whenever they liked.
 */

.pub-account-head { margin-bottom: 36px; }

.pub-account-head h1 {
  font: 500 30px var(--ui);
  letter-spacing: -0.028em;
  margin: 0 0 6px;
}

/* Where they stand, as one line under their name rather than a heading of its
   own. It is a fact about them, not a section. */
.pub-standing-line { color: var(--ink-2); font-size: 15px; margin: 0; }
.pub-standing-line strong { color: var(--ink); font-weight: 600; }

/* Unless something is wrong, in which case it IS the page. Somebody waiting on
   approval or on hold needs the reason before anything else, and a start
   button they cannot use is worse than none. */
.pub-standing {
  padding: 20px 22px;
  border-radius: var(--r-lg);
  background: var(--sunken);
  border: 1px solid var(--line);
  margin-bottom: 36px;
}

.pub-standing h2 { font: 500 18px var(--ui); margin: 0 0 6px; letter-spacing: -0.015em; }
.pub-standing p { margin: 0; color: var(--ink-2); font-size: 14.5px; }
.pub-standing-pending { background: var(--signal-bg); border-color: var(--signal-line); }
.pub-standing-declined,
.pub-standing-on_hold { background: var(--danger-bg); border-color: var(--danger); }

/* --- the two ways to start ---------------------------------------------- */

.pub-starts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 16px;
  margin-bottom: 44px;
}

/* One of the two is an <a> and the other is a <button>, because one opens a
   page and the other starts a conversation. They must not look like two
   different things.

   The form button_to wraps its button in is taken OUT of the layout, so the
   grid lays out the two cards rather than a card and a form. */
.pub-starts > form.button_to { display: contents; }

.pub-start {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 24px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--surface);
  text-decoration: none;
  color: inherit;
  transition: border-color .12s, transform .12s;

  /* Everything the global `button` rule imposes, undone. It sets padding,
     background, border, colour, font and centring on the ELEMENT, so the
     button card came out centred in a smaller face — and `button:hover`
     paints #000, which put near-black text on a black tile. The hover is the
     one people forget; it is the reason this comment is long. */
  width: 100%;
  text-align: start;
  font: inherit;
  cursor: pointer;
  appearance: none;
}

.pub-start:hover,
.pub-start:focus-visible {
  border-color: var(--ink);
  background: var(--surface);
  color: inherit;
  transform: translateY(-1px);
}

.pub-start:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* The icon carries the difference between the two, so it leads. Outlined and
   16px-grid like every other icon in this app rather than a filled pictogram:
   the shop and the back office are one product and their marks are drawn the
   same way. */
.pub-start-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-bottom: 14px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--sunken);
  color: var(--ink);
}
.pub-start-icon svg { width: 22px; height: 22px; }
.pub-start:hover .pub-start-icon { border-color: var(--line-2); }

.pub-start h2 {
  font: 500 19px var(--ui);
  margin: 0 0 6px;
  letter-spacing: -0.018em;
}

/* One line each, and no call to action under it. The whole card is the
   control, so a second "start a job" inside it was a button drawn on a button
   — and with the sentence gone to one line, the arrow was pointing at nothing
   the card did not already say. */
.pub-start p { margin: 0; color: var(--ink-2); font-size: 14.5px; line-height: 1.5; }

/* --- quotes ------------------------------------------------------------- */

.pub-section { margin-bottom: 44px; }

.pub-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}

.pub-section-head h2 { font: 500 15px var(--ui); margin: 0; letter-spacing: -0.01em; }
.pub-section-head a { font-size: 14px; color: var(--ink-2); }

.pub-quotes { list-style: none; margin: 0; padding: 0; }

.pub-quotes li {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: baseline;
  gap: 16px;
  padding: 13px 0;
  border-bottom: 1px solid var(--line);
}

.pub-quotes li:last-child { border-bottom: 0; }
.pub-quote-name { min-width: 0; }
.pub-quote-name a { text-decoration: none; font-weight: 500; color: var(--ink); }
.pub-quote-name a:hover { text-decoration: underline; }

/* The job, under its number. A list of numbers is a list nobody can read. */
.pub-quote-job {
  display: block;
  color: var(--ink-3);
  font-size: 13.5px;
  margin-top: 2px;
}

.pub-quote-total { font-family: var(--mono); font-size: 14.5px; white-space: nowrap; }

/* Which way it went. All four used to look identical, so "waiting on you" and
   "expired six weeks ago" read the same. */
.pub-state {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.02em;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--sunken);
  color: var(--ink-2);
  border: 1px solid var(--line);
  white-space: nowrap;
}

.pub-state-sent { background: var(--signal-bg); border-color: var(--signal-line); color: var(--signal); }
.pub-state-accepted { background: var(--ok-bg); border-color: var(--ok); color: var(--ok); }
.pub-state-rejected,
.pub-state-expired { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }

.pub-empty { color: var(--ink-3); font-size: 14.5px; margin: 0; }

/* --- their details, last ------------------------------------------------ */

.pub-dl {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 9px 20px;
  margin: 0;
  font-size: 14.5px;
}

.pub-dl dt { color: var(--ink-3); }
.pub-dl dd { margin: 0; color: var(--ink); }

@media (max-width: 640px) {
  .pub-quotes li { grid-template-columns: 1fr auto; }
  .pub-quote-total { grid-column: 1; }
  .pub-dl { grid-template-columns: 1fr; gap: 2px 0; }
  .pub-dl dt { margin-top: 10px; font-size: 13px; }
}

/* --- planning a job: a rail and a workspace ------------------------------ */

/* The conversation is a column of speech, so it is narrow and fixed. The
   schedule is a document that grows to twenty-two rows and keeps growing, so
   it takes everything else. The first attempt had these the other way round —
   a wide chat beside a 380px card — and the card was where all the numbers
   were going.

   Full bleed, because this is a workspace and not a document. The rest of the
   shop is centred at 1120px and reads like a catalogue; centring this left
   440px of empty paper down each side of a 2000px screen.

   Height by flexbox rather than by subtracting a guess from 100vh: a magic
   number is right on the machine it was measured on and wrong on every other
   one, and the header and the composer are both things that change size.

   The chain only works if something in it has a DEFINITE height. min-height
   was not enough — measured, the panes grew to 1920px and the whole page
   scrolled 1343px, which is the fault this was meant to fix. */
body.pub { display: flex; flex-direction: column; min-height: 100vh; }
.pub-main { flex: 1 1 auto; }

/* html as well as body. `overflow` set on body alone is PROPAGATED to the
   viewport and body itself goes on behaving as `visible` — so the panes grew
   and the page scrolled with every rule apparently in place. Constraining html
   is what actually gives the chain a height to divide. */
html:has(.job) { height: 100%; overflow: hidden; }
body.pub:has(.job) { height: 100%; min-height: 0; overflow: hidden; }

/* The footer is site chrome — an address and a link — and this is a working
   surface where nothing scrolls, so it could never be reached anyway. Hidden
   deliberately rather than clipped by accident. */
body.pub:has(.job) .pub-footer { display: none; }

.pub-main:has(.job) {
  display: flex;
  flex-direction: column;
  min-height: 0;
  max-width: none;
  margin: 0;
  padding: 0;
}

/* The FRAME is in the middle of this chain, and a chain is only as good as the
   box nobody styled. `main` measured 823px and the frame inside it 1920px: a
   turbo-frame is not a flex item anybody had thought about, so it sat at its
   content height and everything below it inherited that instead of the window.
   Nothing on screen said so — the panes just quietly ran off the bottom. */
.pub-main:has(.job) > turbo-frame {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* The SAME conversation, through the back office door.
   The chain above is built from <body> down, which the shop's layout can do
   because the page IS the conversation. A staff page has a head above it and
   an ordinary scrolling column around it, so the chain starts here instead —
   from a height rather than from the whole document.
   Measured before it existed: the panes sat at 272px on an empty conversation
   with the rest of the window blank underneath, because `.job` is
   `flex: 1 1 auto` and had no parent telling it what to fill. The frame is the
   link in the middle that nobody styles by accident. */
/* One line of chrome over the whole conversation, not a page head.
   The customer's name, where you came from, and the quote if there is one. */
.job-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin: 0 0 12px;
}
.job-head h1 { margin: 2px 0 0; font: 600 20px var(--ui); letter-spacing: -0.02em; }
.job-head .eyebrow { text-decoration: none; }
.job-head .eyebrow:hover { color: var(--ink); }

/* THE WHOLE WINDOW, not the 1240px column the rest of the back office reads
   in. Every other staff page is prose and tables at a comfortable measure; this
   one is a conversation beside a document that grows to thirty priced rows, and
   on a wide screen the column left the schedule scrolling sideways with the
   room names cut off while 1300px of desk sat empty beside it. */
.app-main:has(.job-page) { max-width: none; }

.job-page {
  display: flex;
  flex-direction: column;
  /* The chrome above it, plus the column's own padding. Measured rather than
     guessed — anything less scrolls the whole window by a dozen pixels, which
     reads as broken rather than as roomy. */
  height: calc(100vh - 152px);
  min-height: 420px;
}
.job-page > turbo-frame {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* The rail's width is a CUSTOM PROPERTY ON <html>, set by the grip. It cannot
   live on the pane: this whole grid is inside the job frame, and Turbo swaps
   that frame's children on every reply — a width written here would be thrown
   away by the next thing the agent said. */
.job {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: var(--rail, 470px) minmax(0, 1fr);
  align-items: stretch;
}

/* Over the hairline rather than a track of its own: a grid column wide enough
   to grab would be a visible gutter, and the edge here is a line. */
.job-grip {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--rail, 470px);
  width: 11px;
  margin-left: -6px;
  z-index: 2;
  cursor: col-resize;
  background: transparent;
}
.job-grip:hover::after,
.job-grip:focus-visible::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 5px;
  width: 1px;
  background: var(--ink-3);
}
.job-grip:focus-visible { outline: 2px solid var(--ink); outline-offset: -1px; }

/* While the edge is being dragged the cursor is the handle's wherever the
   pointer is, and nothing highlights — otherwise dragging left over the
   transcript selects every message it crosses. */
body.is-resizing { cursor: col-resize; user-select: none; }

/* --- the rail --- */

/* White against the workspace's paper, and separated by a hairline rather than
   a shadow: hairlines are how every other edge in this app is drawn, and one
   shadow would be the only shadow on the site. */
/* The rail is CHROME and the schedule is the SHEET. It was the other way
   round for one draft — a white rail beside a paper workspace — and at a 3%
   difference in tone neither looked like anything. The document you are
   filling in is the white one, which is also the way round a person would
   expect: you talk in the margin and write on the page. */
.job-talk {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  background: var(--paper);
  border-right: 1px solid var(--line);
}

/* The box you type in sits at the foot of the rail and stays there. It used to
   float at the bottom of a 700px void under one paragraph of conversation. */
.chat-ask {
  flex: none;
  padding: 16px 20px 20px;
  border-top: 1px solid var(--line);
  background: var(--paper);
}

/* --- the workspace --- */

.job-main {
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  background: var(--surface);
  padding: 30px 34px 0;
}

/* Capped, because a table row 1,700px wide is one your eye loses its place on
   between the room and the price. It sits left rather than centred — it is
   attached to the rail, not floating in the window. */
.job-main > * { max-width: 1200px; }

/* Stacked below 1200px, and the conversation FIRST: the thing you are doing
   comes before the thing you are watching. Nothing fills the window here — a
   single column that scrolls is what a narrow screen expects.
 *
 * It was 1000px, from before the schedule had six columns. The rail is a fixed
 * 470px, so between 1000 and 1200 the schedule was left 257–385px: still two
 * columns, and the second one too narrow to read a price in. Measured at 1152
 * — pane 385px — which is worse than stacking, because it looks like it works. */
@media (max-width: 1200px) {
  html:has(.job) { height: auto; overflow: visible; }
  body.pub:has(.job) { height: auto; min-height: 100vh; overflow: visible; }
  body.pub:has(.job) .pub-footer { display: block; }
  .pub-main:has(.job) { display: block; padding: 0 0 60px; }
  .pub-main:has(.job) > turbo-frame { display: block; }
  .job { display: block; }
  /* Nothing to drag when there is only one column. */
  .job-grip { display: none; }
  .job-talk { border-right: 0; border-bottom: 1px solid var(--line); }
  .chat-ask { position: static; }
  .job-main { overflow-y: visible; padding: 24px 32px 40px; }
  .chat-thread { flex: none; max-height: 60vh; }
}

.chat { display: flex; flex-direction: column; gap: 20px; }

.chat-thread {
  display: flex;
  flex-direction: column;
  gap: 14px;
  /* Takes whatever the composer and the buttons leave. The floor is low
     because on a short screen with a question open there is very little
     left — and a thread of two lines you can scroll beats a composer you
     cannot reach. */
  flex: 1 1 auto;
  min-height: 120px;
  overflow-y: auto;
  padding: 22px 20px;
}

.chat-turn { display: flex; }
.chat-turn-user { justify-content: flex-end; }

.chat-bubble {
  max-width: 88%;
  padding: 12px 16px;
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--line);
  font-size: 14.5px;
  line-height: 1.55;
}

.chat-bubble p { margin: 0 0 8px; }
.chat-bubble p:last-child { margin-bottom: 0; }

/* Theirs reads as theirs. Reversed rather than tinted, because a colour would
   have to mean something and this only means "you said this". */
.chat-turn-user .chat-bubble {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--surface);
}

.chat-files {
  list-style: none;
  margin: 10px 0 0;
  padding: 8px 0 0;
  border-top: 1px solid rgba(255, 255, 255, .2);
  font-size: 13px;
  font-family: var(--mono);
}

.chat-turn-assistant .chat-files { border-top-color: var(--line); }
.chat-files a { color: inherit; }
.chat-files a:hover { text-decoration-thickness: 2px; }

/* Working. Three dots rather than a spinner: it says "reading" rather than
   "loading", which is what is actually happening to a drawing somebody sent. */
.chat-thinking { display: flex; gap: 5px; align-items: center; padding: 16px; }

.chat-thinking span {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--ink-3);
  animation: chat-pulse 1.2s ease-in-out infinite;
}

.chat-thinking span:nth-child(2) { animation-delay: .15s; }
.chat-thinking span:nth-child(3) { animation-delay: .3s; }

@keyframes chat-pulse {
  0%, 60%, 100% { opacity: .25; }
  30% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .chat-thinking span { animation: none; opacity: .5; }
}

/* --- the composer -------------------------------------------------------- */

.chat-composer {
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  padding: 12px;
  background: var(--surface);
}

.chat-composer:focus-within { border-color: var(--ink); }

/* The upload is the point of this page, so it is a control you can see rather
   than a paperclip nobody finds. */
.chat-attach {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 11px;
  margin-bottom: 10px;
  border: 1px dashed var(--line-2);
  border-radius: 999px;
  font-size: 13.5px;
  color: var(--ink-2);
  cursor: pointer;
}

.chat-attach:hover { border-color: var(--ink); color: var(--ink); }
.chat-attach span[aria-hidden] { font-size: 15px; line-height: 1; }

.chat-chosen { margin-bottom: 10px; font-size: 13px; color: var(--ink-2); font-family: var(--mono); }
.chat-chosen[hidden] { display: none; }

.chat-row { display: flex; gap: 10px; align-items: flex-end; }

.chat-input {
  flex: 1;
  border: 0;
  resize: none;
  padding: 6px 4px;
  font: 400 15px var(--ui);
  color: var(--ink);
  background: none;
}

/* One ring, not two. The composer already says it has focus by taking a dark
   border — `:focus-within` above — and the app's global textarea:focus glow
   drew a second 3px ring INSIDE that border. Measured: box-shadow
   rgba(23,25,26,.09) 0 0 0 3px on the input, inside a 1px --ink border on the
   form around it. */
.chat-input:focus { outline: none; box-shadow: none; }

/* --- what the conversation has learned ---------------------------------- */

.pub-gathered {
  padding: 22px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--sunken);
}

.pub-gathered h2 { font: 500 14px var(--ui); margin: 0 0 12px; letter-spacing: -0.01em; }
.pub-gathered-list { margin: 0; padding-left: 20px; font-size: 14px; }
.pub-gathered-list li { margin-bottom: 11px; }
.pub-gathered-name { display: block; font-weight: 500; }
.pub-gathered-size { color: var(--ink-2); font-size: 13px; font-family: var(--mono); }

.pub-gathered-from {
  display: block;
  font-size: 12px;
  color: var(--ink-3);
}

.pub-gathered-brief { margin: 0; font-size: 13.5px; color: var(--ink-2); line-height: 1.55; }

.pub-gathered-sent {
  margin: 18px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-size: 13.5px;
  color: var(--ok);
}

@media (max-width: 900px) {
  .chat-bubble { max-width: 88%; }
}

.pub-gathered-send { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line); }
.pub-gathered-send .pub-cta { width: 100%; }

.pub-state-pending { background: var(--signal-bg); border-color: var(--signal-line); color: var(--signal); }

/* Beginning again. Quiet, beside the composer rather than above it — offering
   it loudly would read as "this probably will not work". */
.chat-foot {
  display: flex;
  align-items: baseline;
  gap: 9px;
  margin-top: 12px;
  font-size: 13px;
  color: var(--ink-3);
}

/* Everything the global button rule imposes, undone — including the hover
   background, which is the half that turns a control into a black tile. */
.chat-restart {
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-2);
  font: 400 13px var(--ui);
  text-decoration: underline;
  cursor: pointer;
}

.chat-restart:hover,
.chat-restart:focus-visible { background: none; color: var(--ink); }

/* Spelled out, because .chat-foot is display:flex and a `display:` rule beats
   the hidden attribute — the trap this codebase documents. */
.chat-foot[hidden] { display: none; }

/* Something went wrong. Marked, and still a message in the thread — a failure
   that renders as an empty page is the thing being fixed. */
.chat-bubble-trouble { background: var(--danger-bg); border-color: var(--danger); }

/* --- a question with its answers attached -------------------------------- */

.choices { margin: 4px 0 18px; }

.choices-question { margin: 0 0 4px; font: 500 15px var(--ui); }

.choices-why {
  margin: 0 0 12px;
  font-size: 13.5px;
  color: var(--ink-2);
  line-height: 1.5;
}

/* Even columns rather than a wrapped row. In a 400px rail the one carrying a
   note came out wider than the others and the ragged edge read as a fault. */
.choices-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.choices-form { display: contents; }

/* Everything the global button rule imposes, undone — background, border,
   colour, padding, font, centring, AND the hover paint, which is the half
   that turns a control into a black tile. */
.choice {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  min-width: 0;
  padding: 12px 15px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  text-align: start;
  cursor: pointer;
  transition: border-color .12s, transform .12s;
}

.choice:hover,
.choice:focus-visible {
  background: var(--surface);
  color: var(--ink);
  border-color: var(--ink);
  transform: translateY(-1px);
}

.choice:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* The colour itself, above its name. A band rather than a dot: a swatch has
   to be big enough to tell Linen from Alabaster, which a 12px chip is not.
   Bordered because a near-white cloth on a white card is otherwise an empty
   rectangle. */
.choice-swatch {
  display: block;
  width: 100%;
  height: 34px;
  margin-bottom: 5px;
  border: 1px solid var(--line-2);
  border-radius: var(--r-sm);
}

.choice-label { font-weight: 500; font-size: 14.5px; }
.choice-note { font-size: 13px; color: var(--ink-2); line-height: 1.45; }

/* What we would pick, marked rather than pre-selected. A default that is
   already chosen is a decision made for somebody who never looked. */
.choice-picked { border-color: var(--signal-line); background: var(--signal-bg); }
.choice-picked:hover, .choice-picked:focus-visible { background: var(--signal-bg); border-color: var(--signal); }

.choice-mark {
  margin-top: 4px;
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--signal);
}

.choices-hint { margin: 10px 0 0; font-size: 13px; color: var(--ink-3); }

/* --- the quote, in the conversation -------------------------------------- */

/* The schedule. The document of this trade — one row per opening, an ID, a
   room, a size — and what the whole conversation exists to complete.

   Ruled rather than boxed. A window schedule is a ruled sheet, and the app's
   own language is hairlines; a card around it would make it look like a
   summary of something else, which is the one thing it must not be. */
.sched-head { margin: 0 0 22px; }
.sched-head h1 {
  font: 500 25px/1.15 var(--ui);
  letter-spacing: -0.025em;
  margin: 0 0 6px;
}
.sched-count {
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  margin: 0; font-size: 13.5px; color: var(--ink-2);
}
/* The half that is actionable. Separated by a dot the way the rest of this app
   separates facts, and coloured because it is the only thing on the header
   line somebody has to do something about. */
.sched-waiting::before { content: "·"; margin-right: 9px; color: var(--line-2); }
.sched-waiting { color: var(--signal); }

/* WHAT THEY SENT, at the head of the sheet it produced.
   Not a panel and not a disclosure: two documents do not need hiding, and the
   whole point is that they are one click away while somebody is looking at the
   row they are checking. */
.sched-docs {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 12px;
}
.sched-docs-label {
  font: 500 11px/1 var(--ui);
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3);
  margin-right: 2px;
}

.sched-doc {
  display: inline-flex; align-items: center; gap: 8px;
  /* Never wider than the panel it sits in. A flat 320px overflowed the moment
     somebody dragged the rail across and left the schedule 257px wide. */
  max-width: min(320px, 100%);
  padding: 5px 11px 5px 7px;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--sunken);
  color: var(--ink); text-decoration: none; font-size: 13px;
}
.sched-doc:hover, .sched-doc:focus-visible { border-color: var(--ink-3); background: var(--paper); }
.sched-doc:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* The extension leads, because a PDF and a photo of a notebook page are
   different things to reach for — and identifying values are monospaced
   everywhere else in this app, which is why there is no icon set here. */
.sched-doc-kind {
  font-size: 10.5px; font-weight: 500; letter-spacing: .06em;
  color: var(--ink-3);
  padding: 2px 5px; border-radius: 3px; background: var(--surface);
  border: 1px solid var(--line);
}
.sched-doc-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* FIXED, so the schedule always fits the panel it is in.
 *
 * Auto layout asks the content how wide it wants to be, and on a real job the
 * answer was 1392px inside a 688px panel — so the whole schedule scrolled
 * sideways at every window size, which is not a thing you can read while
 * talking to somebody about it. The widths below are shares of the panel, so
 * it fits whatever the panel is; what is too long wraps, which for a room name
 * or a product name is the right answer and for a price never happens. */
.sched { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 14px; }
.sched th:nth-child(1), .sched td:nth-child(1) { width: 22%; }
.sched th:nth-child(2), .sched td:nth-child(2) { width: 26%; }
.sched th:nth-child(3), .sched td:nth-child(3) { width: 18%; }
.sched th:nth-child(4), .sched td:nth-child(4) { width: 11%; }
.sched th:nth-child(5), .sched td:nth-child(5) { width: 10%; }
.sched th:nth-child(6), .sched td:nth-child(6) { width: 13%; }
.sched th {
  text-align: left;
  font: 500 11px/1 var(--ui);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 0 14px 9px 0;
  border-bottom: 1px solid var(--line-2);
  white-space: nowrap;
}
.sched th.num, .sched td.num { text-align: right; }
.sched th:last-child, .sched td:last-child { padding-right: 0; }
.sched td {
  padding: 13px 14px 13px 0;
  border-bottom: 1px solid var(--line);
  vertical-align: baseline;
}
.sched td.num { white-space: nowrap; }

/* THE WINDOW COLUMN WRAPS, and that is what keeps the schedule on the screen.
 *
 * `width: 1%` means shrink to fit. With `white-space: nowrap` beside it, it
 * does the opposite: the cell cannot break, so it grows to the longest room
 * name on the job and takes the table with it. Measured on a 29-row school
 * job — Window alone was 882px of a 1462px table inside a 688px panel, so the
 * whole schedule scrolled sideways at every window size.
 *
 * The CODE still cannot break, because "101A" split across two lines is not an
 * identifier any more. The room can, and does. */
.sched-w { width: 1%; }
/* NOT nowrap, which was the next thing holding the table open at 798px. A
   rolled-up row carries every opening it covers in one span — "Line 104 (172)
   - Split 59 1/4, Line 104 (237) - Split …" — and forbidding a break there
   forbids it for the whole list. Left alone, it breaks at the spaces it
   already has, and a code with no spaces in it still cannot break. */
.sched-room { overflow-wrap: anywhere; }

/* The opening's ID is the thing somebody scans down looking for their room, so
   it leads and it is monospaced — the same rule the rest of the app uses for
   anything measured. The room is what tells them which one it is. */
/* The window's name, which is a button. The global `button` rule paints a dark
   pill — padding, background, border, colour AND the hover paint, which is the
   half that turns a control into a black tile — so all five are undone here.
   It reads as the identifier it is, and underlines when you go near it. */
.sched-pick { display: inline; }
button.sched-code,
.sched-code {
  font-weight: 500;
  font-family: var(--mono);
  font-size: inherit;
}
button.sched-code {
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--line-2);
  text-underline-offset: 3px;
}
button.sched-code:hover,
button.sched-code:focus-visible {
  background: none;
  color: var(--ink);
  text-decoration-color: var(--ink);
}
button.sched-code:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.sched-room { margin-left: 9px; color: var(--ink-2); font-size: 13px; }
.sched-opts {
  display: block; margin-top: 3px;
  font-size: 12.5px; color: var(--ink-2); line-height: 1.6;
}

/* A swatch and the colour it is are one thing. Left to wrap, the chip ended a
   line and its name started the next one, which reads as a stray mark. */
.sched-opt { white-space: nowrap; }

/* A window that is on the schedule and has nothing chosen for it yet. Present,
   quiet, and NOT hidden: a total covering eighteen of twenty-two windows is
   the number somebody signs off on, and this row is the only thing on screen
   that says there are four more. */
.sched-open { background: var(--sunken); }
/* Prose, not a measurement — it sits in the money column and italic monospace
   reads as a broken number rather than as a note. */
.sched-todo { font-family: var(--ui); font-size: 13px; color: var(--ink-3); }
.sched-dash { color: var(--line-2); }

.sched-empty td {
  padding: 4px 0;
  color: var(--ink-3);
  font-size: 14.5px;
  border-bottom: 0;
}

/* Pinned to the foot of the sheet. Twenty-two rows is more than a screen, and
   the total is the one number somebody is watching while they scroll — it
   should not be a thing you go and find. The only sticky element on the site,
   and it earns it. */
/* The totals on a quote. There was NO CSS for this at all — the block that
   carries the money on the document a customer sends round was three unstyled
   divs. */
.pub-totals {
  display: flex; flex-direction: column; gap: 8px;
  margin: 26px 0 0; padding-top: 18px;
  border-top: 1px solid var(--line-2);
  font-size: 14.5px; color: var(--ink-2);
}
.pub-totals > div { display: flex; justify-content: flex-end; gap: 26px; }
.pub-totals > div > span:last-child { min-width: 140px; text-align: right; }
.pub-totals .grand {
  margin-top: 6px; padding-top: 12px;
  border-top: 1px solid var(--line);
  font-size: 17px; color: var(--ink);
}
.pub-totals .grand span:last-child { font-size: 21px; font-weight: 500; letter-spacing: -0.02em; }

/* What came off, and why. Coloured because it is the one line on a quote that
   is good news, and named because "your discount" invites the question. */
.pub-off { color: var(--signal); }
.pub-off em { font-style: normal; color: var(--ink-3); font-size: 13px; }

/* Why there is no tax on it. Evidence, not decoration. */
.pub-tax-note { font-size: 13px; color: var(--ink-3); }

/* What came off this row. Coloured like the summary line it adds up to, so
   the eye can follow one down the column and land on the total. */
.sched-off-cell { color: var(--signal); white-space: nowrap; }
.sched-off-cell .sched-dash { color: var(--line-2); }

/* The arithmetic above the total: list, what came off, and the tax. Right
   against the money column so the numbers line up with the rows above them. */
.sched-sums {
  display: flex; flex-direction: column; gap: 6px;
  margin-bottom: 10px; font-size: 13.5px; color: var(--ink-2);
}
.sched-sums > div { display: flex; justify-content: flex-end; gap: 22px; }
.sched-sums > div > span:last-child { min-width: 120px; text-align: right; }
.sched-off { color: var(--signal); }
.sched-off em { font-style: normal; color: var(--ink-3); font-size: 12.5px; }

/* Pinned to the foot of the sheet, and ALL of the money with it. Twenty-two
   rows is more than a screen, and the arithmetic is what somebody is watching
   while they scroll — it should not be a thing you go and find. List and
   discount used to sit above this and scroll away, which left the one number
   that stayed as the only one you could not check. */
.sched-foot {
  position: sticky;
  bottom: 0;
  margin-top: -1px;
  padding: 14px 0 18px;
  background: var(--surface);
  border-top: 1px solid var(--line-2);
}

.sched-total {
  display: flex; align-items: baseline; justify-content: flex-end;
  gap: 22px;
}
.sched-total span { font-size: 13.5px; color: var(--ink-2); }
.sched-total strong { font-size: 23px; font-weight: 500; letter-spacing: -0.02em; }

.sched-note { margin: 16px 0 0; font-size: 13px; color: var(--ink-3); line-height: 1.55; }

/* --- markdown inside a bubble -------------------------------------------
 *
 * The same renderer the back office panel uses, which emits .ap-h for a
 * heading and .ap-table for a table. Its own sizes here: the panel is a 600px
 * drawer and this is a conversation somebody reads at full width.
 */
.chat-bubble .ap-h { margin: 14px 0 6px; font: 500 15px var(--ui); }
.chat-bubble .ap-h:first-child { margin-top: 0; }
.chat-bubble ul, .chat-bubble ol { margin: 8px 0; padding-left: 20px; }
.chat-bubble li { margin-bottom: 5px; }
.chat-bubble li:last-child { margin-bottom: 0; }
.chat-bubble li > ul, .chat-bubble li > ol { margin: 5px 0 0; }
.chat-bubble code { font-size: 13px; background: rgba(0, 0, 0, .05); padding: 1px 4px; border-radius: 3px; }
.chat-bubble hr { border: 0; border-top: 1px solid var(--line); margin: 14px 0; }

/* Theirs is reversed, so a tint that works on white does not work on it. */
.chat-turn-user .chat-bubble code { background: rgba(255, 255, 255, .16); }
.chat-turn-user .chat-bubble hr { border-top-color: rgba(255, 255, 255, .25); }

.chat-bubble .ap-table { overflow-x: auto; margin: 10px 0; }
.chat-bubble .ap-table table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.chat-bubble .ap-table th,
.chat-bubble .ap-table td { padding: 7px 10px 7px 0; text-align: left; border-bottom: 1px solid var(--line); }
.chat-bubble .ap-table th { font-weight: 500; color: var(--ink-3); font-size: 12px; text-transform: uppercase; letter-spacing: .05em; }

/* A colour beside its name in the running total. Small — this is a table of
   money, and the swatch is there so nobody has to remember which grey. */
.chat-swatch {
  display: inline-block; width: 10px; height: 10px; border-radius: 2px;
  margin-right: 5px; vertical-align: middle; border: 1px solid var(--line-2);
}

/* The quote's own controls, on the screen it is worked on.
 *
 * A row rather than the page-head cluster they came from: this sheet has no
 * page head — the conversation's heading is the customer — so they sit above
 * the document they act on, wrap when there are six of them, and are separated
 * from the sheet by the same hairline everything else here is.
 *
 * Staff only, and never rendered on the shop: see job_conversations/_quote. */
.sched-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 22px;
  padding: 0 0 20px;
  border-bottom: 1px solid var(--line);
}
/* button_to renders a form around every one of them, and a form is a block. */
.sched-actions form { margin: 0; }
