/* Bristle: the corner assistant.
 *
 * Loaded only on customer-facing surfaces (partials/site-assistant.html.raw).
 * Everything here is a real stylesheet rather than the inline style attributes
 * QuoteChecker's Quotey uses, because this site's style-src is
 * `'self' 'nonce-Y'` with no 'unsafe-inline' (lib/shared/render.ts) and
 * scripts/validate-render-security.mjs asserts that. Inline styling would have
 * to weaken the policy; a stylesheet does not.
 *
 * The one rule the whole file exists to protect: the launcher's box never
 * moves. Every animation runs on .tr-bristle__pivot inside it. A transform on
 * a child cannot change the parent's layout box, so the click target stays
 * exactly where the pointer, the keyboard, assistive technology and the
 * browser test harness last found it, no matter what the character is doing.
 */

:root {
  /* Sized so the face reads. The art is a tall canvas that is mostly handle,
     so a height that looks generous as a number renders a small head. */
  --bristle-h: 208px;
  --bristle-w: 144px;
  /* Clear of the viewport edge, and clear of the home indicator on phones
     that have one. Quotey does not handle safe areas; on an iPhone the
     launcher sat under the gesture bar. */
  --bristle-inset-x: 18px;
  /* 20px, not 10. The sweep rotates the art about the bristles, which swings
     the lower corners about 11px past the resting box; at a 10px inset the
     leading edge of every sweep clipped on the viewport's bottom edge. */
  --bristle-inset-y: calc(20px + env(safe-area-inset-bottom, 0px));
  /* Above the sticky site header (50) and its dropdown (60); below the skip
     link (100), which has to stay reachable as the first tab stop. */
  --bristle-z: 80;
  --bristle-panel-z: 90;
  --bristle-panel-w: 384px;
  /* Space between the panel and the broom. Close enough to read as one unit,
     wide enough that the sweep's swing never overlaps the panel edge. */
  --bristle-panel-gap: 12px;
}

/* ── launcher ───────────────────────────────────────────────────────────── */

/* Measures the box `position: fixed` resolves against. Never painted, never
   hit-tested, and removed as soon as it has been read. */
.tr-bristle-probe {
  position: fixed;
  inset: 0;
  visibility: hidden;
  pointer-events: none;
}

.tr-bristle {
  position: fixed;
  /* Offset by however far the fixed box overhangs the visible frame, which is
     0px everywhere except a page whose own content overflows sideways. Without
     this the broom hung off the bottom-right corner of the home page on a
     phone. */
  right: calc(var(--bristle-inset-x) + var(--bristle-frame-gap-x, 0px));
  bottom: calc(var(--bristle-inset-y) + var(--bristle-frame-gap-y, 0px));
  z-index: var(--bristle-z);
  /* Explicit reset, not tidiness. Marketing pages ship generic `button {}`
     rules for margin, font, border and background; any of them would move or
     reshape a bottom-anchored fixed button. */
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  line-height: 0;
  cursor: pointer;
  width: var(--bristle-w);
  height: var(--bristle-h);
  /* The box is fixed at the art's aspect ratio and never derived from the
     image, so a pose that fails to load leaves a hole rather than a collapsed
     target that silently stops being clickable. */
  display: block;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

.tr-bristle:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 4px;
  border-radius: var(--radius-md);
}

/* The cast shadow on the floor.

   Separate from the drop-shadow on the art, and not a duplicate of it: that one
   is a FORM shadow and travels with the character, so it leans when he leans.
   This one is the patch of floor he is standing on. It stays put while he moves
   and breathes counter to him, and that disagreement between the two is what
   reads as contact rather than as a sticker with a glow.

   A sibling of the moving layers, so their transforms cannot drag it.

   Sized from the art, not from taste: measured off the rendered launcher, the
   bristles are ~62% of the box wide at y=90%, taper to ~36% at 95.5% and ~11%
   at 97%, and stop at 97.5%. The first version sat at bottom 4% and 46% wide,
   which put the whole ellipse inside the widest part of the silhouette - it
   rendered, was completely occluded, and the breathing counter-phase it exists
   to carry was invisible. Down at the tapered tip and wider than it, the pool
   spreads either side of where he actually meets the floor. */
.tr-bristle__shadow {
  position: absolute;
  left: 50%;
  bottom: 1.5%;
  width: 64%;
  height: 5%;
  margin-left: -32%;
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(16, 39, 71, 0.34), rgba(16, 39, 71, 0) 72%);
  pointer-events: none;
  transform-origin: 50% 50%;
  will-change: transform, opacity;
}

/* Three layers, one per kind of movement, because `transform-origin` is per
   element: travel has no meaningful origin, the sweep rotates about the
   bristles, and the squash compresses toward the floor.

   All three drive the INDEPENDENT `translate` / `rotate` / `scale` properties
   rather than one `transform`, so breathing, a lean and a squash compose in the
   compositor instead of the last one written winning. */
.tr-bristle__lift {
  display: block;
  width: 100%;
  height: 100%;
  will-change: translate;
}

/* The element every rotation acts on. Separate from the button so the button's
   geometry is untouched, and separate from the <img> so a rotation and a squash
   can run at the same time without fighting over one transform. */
.tr-bristle__pivot {
  display: block;
  width: 100%;
  height: 100%;
  transform-origin: 50% 92%;
  will-change: transform, rotate;
}

.tr-bristle__squash {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  transform-origin: 50% 100%;
}

/* ── eyelids ─────────────────────────────────────────────────────────────
   Two head-coloured ellipses over the eye whites, flat at rest and scaled shut
   to blink. Not a redraw and not an image layer: the fill is the navy sampled
   from the art immediately around the eyes, where it varies by less than 6/255
   per channel, so a flat colour is invisible against it.

   Geometry comes from assistant.js in percentages of the launcher box, which is
   what lets one set of numbers serve the 144x208 desktop mascot and the 78x112
   mobile one. They are oversized ~40% so their edges land on the eyes' own
   black outline rather than on the head, which makes the fit forgiving. */
.tr-bristle__lids {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.tr-bristle__lid {
  position: absolute;
  border-radius: 50%;
  background: #39465C;
  /* Closes from the top, the way an eyelid does. Closing from the middle reads
     as a shutter. */
  transform-origin: 50% 12%;
  /* Open. The blink scales to 1 and back. */
  scale: 1 0;
  will-change: scale;
}

/* ── press accent ────────────────────────────────────────────────────────
   A few grains off the bristles when he is pressed. Six spans that remove
   themselves, transform and opacity only. Deliberately not a particle system:
   nothing loops and nothing is left behind. */
.tr-bristle-dust {
  position: fixed;
  z-index: var(--bristle-z);
  width: 4px;
  height: 4px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  background: var(--color-accent, #C9A24D);
  box-shadow: 0 0 6px rgba(201, 162, 77, 0.75);
  pointer-events: none;
}

.tr-bristle__pose {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* The character is drawn without a shadow. A soft one under the bristles is
     what keeps it from looking pasted onto the page. Grounding only: no halo
     behind the art. */
  filter: drop-shadow(0 10px 14px rgba(16, 39, 71, 0.28));
  pointer-events: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* A hover affordance that costs no motion, so it survives reduced-motion
   intact and gives touch users something too. */
.tr-bristle:hover .tr-bristle__pose,
.tr-bristle:focus-visible .tr-bristle__pose {
  filter: drop-shadow(0 12px 18px rgba(16, 39, 71, 0.3));
}

/* ── panel ──────────────────────────────────────────────────────────────── */

.tr-bristle-panel {
  position: fixed;
  /* Beside the broom, not on top of it.
     Stacking the panel above the launcher pushed the composer a full mascot
     height up the screen, so the box you type into sat nowhere near the
     character you are typing to. Sitting to its left puts the composer
     directly beside the broom: the two read as one object, and the eye travels
     from what you typed to who is answering without crossing the panel. */
  right: calc(
    var(--bristle-inset-x) + var(--bristle-frame-gap-x, 0px)
    + var(--bristle-w) + var(--bristle-panel-gap)
  );
  /* Bottom-aligned with the launcher, which is what puts the composer level
     with the broom rather than level with its handle. */
  bottom: calc(var(--bristle-inset-y) + var(--bristle-frame-gap-y, 0px));
  z-index: var(--bristle-panel-z);
  width: var(--bristle-panel-w);
  /* Room now comes off the side rather than the top, so the panel is bounded
     by what is left after the broom and both insets. */
  max-width: calc(
    100vw - (var(--bristle-inset-x) * 2) - var(--bristle-w) - var(--bristle-panel-gap)
  );
  /* Taller than before: nothing is above it to clear. */
  max-height: min(620px, calc(100vh - (var(--bristle-inset-y) * 2)));
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--color-white);
  color: var(--color-text);
  border-radius: var(--radius-xl);
  /* A real black outline, not a faint tint. The panel floats over photography
     and over the dark hero, and a 6%-opacity hairline disappeared against
     both, leaving the white card bleeding into whatever was behind it. */
  border: 2px solid var(--color-ink, #101010);
  box-shadow: var(--shadow-lg);
  font-family: var(--font-sans);
}

.tr-bristle-panel[hidden] { display: none; }

.tr-bristle-panel__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-3);
  background: var(--color-header);
  color: var(--color-white);
}

/* The character, in the panel. Without it the panel is a generic chat box that
   happened to open near a broom. */
.tr-bristle-panel__avatar {
  flex: none;
  width: 34px;
  height: 49px;
  object-fit: contain;
  object-position: bottom center;
}

.tr-bristle-panel__identity { flex: 1 1 auto; min-width: 0; }
.tr-bristle-panel__name {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 700;
  letter-spacing: -0.01em;
}
.tr-bristle-panel__role {
  margin: 0;
  font-size: var(--fs-xs);
  color: rgba(255, 255, 255, 0.78);
}

.tr-bristle-panel__close {
  flex: none;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.12);
  color: var(--color-white);
  font-size: var(--fs-md);
  line-height: 1;
  cursor: pointer;
}
.tr-bristle-panel__close:hover { background: rgba(255, 255, 255, 0.22); }
.tr-bristle-panel__close:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ── conversation ───────────────────────────────────────────────────────── */

.tr-bristle-panel__log {
  flex: 1 1 auto;
  min-height: 132px;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  background: var(--color-off-white);
}

.tr-bristle-msg {
  max-width: 88%;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-lg);
  font-size: var(--fs-sm);
  line-height: 1.5;
  /* Long addresses and order numbers are exactly what somebody pastes into a
     chat, and they have nowhere to break. */
  overflow-wrap: anywhere;
}

.tr-bristle-msg--customer {
  align-self: flex-end;
  background: var(--color-primary);
  color: var(--color-white);
  border-bottom-right-radius: var(--radius-xs);
}

.tr-bristle-msg--assistant {
  align-self: flex-start;
  background: var(--color-white);
  color: var(--color-text);
  border-bottom-left-radius: var(--radius-xs);
  box-shadow: var(--shadow-xs);
}

/* Openers in the empty state.
   Sized and spaced as real targets rather than text links: on a phone these
   are the first thing a thumb reaches for, and a 32px pill is the difference
   between a tap and three tries. */
.tr-bristle-starters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.tr-bristle-starter {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  min-height: 32px;
  border-radius: 999px;
  border: 1px solid var(--color-border, rgba(16, 39, 71, 0.16));
  background: var(--color-white);
  color: var(--color-text);
  font: inherit;
  font-size: var(--fs-xs, 12px);
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
  transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease;
}

.tr-bristle-starter:hover,
.tr-bristle-starter:focus-visible {
  border-color: var(--color-primary, #1d4ed8);
  background: var(--color-off-white);
}

.tr-bristle-starter:active { transform: translateY(1px); }

/* The way back from a failure. An apology with no action makes the visitor
   retype what they already typed, and most of them just leave instead. */
.tr-bristle-retry {
  margin: var(--space-2) 0 0;
  padding: var(--space-1) var(--space-3);
  min-height: 32px;
  border-radius: 999px;
  border: 1px solid currentColor;
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: var(--fs-xs, 12px);
  font-weight: 600;
  cursor: pointer;
}

.tr-bristle-retry:hover,
.tr-bristle-retry:focus-visible { background: rgba(255, 255, 255, 0.5); }

/* A disabled send should look unavailable, not broken. */
.tr-bristle-panel__send:disabled {
  opacity: 0.42;
  cursor: default;
}

/* Source chips: what the answer rested on.
   Rendered under the reply rather than inline, because a customer reads the
   answer first and checks the source second. A chip with a path is a link; a
   chip without one is a plain label, which is how an answer grounded in the
   customer's own account is shown without inventing a URL for it. */
.tr-bristle-sources {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.tr-bristle-source {
  display: inline-block;
  padding: 2px var(--space-2);
  border-radius: 999px;
  border: 1px solid var(--color-border, rgba(0, 0, 0, 0.14));
  background: var(--color-surface-muted, rgba(0, 0, 0, 0.03));
  color: var(--color-text-muted, #55606b);
  font-size: 11px;
  line-height: 1.7;
  text-decoration: none;
  /* A long page title should not stretch the bubble past its own max-width. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

a.tr-bristle-source:hover,
a.tr-bristle-source:focus-visible {
  color: var(--color-text);
  border-color: currentColor;
}

/* While text is still arriving it is provisional: the server has not yet
   accepted it, and a citation failure will clear it. The caret is the only
   signal that this is not a finished answer. */
.tr-bristle-msg--streaming::after {
  content: '';
  display: inline-block;
  width: 0.5em;
  height: 1em;
  margin-left: 1px;
  vertical-align: -0.15em;
  background: currentColor;
  opacity: 0.55;
  animation: tr-bristle-caret 1s steps(2, start) infinite;
}

@keyframes tr-bristle-caret {
  50% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .tr-bristle-msg--streaming::after { animation: none; }
}

.tr-bristle-msg--notice {
  align-self: stretch;
  max-width: 100%;
  background: var(--color-primary-light);
  color: var(--color-primary);
  border-radius: var(--radius-md);
  font-size: var(--fs-xs);
}

.tr-bristle-msg--error {
  align-self: stretch;
  max-width: 100%;
  background: #FDF0EF;
  color: var(--color-danger);
  border-radius: var(--radius-md);
  font-size: var(--fs-xs);
  font-weight: 600;
}

/* Welcome state. Not an empty panel: a greeting and the things Bristle will be
   able to help with, so an operator can see what the surface is for. */
.tr-bristle-welcome { display: grid; gap: var(--space-3); }
.tr-bristle-welcome__lead {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--color-text);
}
.tr-bristle-welcome__hint {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--color-muted);
}

/* Thinking indicator. Three dots that fade in sequence; under reduced motion
   the animation is dropped and the word carries it instead. */
.tr-bristle-thinking {
  align-self: flex-start;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--color-white);
  box-shadow: var(--shadow-xs);
  font-size: var(--fs-xs);
  color: var(--color-muted);
}
.tr-bristle-thinking__dots { display: inline-flex; gap: 3px; }
.tr-bristle-thinking__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-primary);
  opacity: 0.35;
  animation: tr-bristle-pulse 1.15s ease-in-out infinite;
}
.tr-bristle-thinking__dot:nth-child(2) { animation-delay: 0.16s; }
.tr-bristle-thinking__dot:nth-child(3) { animation-delay: 0.32s; }

@keyframes tr-bristle-pulse {
  0%, 100% { opacity: 0.28; transform: translateY(0); }
  50% { opacity: 1; transform: translateY(-2px); }
}

/* ── composer ───────────────────────────────────────────────────────────── */

.tr-bristle-panel__composer {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--color-white);
  border-top: 1px solid var(--color-rule);
}

.tr-bristle-panel__input {
  flex: 1 1 auto;
  min-width: 0;
  /* Grows with the message up to a ceiling, then scrolls. A single-line input
     for something a customer might paste an address into is a bad trade. */
  min-height: 44px;
  max-height: 108px;
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-white);
  color: var(--color-text);
  font-family: var(--font-sans);
  /* 16px exactly. Anything smaller and iOS Safari zooms the page on focus,
     which on a fixed-position panel leaves the composer off screen. */
  font-size: 16px;
  line-height: 1.4;
  resize: none;
}
.tr-bristle-panel__input:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
  border-color: var(--color-primary);
}

.tr-bristle-panel__send {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  background: var(--color-primary);
  color: var(--color-white);
  cursor: pointer;
  transition: background-color var(--dur-quick) ease;
}
.tr-bristle-panel__send:hover:not(:disabled) { background: var(--color-primary-dark); }
.tr-bristle-panel__send:disabled { opacity: 0.4; cursor: not-allowed; }
.tr-bristle-panel__send:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}
.tr-bristle-panel__send svg { width: 20px; height: 20px; display: block; }

/* ── narrow viewports ───────────────────────────────────────────────────── */

/* The small broom belongs to every full-bleed viewport, not only to phones.
   At 920px the estimate and signing layouts centre their content into a grid
   and leave a right-hand gutter the broom can stand in. Below that the form
   runs to within about 57px of the right edge, and a 144px broom inset 20px
   covers the right 105px of the 140px Next button — its centre included. The
   control was not clickable at all on a 768px tablet: Playwright reported the
   squash layer intercepting the press, which is exactly what a thumb would
   have found. Measured on the wizard: the button's centre is under the broom
   at 641, 768 and 860, and clear at 960 and above.

   Only the size lives here. The bottom-sheet panel below is a phone decision
   and keeps its own narrower query. */
@media (max-width: 919px) {
  :root {
    /* A phone is not a small desktop. At 148px the broom was 17.5% of the
       screen's height and a quarter of its width, which is a lot of furniture
       for something standing in a corner waiting to be asked a question.
       112px keeps the face readable at arm's length and gives the page back
       most of that space. The 0.69 aspect is the art's and is preserved
       exactly, or the shared crop box stops lining up between poses. */
    --bristle-h: 112px;
    --bristle-w: 78px;
    --bristle-inset-x: 12px;
    --bristle-inset-y: calc(14px + env(safe-area-inset-bottom, 0px));
  }
}

@media (max-width: 40rem) {
  /* A bottom sheet, not a shrunken corner card. At 390px a corner panel leaves
     a composer around 300px wide with a keyboard over it; the sheet gives the
     conversation the width of the screen and puts the composer where a thumb
     already is. */
  .tr-bristle-panel {
    /* Anchored left, not right: `right: 0` also resolves against the layout
       viewport, so on an overflowing page the correctly-sized sheet was still
       pushed sideways by the difference. */
    left: 0;
    right: auto;
    bottom: var(--bristle-frame-gap-y, 0px);
    /* Measured frame width, not the layout viewport: a host page with its own
       horizontal overflow widens the latter and would push the composer's send
       button off screen. Falls back to 100% where the script has not run. */
    width: var(--bristle-frame-w, 100%);
    max-width: 100%;
    /* 82vh let a long conversation swallow the whole screen, and even an empty
       one came to half of it. Two thirds is enough to read several turns while
       leaving the page visible behind, which is the point of a non-modal
       assistant: people ask about the thing they are looking at. */
    max-height: min(66vh, calc(100vh - 96px));
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }

  /* The chrome is trimmed rather than the conversation. Header and composer
     were 150px of the 431px an empty sheet occupied; the words the customer
     came for got what was left. */
  .tr-bristle-panel__head {
    padding: var(--space-3) var(--space-3) var(--space-2);
  }

  .tr-bristle-panel__avatar {
    width: 26px;
    height: 38px;
  }

  .tr-bristle-panel__log {
    min-height: 120px;
    padding: var(--space-3);
    gap: var(--space-2);
  }

  .tr-bristle-panel__composer {
    padding: var(--space-2);
  }

  /* The input keeps its 44px target and its 16px type: anything smaller and
     iOS zooms the page on focus, which on a fixed sheet puts the composer off
     screen. Only the growth ceiling comes down. */
  .tr-bristle-panel__input {
    max-height: 84px;
  }

  /* Openers stay full-size targets; only the space around them tightens. */
  .tr-bristle-starters {
    gap: 6px;
    margin-top: var(--space-2);
  }

  .tr-bristle-starter {
    padding: 6px var(--space-3);
  }
}

/* ── reduced motion ─────────────────────────────────────────────────────── */

/* The character goes still. Every function stays: hovering and focusing still
   change the shadow, clicking still opens the panel, the panel still works.
   assistant.js checks the same query and skips the pose swaps entirely, so
   this block is the second lock rather than the only one. */
@media (prefers-reduced-motion: reduce) {
  /* Every property the character can be moved on, pinned, so even a stale
     animation cannot move him.

     The pose still CHANGES under reduced motion - a working broom and a resting
     one are different drawings - because a state that can only be told by
     movement is a state that is not told at all to someone who asked for none. */
  .tr-bristle__lift,
  .tr-bristle__pivot,
  .tr-bristle__squash,
  .tr-bristle__shadow {
    transform: none !important;
    translate: none !important;
    rotate: none !important;
    scale: none !important;
  }
  /* The lid's resting state IS a scale, so it cannot be unset with the rest:
     `scale: none` would open two navy ellipses over his eyes. */
  .tr-bristle__lid {
    scale: 1 0 !important;
    animation: none !important;
  }
  .tr-bristle-dust { display: none; }
  .tr-bristle-thinking__dot {
    animation: none;
    opacity: 0.55;
  }
}
